@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,167 @@
1
+ /**
2
+ * Babylon's GUI on the screen: a panel of the game's own drawn into the client's full-screen GUI layer (`client.gui`,
3
+ * an `AdvancedDynamicTexture`), with Babylon's controls (`@babylonjs/gui`). It reads the battle from the snapshots the
4
+ * screen is sent, and acts by commands and the client's settings. Everything added is taken away as the example is left
5
+ */
6
+ import { Button } from '@babylonjs/gui/2D/controls/button.js'
7
+ import { Checkbox } from '@babylonjs/gui/2D/controls/checkbox.js'
8
+ import { Control } from '@babylonjs/gui/2D/controls/control.js'
9
+ import { Rectangle } from '@babylonjs/gui/2D/controls/rectangle.js'
10
+ import { Slider } from '@babylonjs/gui/2D/controls/sliders/slider.js'
11
+ import { StackPanel } from '@babylonjs/gui/2D/controls/stackPanel.js'
12
+ import { TextBlock } from '@babylonjs/gui/2D/controls/textBlock.js'
13
+ import type { GameClientApi } from '@v12-engine/client'
14
+ import { ENEMY, PLAYER } from '../common/units'
15
+ import { charge, resetCamp } from './index'
16
+
17
+ const GOLD = '#d9b45a'
18
+ const INK = '#f1e4c3'
19
+ const FONT = 'Georgia, serif'
20
+
21
+ /** Words in the panel */
22
+ function words(text: string, size: number, color = INK, height = size + 10): TextBlock {
23
+ const block = new TextBlock(undefined, text)
24
+ block.color = color
25
+ block.fontSize = size
26
+ block.fontFamily = FONT
27
+ block.height = `${height}px`
28
+ block.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT
29
+ return block
30
+ }
31
+
32
+ /** A button of the panel: dark, gold-edged, lighter as the pointer is over it */
33
+ function panelButton(name: string, text: string, run: () => void): Button {
34
+ const made = Button.CreateSimpleButton(name, text)
35
+ made.height = '36px'
36
+ made.width = '254px'
37
+ made.color = GOLD
38
+ made.background = '#2a2214'
39
+ made.cornerRadius = 6
40
+ made.thickness = 1.5
41
+ made.fontFamily = FONT
42
+ made.fontSize = 16
43
+ made.paddingTop = '6px'
44
+ if (made.textBlock) made.textBlock.color = INK
45
+ made.onPointerEnterObservable.add(() => (made.background = '#46381c'))
46
+ made.onPointerOutObservable.add(() => (made.background = '#2a2214'))
47
+ made.onPointerUpObservable.add(run)
48
+ return made
49
+ }
50
+
51
+ /** A bar for a side's strength: a frame and a fill as wide as the share left */
52
+ function strengthBar(color: string): { bar: Rectangle; set: (share: number) => void } {
53
+ const bar = new Rectangle()
54
+ bar.height = '14px'
55
+ bar.thickness = 1
56
+ bar.color = '#00000088'
57
+ bar.background = '#00000066'
58
+ bar.cornerRadius = 3
59
+ const fill = new Rectangle()
60
+ fill.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT
61
+ fill.thickness = 0
62
+ fill.background = color
63
+ bar.addControl(fill)
64
+ return { bar, set: (share) => (fill.width = `${Math.max(0, Math.min(1, share)) * 100}%`) }
65
+ }
66
+
67
+ /** Puts the panel up on the client's GUI layer; gives back what takes it down */
68
+ export function babylonPanel(client: GameClientApi): () => void {
69
+ const panel = new Rectangle('gui.panel')
70
+ panel.width = '290px'
71
+ panel.adaptHeightToChildren = true
72
+ panel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT
73
+ panel.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP
74
+ panel.left = '24px'
75
+ panel.top = '90px'
76
+ panel.background = 'rgba(14, 16, 24, 0.86)'
77
+ panel.color = GOLD
78
+ panel.thickness = 2
79
+ panel.cornerRadius = 10
80
+ panel.shadowBlur = 16
81
+ panel.shadowColor = '#000'
82
+ panel.isPointerBlocker = true
83
+
84
+ const column = new StackPanel()
85
+ column.paddingLeft = column.paddingRight = '16px'
86
+ column.paddingTop = '12px'
87
+ column.paddingBottom = '14px'
88
+ panel.addControl(column)
89
+
90
+ column.addControl(words('War council', 22, GOLD, 34))
91
+ column.addControl(words('Drawn by Babylon GUI, in the canvas', 12, '#9c927a', 22))
92
+
93
+ const yours = words('', 15)
94
+ const ours = strengthBar('#4f9cff')
95
+ const theirs = words('', 15)
96
+ const enemy = strengthBar('#e2513c')
97
+ for (const control of [yours, ours.bar, theirs, enemy.bar]) column.addControl(control)
98
+
99
+ column.addControl(panelButton('gui.charge', 'Charge', () => client.session.send(charge.make({}))))
100
+ column.addControl(panelButton('gui.reset', 'Lay it out again', () => client.session.send(resetCamp.make({}))))
101
+
102
+ // the camera's height, a slider over the camera's distance
103
+ const height = words('', 14, INK, 30)
104
+ const slider = new Slider('gui.height')
105
+ slider.minimum = 14
106
+ slider.maximum = 60
107
+ slider.value = client.camera.pose().distance
108
+ slider.height = '20px'
109
+ slider.color = GOLD
110
+ slider.background = '#3a3020'
111
+ slider.thumbColor = INK
112
+ slider.borderColor = '#00000000'
113
+ const showHeight = (distance: number) => (height.text = `Camera ${Math.round(distance)} away`)
114
+ showHeight(slider.value)
115
+ slider.onValueChangedObservable.add((distance) => {
116
+ client.camera.setPose({ ...client.camera.pose(), distance })
117
+ showHeight(distance)
118
+ })
119
+ column.addControl(height)
120
+ column.addControl(slider)
121
+
122
+ // bars over every unit, not just the hurt: the client's own setting (Alt held does the same)
123
+ const row = new StackPanel()
124
+ row.isVertical = false
125
+ row.height = '34px'
126
+ row.paddingTop = '10px'
127
+ const box = new Checkbox('gui.bars')
128
+ box.width = box.height = '20px'
129
+ box.color = GOLD
130
+ box.background = '#2a2214'
131
+ box.isChecked = client.showAllBars
132
+ box.onIsCheckedChangedObservable.add((on) => (client.showAllBars = on))
133
+ const label = words(' Bars over everyone', 15, INK, 24)
134
+ label.width = '220px'
135
+ row.addControl(box)
136
+ row.addControl(label)
137
+ column.addControl(row)
138
+
139
+ client.gui.addControl(panel)
140
+
141
+ // the battle's numbers, read twice a second from the snapshot the screen has
142
+ // the most health each side had since it was laid out: what its bar is full at (the fallen are gone from the snapshot)
143
+ const most = new Map<number, number>()
144
+ const count = () => {
145
+ const { entities } = client.session.snapshot()
146
+ const side = (owner: number) => {
147
+ const alive = entities.filter((each) => each.owner === owner && each.health > 0)
148
+ const health = alive.reduce((sum, each) => sum + each.health, 0)
149
+ const full = alive.reduce((sum, each) => sum + (each.maxHealth ?? each.health), 0)
150
+ if (health >= (most.get(owner) ?? 0) || full > (most.get(owner) ?? 0)) most.set(owner, full)
151
+ return { alive: alive.length, share: health / (most.get(owner) || 1) }
152
+ }
153
+ const mine = side(PLAYER)
154
+ const others = side(ENEMY)
155
+ yours.text = `Yours: ${mine.alive} standing`
156
+ theirs.text = `Theirs: ${others.alive} standing`
157
+ ours.set(mine.share)
158
+ enemy.set(others.share)
159
+ }
160
+ count()
161
+ const timer = setInterval(count, 500)
162
+ return () => {
163
+ clearInterval(timer)
164
+ client.gui.removeControl(panel)
165
+ panel.dispose()
166
+ }
167
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Babylon's GUI in the world, two ways:
3
+ *
4
+ * - On the screen, tied to a point of the world: a name and a health bar over every unit, each a control of the
5
+ * client's GUI layer linked to a node (`linkWithMesh`) that a phase of the frame keeps over its unit
6
+ * (`client.frame.add`, the unit where it is drawn between two ticks: `state.get(id).at(alpha)`)
7
+ * - In the world itself: a signpost in the field, a plane with a GUI texture of its own (`CreateForMesh`), drawn in
8
+ * perspective with the scene, with a button on it
9
+ *
10
+ * The client's own plates are taken off the units while it is up (`client.unitHud.defs.set(type, null)`)
11
+ */
12
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
13
+ import { Color3 } from '@babylonjs/core/Maths/math.color.js'
14
+ import { CreateCylinder } from '@babylonjs/core/Meshes/Builders/cylinderBuilder.js'
15
+ import { CreatePlane } from '@babylonjs/core/Meshes/Builders/planeBuilder.js'
16
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
17
+ import { AdvancedDynamicTexture } from '@babylonjs/gui/2D/advancedDynamicTexture.js'
18
+ import { Button } from '@babylonjs/gui/2D/controls/button.js'
19
+ import { Control } from '@babylonjs/gui/2D/controls/control.js'
20
+ import { Rectangle } from '@babylonjs/gui/2D/controls/rectangle.js'
21
+ import { StackPanel } from '@babylonjs/gui/2D/controls/stackPanel.js'
22
+ import { TextBlock } from '@babylonjs/gui/2D/controls/textBlock.js'
23
+ import type { GameClientApi } from '@v12-engine/client'
24
+ import { PLAYER, UNIT } from '../common/units'
25
+ import { gather } from './index'
26
+
27
+ /** How high over a unit's feet its plate is, in the world's units */
28
+ const OVER = 2.4
29
+ /** Where the signpost stands */
30
+ export const POST = { x: 31, z: 21 }
31
+
32
+ /** A unit's plate: a node over the unit, and the controls linked to it */
33
+ interface Plate {
34
+ node: TransformNode
35
+ frame: Rectangle
36
+ fill: Rectangle
37
+ name: TextBlock
38
+ }
39
+
40
+ /** Makes a unit's plate: its name over a bar, in its side's colour */
41
+ function makePlate(client: GameClientApi, id: number, label: string, mine: boolean): Plate {
42
+ const node = new TransformNode(`gui.plate.${id}`, client.scene)
43
+ const frame = new Rectangle()
44
+ frame.width = '86px'
45
+ frame.height = '30px'
46
+ frame.thickness = 0
47
+ frame.isPointerBlocker = false
48
+ const column = new StackPanel()
49
+ frame.addControl(column)
50
+ const name = new TextBlock(undefined, label)
51
+ name.height = '16px'
52
+ name.fontSize = 12
53
+ name.fontFamily = 'Georgia, serif'
54
+ name.color = mine ? '#cfe3ff' : '#ffd0c6'
55
+ name.outlineWidth = 3
56
+ name.outlineColor = '#000'
57
+ column.addControl(name)
58
+ const bar = new Rectangle()
59
+ bar.height = '8px'
60
+ bar.width = '70px'
61
+ bar.thickness = 1
62
+ bar.color = '#000'
63
+ bar.background = '#1a1a1acc'
64
+ bar.cornerRadius = 2
65
+ const fill = new Rectangle()
66
+ fill.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT
67
+ fill.thickness = 0
68
+ fill.background = mine ? '#4fd36b' : '#e2513c'
69
+ bar.addControl(fill)
70
+ column.addControl(bar)
71
+ client.gui.addControl(frame)
72
+ frame.linkWithMesh(node)
73
+ return { node, frame, fill, name }
74
+ }
75
+
76
+ /** The signpost's post: a log of the shared art (`resources/wood_log_a`, lying along z, 0.72 across and 1.35 long), stood on end */
77
+ const POST_LOG = { id: 990_001, model: 'resources/wood_log_a.glb', thick: 0.26, long: 2.8 }
78
+
79
+ /** The signpost: a post and a board, the board's face a GUI texture of its own with a button to gather the army there */
80
+ function signpost(client: GameClientApi): () => void {
81
+ const { scene } = client
82
+ const wood = new StandardMaterial('gui.wood', scene)
83
+ wood.diffuseColor = new Color3(0.42, 0.29, 0.16)
84
+ // a plain post while the log loads, inside it once it has
85
+ const post = CreateCylinder('gui.post', { height: POST_LOG.long, diameter: POST_LOG.thick - 0.04 }, scene)
86
+ post.position.set(POST.x, POST_LOG.long / 2, POST.z + 0.3)
87
+ post.material = wood
88
+ // the log, scenery of this screen's alone (`client.doodads`), tipped up on end round its middle
89
+ const across = POST_LOG.thick / 0.72
90
+ client.doodads.add({
91
+ id: POST_LOG.id,
92
+ model: POST_LOG.model,
93
+ x: POST.x,
94
+ // a little behind the board's middle, which leans back over it
95
+ z: POST.z + 0.3,
96
+ y: POST_LOG.long / 2 - 0.15,
97
+ yaw: 0,
98
+ pitch: Math.PI / 2,
99
+ scale: [across, across, POST_LOG.long / 1.35]
100
+ })
101
+ const board = CreatePlane('gui.board', { width: 4, height: 2 }, scene)
102
+ board.position.set(POST.x, 3.4, POST.z)
103
+ // its face (a plane's is towards -z) to the camera's usual way, from the south, a little turned and leant back
104
+ board.rotation.set(0.6, -0.3, 0)
105
+ const texture = AdvancedDynamicTexture.CreateForMesh(board, 640, 320)
106
+ const face = new Rectangle()
107
+ face.background = '#2b2014'
108
+ face.color = '#d9b45a'
109
+ face.thickness = 10
110
+ face.cornerRadius = 18
111
+ texture.addControl(face)
112
+ const column = new StackPanel()
113
+ face.addControl(column)
114
+ const title = new TextBlock(undefined, 'RALLY POINT')
115
+ title.height = '90px'
116
+ title.fontSize = 64
117
+ title.fontFamily = 'Georgia, serif'
118
+ title.color = '#f1e4c3'
119
+ column.addControl(title)
120
+ const go = Button.CreateSimpleButton('gui.gatherHere', 'Gather here')
121
+ go.width = '400px'
122
+ go.height = '110px'
123
+ go.fontSize = 52
124
+ go.fontFamily = 'Georgia, serif'
125
+ go.color = '#d9b45a'
126
+ go.background = '#4a3418'
127
+ go.cornerRadius = 14
128
+ go.thickness = 6
129
+ go.paddingTop = '14px'
130
+ go.onPointerEnterObservable.add(() => (go.background = '#6a4c22'))
131
+ go.onPointerOutObservable.add(() => (go.background = '#4a3418'))
132
+ go.onPointerUpObservable.add(() => client.session.send(gather.make({ at: { x: POST.x - 2, z: POST.z - 2 } })))
133
+ column.addControl(go)
134
+ return () => {
135
+ client.doodads.remove(POST_LOG.id)
136
+ texture.dispose()
137
+ for (const each of [board, post]) each.dispose()
138
+ wood.dispose()
139
+ }
140
+ }
141
+
142
+ /** Puts the plates and the signpost up; gives back what takes them away and gives the units their own plates back */
143
+ export function babylonWorld(client: GameClientApi): () => void {
144
+ const types = [UNIT.footman, UNIT.archer, UNIT.mage, UNIT.knight]
145
+ for (const type of types) client.unitHud.defs.set(type, null)
146
+ const plates = new Map<number, Plate>()
147
+ const names: Record<string, string> = { footman: 'Footman', archer: 'Archer', mage: 'Mage', knight: 'Knight' }
148
+ const stop = client.frame.add(
149
+ {
150
+ name: 'gui.plates',
151
+ run: (frame) => {
152
+ const seen = new Set<number>()
153
+ for (const entity of client.state.entities.values()) {
154
+ if (entity.hidden || !entity.inSight || entity.health <= 0) continue
155
+ seen.add(entity.id)
156
+ let plate = plates.get(entity.id)
157
+ if (!plate) {
158
+ const label =
159
+ entity.state.name === 'hero'
160
+ ? 'Sir Aldwin'
161
+ : entity.state.name === 'warlord'
162
+ ? 'The Warlord'
163
+ : (names[entity.state.type] ?? entity.state.type)
164
+ plate = makePlate(client, entity.id, label, entity.owner === PLAYER)
165
+ plates.set(entity.id, plate)
166
+ }
167
+ const at = entity.at(frame.alpha)
168
+ plate.node.position.set(at.x, OVER, at.z)
169
+ plate.fill.width = `${Math.max(0, entity.health / Math.max(1, entity.maxHealth)) * 100}%`
170
+ }
171
+ for (const [id, plate] of plates) {
172
+ if (seen.has(id)) continue
173
+ plate.frame.dispose()
174
+ plate.node.dispose()
175
+ plates.delete(id)
176
+ }
177
+ }
178
+ },
179
+ { after: 'simulate' }
180
+ )
181
+ const post = signpost(client)
182
+ return () => {
183
+ stop()
184
+ post()
185
+ for (const plate of plates.values()) {
186
+ plate.frame.dispose()
187
+ plate.node.dispose()
188
+ }
189
+ for (const type of types) client.unitHud.defs.set(type, undefined)
190
+ }
191
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A part of the HUD in plain HTML: no framework, an element the HUD gives (`HudWidgetDef.mount`) and the part fills,
3
+ * keeps up to date and empties again when it is taken away. A roster of the player's army: a row a unit, its health
4
+ * as a bar, the selected ones lit. A click on a row selects the unit and looks at it. What it shows it reads from the
5
+ * snapshot (the units) and from the HUD's own state (`client.ui.watch(() => client.ui.selection(), …)`)
6
+ */
7
+ import type { GameClientApi, HudWidgetDef } from '@v12-engine/client'
8
+
9
+ const STYLE = `
10
+ .gui-roster { pointer-events: auto; width: 230px; padding: 10px 10px 8px; border-radius: 8px; background: rgba(12, 14, 20, 0.88);
11
+ border: 1px solid #3c4252; color: #e8e2d0; font: 13px/1.2 system-ui, sans-serif; box-shadow: 0 6px 18px rgba(0,0,0,.45) }
12
+ .gui-roster h3 { margin: 0 0 2px; font: 600 15px system-ui, sans-serif; color: #f2d58a }
13
+ .gui-roster small { display: block; margin-bottom: 8px; color: #8a8f9c; font-size: 11px }
14
+ .gui-roster ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; max-height: 360px; overflow: auto }
15
+ .gui-roster li { display: grid; grid-template-columns: 1fr 70px; gap: 8px; align-items: center; padding: 4px 6px; border-radius: 5px; cursor: pointer }
16
+ .gui-roster li:hover { background: #262b38 }
17
+ .gui-roster li.picked { background: #33405e; box-shadow: inset 0 0 0 1px #6f8fd6 }
18
+ .gui-roster .bar { height: 6px; border-radius: 3px; background: #1b1d24; overflow: hidden }
19
+ .gui-roster .bar i { display: block; height: 100%; background: linear-gradient(90deg, #3fae5a, #7fdc6e) }
20
+ .gui-roster li.low .bar i { background: linear-gradient(90deg, #c0392b, #e67e22) }
21
+ `
22
+
23
+ /** The roster as a part of the HUD: on the left over the map */
24
+ export function rosterPart(client: GameClientApi): HudWidgetDef {
25
+ return {
26
+ id: 'gui.roster',
27
+ place: 'left',
28
+ mount(element) {
29
+ const style = document.createElement('style')
30
+ style.textContent = STYLE
31
+ const panel = document.createElement('div')
32
+ panel.className = 'gui-roster'
33
+ panel.innerHTML = '<h3>Roster</h3><small>Plain HTML: a click selects and looks</small><ol></ol>'
34
+ const list = panel.querySelector('ol')!
35
+ element.append(style, panel)
36
+
37
+ /** The rows by unit, made as units come and taken away as they go */
38
+ const rows = new Map<number, { row: HTMLLIElement; fill: HTMLElement }>()
39
+ let picked = new Set<number>()
40
+
41
+ const draw = () => {
42
+ const mine = client.session.snapshot().entities.filter((each) => each.owner === client.session.playerId && each.health > 0)
43
+ const seen = new Set<number>()
44
+ for (const unit of mine) {
45
+ seen.add(unit.id)
46
+ let shown = rows.get(unit.id)
47
+ if (!shown) {
48
+ const row = document.createElement('li')
49
+ const name = document.createElement('span')
50
+ name.textContent = unit.name === 'hero' ? 'Sir Aldwin' : (client.session.registry.unit(unit.type)?.name ?? unit.type)
51
+ const bar = document.createElement('div')
52
+ bar.className = 'bar'
53
+ const fill = document.createElement('i')
54
+ bar.append(fill)
55
+ row.append(name, bar)
56
+ row.onclick = () => {
57
+ client.controller.select([unit.id])
58
+ const now = client.session.snapshot().entities.find((each) => each.id === unit.id)
59
+ if (now) client.camera.lookAt({ x: now.x, z: now.z }, { seconds: 0.5 })
60
+ }
61
+ list.append(row)
62
+ shown = { row, fill }
63
+ rows.set(unit.id, shown)
64
+ }
65
+ const share = unit.health / Math.max(1, unit.maxHealth ?? unit.health)
66
+ shown.fill.style.width = `${share * 100}%`
67
+ shown.row.classList.toggle('low', share < 0.35)
68
+ shown.row.classList.toggle('picked', picked.has(unit.id))
69
+ }
70
+ for (const [id, shown] of rows) {
71
+ if (seen.has(id)) continue
72
+ shown.row.remove()
73
+ rows.delete(id)
74
+ }
75
+ }
76
+
77
+ // the health from the snapshot, four times a second; the selection as the HUD's state changes
78
+ draw()
79
+ const timer = setInterval(draw, 250)
80
+ const stop = client.ui.watch(
81
+ () => client.ui.selection(),
82
+ (selection) => {
83
+ picked = new Set(selection.map((item) => item.id))
84
+ draw()
85
+ }
86
+ )
87
+ return () => {
88
+ clearInterval(timer)
89
+ stop()
90
+ panel.remove()
91
+ style.remove()
92
+ }
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The interface examples on the rules' side. An interface is the screen's (`./looks.ts` and the files beside it): the
3
+ * rules here only lay out a camp to look at, two small armies facing each other, and the commands the interfaces'
4
+ * buttons send (charge, lay it out again, gather). Everything the interfaces show they read from the snapshots the
5
+ * screen is sent, and everything they do goes back as a command, as a click on the HUD's own card does
6
+ */
7
+ import { defineCommand, exactMath, type Game, point, type World } from '@v12-engine/engine'
8
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
9
+ import { type ExampleCategory, exampleField } from '../define'
10
+
11
+ /** Where each side stands */
12
+ const OURS = { x: 24, z: 17 }
13
+ const THEIRS = { x: 24, z: 32 }
14
+
15
+ /** The camp: a knight with footmen, archers and a mage on one side; footmen, archers and a knight on the other */
16
+ export function camp(world: World): void {
17
+ world.spawn(UNIT.knight, { at: { x: OURS.x, z: OURS.z + 2 }, owner: PLAYER, facing: 0, name: 'hero' })
18
+ world.spawn(UNIT.mage, { at: { x: OURS.x + 3, z: OURS.z - 2.5 }, owner: PLAYER, facing: 0 })
19
+ for (let i = 0; i < 6; i++) world.spawn(UNIT.footman, { at: { x: OURS.x - 3.5 + i * 1.4, z: OURS.z }, owner: PLAYER, facing: 0 })
20
+ for (let i = 0; i < 4; i++) world.spawn(UNIT.archer, { at: { x: OURS.x - 2.1 + i * 1.4, z: OURS.z - 2 }, owner: PLAYER, facing: 0 })
21
+ world.spawn(UNIT.knight, { at: { x: THEIRS.x, z: THEIRS.z - 2 }, owner: ENEMY, facing: Math.PI, name: 'warlord' })
22
+ for (let i = 0; i < 6; i++) world.spawn(UNIT.footman, { at: { x: THEIRS.x - 3.5 + i * 1.4, z: THEIRS.z }, owner: ENEMY, facing: Math.PI })
23
+ for (let i = 0; i < 4; i++) world.spawn(UNIT.archer, { at: { x: THEIRS.x - 2.1 + i * 1.4, z: THEIRS.z + 2 }, owner: ENEMY, facing: Math.PI })
24
+ }
25
+
26
+ /** Both sides at each other, fighting what they meet */
27
+ export const charge = defineCommand('gui.charge', {}, (world) => {
28
+ for (const unit of world.units.values()) {
29
+ if (!unit.alive || (unit.owner !== PLAYER && unit.owner !== ENEMY)) continue
30
+ unit.order('attackMove', { x: unit.pos.x, z: unit.owner === PLAYER ? THEIRS.z + 4 : OURS.z - 4 })
31
+ }
32
+ })
33
+
34
+ /** Every unit taken away and the camp laid out again */
35
+ export const resetCamp = defineCommand('gui.reset', {}, (world) => {
36
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
37
+ camp(world)
38
+ })
39
+
40
+ /** The player's units gathered round a spot, in a loose ring: what the interfaces' own Gather buttons and keys send */
41
+ export const gather = defineCommand('gui.gather', { at: point }, (world, player, { at }) => {
42
+ const mine = world.find({ owner: player }).filter((unit) => unit.alive)
43
+ mine.forEach((unit, i) => {
44
+ const ring = Math.floor(Math.sqrt(i))
45
+ const angle = i * 2.4
46
+ unit.order('move', { x: at.x + exactMath.cos(angle) * ring * 1.3, z: at.z + exactMath.sin(angle) * ring * 1.3 })
47
+ })
48
+ })
49
+
50
+ /** Every map of the interface examples: the camp's field, two sides, a little room round them */
51
+ const field = (name: string) => () => exampleField({ name, players: 2 })
52
+
53
+ /** An example of the category: the camp on a field, its words and its files */
54
+ const example = (id: string, title: string, summary: string, tags: string[], files: string[] = []) => ({
55
+ id: `gui-${id}`,
56
+ category: 'gui' as const,
57
+ title,
58
+ summary,
59
+ tags,
60
+ map: field(title),
61
+ start: (world: World) => camp(world),
62
+ source: ['examples/gui/index.ts', 'examples/gui/looks.ts', ...files.map((file) => `examples/gui/${file}`)]
63
+ })
64
+
65
+ export const category: ExampleCategory = {
66
+ rules: (game: Game<any>) => {
67
+ game.command(charge)
68
+ game.command(resetCamp)
69
+ game.command(gather)
70
+ },
71
+ examples: [
72
+ example(
73
+ 'babylon-screen',
74
+ 'Babylon GUI on the screen',
75
+ 'A panel drawn with Babylon’s own GUI (@babylonjs/gui) on the game’s full-screen layer: text that follows the battle, buttons, a slider and a checkbox, all in the canvas.',
76
+ ['babylon', 'gui', 'AdvancedDynamicTexture', 'fullscreen', 'button', 'slider', 'checkbox', 'canvas'],
77
+ ['babylonScreen.ts']
78
+ ),
79
+ example(
80
+ 'babylon-world',
81
+ 'Babylon GUI in the world',
82
+ 'Babylon GUI tied to the 3D world: a name and a health bar over every unit (linkWithMesh), and a signpost in the field with a panel on it you can click (a texture on a mesh).',
83
+ ['babylon', 'gui', 'linkWithMesh', 'CreateForMesh', 'world space', 'health bar', 'name plate', '3D panel'],
84
+ ['babylonWorld.ts']
85
+ ),
86
+ example(
87
+ 'solid',
88
+ 'A HUD part in Solid',
89
+ 'A part of the HUD written as a Solid component (solid-js/html, no JSX build step) with the client’s kit: each side’s strength as a meter, the selection by type, buttons that select and send.',
90
+ ['solid', 'component', 'hud widget', 'kit', 'signals', 'Meter', 'Button', 'html'],
91
+ ['solidPart.ts']
92
+ ),
93
+ example(
94
+ 'html',
95
+ 'A HUD part in plain HTML',
96
+ 'A roster of your army drawn with plain DOM, no framework: health bars kept up to date, a click on a row selects the unit and looks at it.',
97
+ ['html', 'dom', 'mount', 'hud widget', 'roster', 'vanilla', 'ui.watch'],
98
+ ['htmlPart.ts']
99
+ ),
100
+ example(
101
+ 'hud-layout',
102
+ 'The HUD rearranged',
103
+ 'The HUD’s own parts moved, hidden and restyled while the game runs: the console centred or in the corners, a theme, parts on and off, its size and the minimap’s, or none of it at all.',
104
+ ['hud', 'layout', 'console', 'corners', 'theme', 'skin', 'hide', 'scale', 'places'],
105
+ ['layout.ts']
106
+ ),
107
+ example(
108
+ 'plates',
109
+ 'Plates over units',
110
+ 'The plates over units’ heads, from bars when hurt to a hero card with a face, a level and mana: pick a design and how it is drawn (canvas, HTML or Babylon GUI).',
111
+ ['unit hud', 'plates', 'health bar', 'name plate', 'unitHud', 'UnitHudDef', 'nameplate'],
112
+ ['plates.ts']
113
+ ),
114
+ example(
115
+ 'minimap',
116
+ 'A minimap of your own',
117
+ 'The HUD’s minimap replaced by one drawn on a canvas of your own: the field, the units as dots, the view’s frame; click it to look there. Or keep the client’s and draw a layer on it.',
118
+ ['minimap', 'canvas', 'radar', 'map', 'addMinimapLayer', 'replace a part'],
119
+ ['minimap.ts']
120
+ ),
121
+ example(
122
+ 'radial',
123
+ 'A radial command menu',
124
+ 'Right-click the ground with units selected: a ring of orders opens where you clicked (move, attack there, hold, stop, gather). Your own menu in place of the smart order.',
125
+ ['radial', 'pie menu', 'command menu', 'right click', 'orders', 'onRightClick', 'attach'],
126
+ ['radial.ts']
127
+ ),
128
+ example(
129
+ 'own-interface',
130
+ 'An interface of your own',
131
+ 'The whole HUD gone and an interface of the game’s own in its place: a top bar, a unit card for what is selected, order buttons, and a log of what happened.',
132
+ ['custom hud', 'own interface', 'hideHud', 'replace the hud', 'selection', 'card', 'html'],
133
+ ['ownInterface.ts']
134
+ ),
135
+ example(
136
+ 'keys',
137
+ 'Keys of your own',
138
+ 'Actions of the game’s own on keys the player can rebind: R gathers your army, C makes it cheer, Z looks at the hero. A legend shows the keys; rebind one and it follows.',
139
+ ['keys', 'hotkeys', 'actions', 'bindings', 'rebind', 'shortcuts', 'keyboard'],
140
+ ['keys.ts']
141
+ )
142
+ ]
143
+ }