@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,87 @@
1
+ /**
2
+ * The plates over units' heads, set while the game runs (`client.unitHud`): a plate is a column of widgets (bars,
3
+ * words, a face, a badge, a ring) and a def picks one for whom and when (`UnitHudDef`). Each design below is set on
4
+ * every unit type of the camp (`client.unitHud.defs.set`); how they are drawn is the defaults' (`client.unitHud.look`):
5
+ * on the canvas over the picture, as HTML elements, or with Babylon's GUI
6
+ */
7
+ import type { GameClientApi, UnitHudDef } from '@v12-engine/client'
8
+ import { UNIT } from '../common/units'
9
+
10
+ /** The camp's unit types, which the designs are set on */
11
+ const TYPES = [UNIT.footman, UNIT.archer, UNIT.mage, UNIT.knight]
12
+
13
+ /** A unit's name as its plate says it: the knights by their own */
14
+ const nameOf = (unit: { def: { name: string }; state: { name?: string } }) =>
15
+ unit.state.name === 'hero' ? 'Sir Aldwin' : unit.state.name === 'warlord' ? 'The Warlord' : unit.def.name
16
+
17
+ /** The designs to pick from, by their names in the example's select */
18
+ export const DESIGNS: Record<string, UnitHudDef> = {
19
+ // a thin bar, only while hurt (and while selected or hovered), as most RTS games do
20
+ 'Bars when hurt': {
21
+ plates: [
22
+ { when: { selected: true }, show: 'always', items: [{ bar: 'health', width: 46, height: 5, ghost: { seconds: 0.5 } }] },
23
+ { show: 'hurt', items: [{ bar: 'health', width: 46, height: 5, ghost: { seconds: 0.5 } }] }
24
+ ]
25
+ },
26
+ // a name over a bar with ticks every 100 points, always
27
+ 'Name and bar': {
28
+ plates: [
29
+ {
30
+ show: 'always',
31
+ items: [
32
+ { text: nameOf, size: 11, color: 'owner', bold: true },
33
+ { bar: 'health', width: 64, height: 7, ticks: 100, ghost: { seconds: 0.6 } }
34
+ ]
35
+ }
36
+ ]
37
+ },
38
+ // the full card: a face, the name, the level, health with its points and mana; enemies get a plain bar
39
+ 'Hero card': {
40
+ plates: [
41
+ {
42
+ when: { relation: ['self', 'ally'] },
43
+ show: 'always',
44
+ items: [
45
+ {
46
+ row: [
47
+ { portrait: true, size: 28, border: 'owner', shape: 'circle' },
48
+ {
49
+ column: [
50
+ { text: nameOf, size: 12, color: 'owner', bold: true },
51
+ { bar: 'health', width: 86, height: 8, label: 'value', ghost: { seconds: 0.6 } },
52
+ { bar: 'mana', width: 86, height: 3 }
53
+ ]
54
+ },
55
+ { badge: (unit) => unit.level ?? 1, size: 16 }
56
+ ],
57
+ gap: 4
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ show: 'always',
63
+ items: [
64
+ { text: nameOf, size: 11, color: 'owner' },
65
+ { bar: 'health', width: 64, height: 6 }
66
+ ]
67
+ }
68
+ ]
69
+ },
70
+ // a ring round a dot, filling as health is lost: small and clean
71
+ Rings: { plates: [{ show: 'always', items: [{ ring: 'health', size: 18, width: 3 }] }] },
72
+ // names alone, no bars at all
73
+ 'Names only': { plates: [{ show: 'always', items: [{ text: nameOf, size: 12, color: 'owner', bold: true }] }] }
74
+ }
75
+
76
+ /** Sets a design on every type of the camp */
77
+ export function setDesign(client: GameClientApi, name: string): void {
78
+ const def = DESIGNS[name]
79
+ if (!def) return
80
+ for (const type of TYPES) client.unitHud.defs.set(type, def)
81
+ }
82
+
83
+ /** The plates as the types' own again, drawn on the canvas */
84
+ export function restorePlates(client: GameClientApi): void {
85
+ for (const type of TYPES) client.unitHud.defs.set(type, undefined)
86
+ client.unitHud.look.render = 'canvas'
87
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A radial menu of orders, the game's own: a right-click on the ground with units selected opens a ring of buttons
3
+ * where the click was (an element pinned to that spot of the world, `client.attach`), in place of the smart order
4
+ * (`GameView.input.onRightClick` says it is the game's). Each button sends an order for the selected units, as the
5
+ * HUD's card would: a command (`{ type: 'order', entities, order }`), or one of the game's own (`gather`)
6
+ */
7
+ import type { GameClientApi, GameView, Pin } from '@v12-engine/client'
8
+ import type { Vec2 } from '@v12-engine/engine'
9
+ import { gather } from './index'
10
+
11
+ /** What a right-click on the map is given (the client's `ClickContext`: the spot, what is under it, what is selected) */
12
+ type ClickContext = Parameters<NonNullable<NonNullable<GameView['input']>['onRightClick']>>[0]
13
+
14
+ /** The ring's buttons: a glyph, a word, and the order sent for the selected units at the spot */
15
+ const ORDERS: { glyph: string; label: string; order: (ids: number[], at: Vec2, client: GameClientApi) => void }[] = [
16
+ {
17
+ glyph: '➜',
18
+ label: 'Move',
19
+ order: (ids, at, client) => client.session.send({ type: 'order', entities: ids, order: { type: 'move', point: at } })
20
+ },
21
+ {
22
+ glyph: '⚔',
23
+ label: 'Attack',
24
+ order: (ids, at, client) => client.session.send({ type: 'order', entities: ids, order: { type: 'attackMove', point: at } })
25
+ },
26
+ { glyph: '⛨', label: 'Hold', order: (ids, _at, client) => client.session.send({ type: 'order', entities: ids, order: { type: 'hold' } }) },
27
+ { glyph: '■', label: 'Stop', order: (ids, _at, client) => client.session.send({ type: 'order', entities: ids, order: { type: 'stop' } }) },
28
+ { glyph: '✺', label: 'Gather', order: (_ids, at, client) => client.session.send(gather.make({ at })) }
29
+ ]
30
+
31
+ const STYLE = `
32
+ .gui-radial { position: relative; width: 190px; height: 190px; pointer-events: none; animation: guiRadialIn .14s ease-out }
33
+ .gui-radial::before { content: ''; position: absolute; inset: 52px; border-radius: 50%; border: 2px dashed rgba(242, 213, 138, .55) }
34
+ .gui-radial button { pointer-events: auto; position: absolute; width: 58px; height: 58px; margin: -29px; border-radius: 50%; cursor: pointer;
35
+ border: 2px solid #c9a35a; background: radial-gradient(circle at 35% 30%, #3b3326, #15120c); color: #f6e3ae;
36
+ display: grid; place-items: center; font: 20px/1 system-ui; box-shadow: 0 4px 12px rgba(0,0,0,.6); transition: transform .08s }
37
+ .gui-radial button span { position: absolute; bottom: -17px; font: 600 11px system-ui, sans-serif; color: #fff; text-shadow: 0 1px 3px #000 }
38
+ .gui-radial button:hover { transform: scale(1.14); border-color: #fff2c4; background: radial-gradient(circle at 35% 30%, #5a4a2a, #241c0e) }
39
+ @keyframes guiRadialIn { from { transform: scale(.5); opacity: 0 } to { transform: scale(1); opacity: 1 } }
40
+ `
41
+
42
+ /** The ring open now, and what closes it */
43
+ let open: { pin: Pin; close: () => void } | undefined
44
+
45
+ /** Closes the ring, if one is open */
46
+ export function closeRadial(): void {
47
+ open?.close()
48
+ }
49
+
50
+ /** Opens the ring at a spot of the world, for the units selected now */
51
+ function openRadial(client: GameClientApi, at: Vec2, ids: number[]): void {
52
+ closeRadial()
53
+ const ring = document.createElement('div')
54
+ ring.className = 'gui-radial'
55
+ const style = document.createElement('style')
56
+ style.textContent = STYLE
57
+ ring.append(style)
58
+ ORDERS.forEach((each, i) => {
59
+ const angle = -Math.PI / 2 + (i / ORDERS.length) * Math.PI * 2
60
+ const button = document.createElement('button')
61
+ button.type = 'button'
62
+ button.style.left = `${95 + Math.cos(angle) * 70}px`
63
+ button.style.top = `${95 + Math.sin(angle) * 70}px`
64
+ button.innerHTML = `${each.glyph}<span>${each.label}</span>`
65
+ button.onpointerdown = (event) => {
66
+ event.stopPropagation()
67
+ each.order(ids, at, client)
68
+ client.ping(at.x, at.z, [1, 0.85, 0.3])
69
+ closeRadial()
70
+ }
71
+ ring.append(button)
72
+ })
73
+ const pin = client.attach(ring, at, { anchor: 'center' })
74
+ // a click anywhere else, or Esc, closes it
75
+ const away = (event: Event) => {
76
+ if (event instanceof KeyboardEvent && event.key !== 'Escape') return
77
+ if (event instanceof PointerEvent && ring.contains(event.target as Node)) return
78
+ closeRadial()
79
+ }
80
+ setTimeout(() => {
81
+ window.addEventListener('pointerdown', away, true)
82
+ window.addEventListener('keydown', away, true)
83
+ })
84
+ open = {
85
+ pin,
86
+ close: () => {
87
+ window.removeEventListener('pointerdown', away, true)
88
+ window.removeEventListener('keydown', away, true)
89
+ pin.remove()
90
+ open = undefined
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * What the view does with a right-click (`GameView.input.onRightClick`): with the player's own units selected, opens
97
+ * the ring at the spot and says the click is the game's (true), so no smart order goes
98
+ */
99
+ export function radialRightClick(click: ClickContext): boolean {
100
+ const mine = click.selected.filter((unit) => unit.owner === click.client.session.playerId).map((unit) => unit.id)
101
+ if (!mine.length) return false
102
+ openRadial(click.client, click.point, mine)
103
+ return true
104
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * A part of the HUD written in Solid: a component, given to the HUD as a widget (`HudWidgetDef.component`). No JSX:
3
+ * a project made from this starter has no Solid compiler in its build, so the markup is Solid's tagged template
4
+ * (`solid-js/html`), which is the same components and signals with nothing to compile. It is built from the client's
5
+ * kit (`@v12-engine/client/kit`: `Panel`, `Meter`, `Button`), so it takes the HUD's theme, and from the HUD's own
6
+ * signals (`client.ui.selection()`), so it changes as they do
7
+ */
8
+ import { Button, Meter, Panel } from '@v12-engine/client/kit'
9
+ import type { GameClientApi, HudWidgetDef } from '@v12-engine/client'
10
+ import { createSignal, For, Index, onCleanup, Show } from 'solid-js'
11
+ import html from 'solid-js/html'
12
+ import { ENEMY, PLAYER } from '../common/units'
13
+ import { charge, resetCamp } from './index'
14
+
15
+ /** A side as the part shows it: how many stand, their health and the most they had */
16
+ interface Side {
17
+ name: string
18
+ owner: number
19
+ alive: number
20
+ health: number
21
+ most: number
22
+ }
23
+
24
+ /** Each side's strength now, from the snapshot the screen was sent last */
25
+ function sides(client: GameClientApi, most: Map<number, number>): Side[] {
26
+ const { entities } = client.session.snapshot()
27
+ return [
28
+ { name: 'Yours', owner: PLAYER },
29
+ { name: 'Theirs', owner: ENEMY }
30
+ ].map(({ name, owner }) => {
31
+ const units = entities.filter((each) => each.owner === owner && each.health > 0)
32
+ const health = units.reduce((sum, each) => sum + each.health, 0)
33
+ // the most a side had since it was last laid out: what its meter is full at
34
+ const full = units.reduce((sum, each) => sum + (each.maxHealth ?? each.health), 0)
35
+ most.set(owner, Math.max(most.get(owner) ?? 0, full))
36
+ if (units.length && health >= (most.get(owner) ?? 0)) most.set(owner, full)
37
+ return { name, owner, alive: units.length, health, most: most.get(owner) || 1 }
38
+ })
39
+ }
40
+
41
+ /**
42
+ * The army's part: each side's strength as a meter, what is selected counted by type (a button for each selects just
43
+ * those), and the two orders of the camp. Solid runs it once; what it reads of signals (`client.ui.selection()`,
44
+ * `strength()`) it keeps up to date by itself
45
+ */
46
+ function ArmyPart(props: { client: GameClientApi }) {
47
+ const { client } = props
48
+ const most = new Map<number, number>()
49
+ const [strength, setStrength] = createSignal(sides(client, most))
50
+ const timer = setInterval(() => setStrength(sides(client, most)), 250)
51
+ onCleanup(() => clearInterval(timer))
52
+
53
+ /** What is selected, by type: its name, how many, and their ids */
54
+ const picked = () => {
55
+ const groups = new Map<string, { name: string; ids: number[] }>()
56
+ for (const item of client.ui.selection()) {
57
+ const group = groups.get(item.typeId) ?? { name: item.name, ids: [] }
58
+ group.ids.push(item.id)
59
+ groups.set(item.typeId, group)
60
+ }
61
+ return [...groups.values()]
62
+ }
63
+
64
+ // styled with the theme's variables (`--vg-accent`, `--vg-muted-foreground`): it follows the HUD's theme as the kit does
65
+ return html`
66
+ <${Panel} padding="md" style="width:260px;pointer-events:auto;display:flex;flex-direction:column;gap:8px">
67
+ <div style="font-family:var(--vg-typeface-display);color:var(--vg-accent);font-size:17px;letter-spacing:.04em">The army</div>
68
+ <div style="font-size:11px;color:var(--vg-muted-foreground);margin-top:-4px">A Solid component, in the HUD's place on the left</div>
69
+ <${Index} each=${strength}>
70
+ ${(side: () => Side) => html`
71
+ <div style="display:flex;flex-direction:column;gap:4px">
72
+ <div style="display:flex;justify-content:space-between;font-size:13px">
73
+ <span>${() => side().name}</span>
74
+ <span style="color:var(--vg-muted-foreground)">${() => side().alive} standing</span>
75
+ </div>
76
+ <${Meter} value=${() => side().health} max=${() => side().most} kind=${() => (side().owner === PLAYER ? 'health' : 'health-low')} />
77
+ </div>
78
+ `}
79
+ <//>
80
+ <div style="font-size:12px;color:var(--vg-muted-foreground);margin-top:4px">Selected</div>
81
+ <${Show}
82
+ when=${() => picked().length > 0}
83
+ fallback=${html`<div style="font-size:12px;color:var(--vg-faint-foreground)">nothing: drag a box round some</div>`}
84
+ >
85
+ <div style="display:flex;flex-wrap:wrap;gap:4px">
86
+ <${For} each=${picked}>
87
+ ${(group: { name: string; ids: number[] }) =>
88
+ html`<${Button} size="sm" onClick=${() => client.controller.select(group.ids)} tip="Select just these"
89
+ >${group.ids.length} × ${group.name}<//
90
+ >`}
91
+ <//>
92
+ </div>
93
+ <//>
94
+ <div style="display:flex;gap:8px;margin-top:4px">
95
+ <${Button} variant="primary" onClick=${() => client.session.send(charge.make({}))}>Charge<//>
96
+ <${Button} onClick=${() => client.session.send(resetCamp.make({}))}>Lay out again<//>
97
+ </div>
98
+ <//>
99
+ `
100
+ }
101
+
102
+ /** The part as the HUD takes it: on the left over the map, drawn by the component above */
103
+ export const armyPart = (client: GameClientApi): HudWidgetDef => ({
104
+ id: 'gui.army',
105
+ place: 'left',
106
+ component: () => ArmyPart({ client }) as ReturnType<NonNullable<HudWidgetDef['component']>>
107
+ })
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Every example on the client's side: their looks and controls by id, and what each category adds to the game's look.
3
+ * Fetched with the game's look (`src/view.ts`), never with the list's page
4
+ */
5
+ import type { CategoryLooks } from '../shell/look'
6
+ import { category as mechanics } from './mechanics/looks'
7
+ import { category as abilities } from './abilities/looks'
8
+ import { category as moba } from './moba/looks'
9
+ import { category as rts } from './rts/looks'
10
+ import { category as fx } from './fx/looks'
11
+ import { category as customFx } from './custom-fx/looks'
12
+ import { category as animation } from './animation/looks'
13
+ import { category as customAnimation } from './custom-animation/looks'
14
+ import { category as destruction } from './destruction/looks'
15
+ import { category as construction } from './construction/looks'
16
+ import { category as nature } from './nature/looks'
17
+ import { category as post } from './post/looks'
18
+ import { category as dynamicMap } from './dynamic-map/looks'
19
+ import { category as cinematics } from './cinematics/looks'
20
+ import { category as dialogs } from './dialogs/looks'
21
+ import { category as gui } from './gui/looks'
22
+ import { category as showcase } from './showcase/looks'
23
+ import { category as performance } from './performance/looks'
24
+
25
+ /** The categories' looks, in the list's order */
26
+ export const categoryLooks: readonly CategoryLooks[] = [
27
+ mechanics,
28
+ abilities,
29
+ moba,
30
+ rts,
31
+ fx,
32
+ customFx,
33
+ animation,
34
+ customAnimation,
35
+ destruction,
36
+ construction,
37
+ nature,
38
+ post,
39
+ dynamicMap,
40
+ cinematics,
41
+ dialogs,
42
+ gui,
43
+ showcase,
44
+ performance
45
+ ]
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Two armies, footmen in front and archers behind, facing each other across the field. Charge sends both at each
3
+ * other on attack-moves: each unit fights what it meets on the way (`attackMove`), the footmen close in and surround,
4
+ * the archers shoot from behind them
5
+ */
6
+ import { defineCommand, type World } from '@v12-engine/engine'
7
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, exampleField } from '../define'
9
+
10
+ /** Ranks of a unit type round a point, `across` wide, 1.4 apart, facing `facing` (radians) */
11
+ export function ranks(world: World, type: string, owner: number, at: { x: number; z: number }, count: number, across = 5): void {
12
+ for (let i = 0; i < count; i++) {
13
+ const row = Math.floor(i / across)
14
+ const column = i % across
15
+ world.spawn(type, { at: { x: at.x + (column - (across - 1) / 2) * 1.4, z: at.z + row * 1.4 }, owner })
16
+ }
17
+ }
18
+
19
+ /** The two armies of the battle example, facing each other across the field */
20
+ function battleArmies(world: World): void {
21
+ ranks(world, UNIT.footman, PLAYER, { x: 24, z: 14 }, 10)
22
+ ranks(world, UNIT.archer, PLAYER, { x: 24, z: 10 }, 5)
23
+ ranks(world, UNIT.footman, ENEMY, { x: 24, z: 32 }, 10)
24
+ ranks(world, UNIT.archer, ENEMY, { x: 24, z: 36 }, 5)
25
+ }
26
+
27
+ /** Sends both sides of the battle at each other, fighting what they meet (`attackMove`) */
28
+ export const charge = defineCommand('mechanics.charge', {}, (world) => {
29
+ for (const unit of world.units.values()) {
30
+ if (!unit.alive || (unit.owner !== PLAYER && unit.owner !== ENEMY)) continue
31
+ unit.order('attackMove', { x: unit.pos.x, z: unit.owner === PLAYER ? 40 : 6 })
32
+ }
33
+ })
34
+
35
+ /** Takes every unit away and lays the battle out again */
36
+ export const resetBattle = defineCommand('mechanics.reset', {}, (world) => {
37
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
38
+ battleArmies(world)
39
+ })
40
+
41
+ export const battle: ExampleDef = {
42
+ id: 'mechanics-battle',
43
+ category: 'mechanics',
44
+ title: 'Two armies',
45
+ summary:
46
+ 'Footmen and archers against footmen and archers. Charge sends both sides at each other; select yours (click or drag) and right-click to lead them yourself.',
47
+ tags: ['combat', 'fight', 'attack', 'selection', 'orders'],
48
+ map: () => exampleField({ name: 'Battle', players: 2 }),
49
+ start: (world) => battleArmies(world),
50
+ source: ['examples/mechanics/battle.ts', 'examples/mechanics/looks.ts']
51
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Damage types, armor and magic resist. An attack deals `physical` damage unless it says `magic` (or `true`, which
3
+ * nothing lessens); armor lessens what is physical and magic resist what is magic, 100 of either halving it. Four
4
+ * targets in a square: two plated (armor 100), two warded (magic resist 100), each shot by archers or by sorceresses
5
+ */
6
+ import { defineCommand, type UnitTypeDef, type World } from '@v12-engine/engine'
7
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, exampleField } from '../define'
9
+
10
+ /** A post in plate: armor 100, no magic resist. Ignored by everyone but who is told to hit it (`ignored`), and it never dies (`undying`) */
11
+ export const plated: UnitTypeDef = {
12
+ id: 'mechanicsPlated',
13
+ name: 'Plated post',
14
+ kind: 'unit',
15
+ radius: 0.5,
16
+ stats: { health: 4000, armor: 100, moveSpeed: 0 },
17
+ sight: 2,
18
+ ignored: true,
19
+ undying: true
20
+ }
21
+
22
+ /** A post with a ward: magic resist 100, no armor */
23
+ export const warded: UnitTypeDef = { ...plated, id: 'mechanicsWarded', name: 'Warded post', stats: { health: 4000, magicResist: 100, moveSpeed: 0 } }
24
+
25
+ /** A sorceress: she shoots bolts of magic (`damageType: 'magic'`), as hard as an archer's arrows */
26
+ export const sorceress: UnitTypeDef = {
27
+ id: 'mechanicsSorceress',
28
+ name: 'Sorceress',
29
+ kind: 'unit',
30
+ radius: 0.42,
31
+ stats: { health: 260, attackDamage: 18, attackSpeed: 1, attackRange: 7, moveSpeed: 4 },
32
+ sight: 11,
33
+ attacks: [{ damageType: 'magic', damagePoint: 0.5, acquisitionRange: 8, projectileSpeed: 18 }]
34
+ }
35
+
36
+ /** The four targets, by where they stand: what shoots each, and what it is */
37
+ export const TARGETS = [
38
+ { name: 'plated · arrows', type: plated.id, shooter: UNIT.archer, at: { x: 18, z: 28 } },
39
+ { name: 'plated · magic', type: plated.id, shooter: sorceress.id, at: { x: 30, z: 28 } },
40
+ { name: 'warded · arrows', type: warded.id, shooter: UNIT.archer, at: { x: 18, z: 18 } },
41
+ { name: 'warded · magic', type: warded.id, shooter: sorceress.id, at: { x: 30, z: 18 } }
42
+ ] as const
43
+
44
+ /** The targets, whole, each with two shooters in front of it, standing still (`hold`) */
45
+ function layOut(world: World): void {
46
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
47
+ for (const each of TARGETS) {
48
+ // the enemy's, which nobody goes for unless told to (`ignored`)
49
+ world.spawn(each.type, { at: each.at, owner: ENEMY, name: each.name })
50
+ for (const side of [-1, 1])
51
+ world.spawn(each.shooter, { at: { x: each.at.x + side * 1.2, z: each.at.z - 5 }, owner: PLAYER, facing: 0 }).order('hold')
52
+ }
53
+ }
54
+
55
+ /** Each pair of shooters fires at its own target */
56
+ export const fire = defineCommand('mechanics.fire', {}, (world, player) => {
57
+ for (const each of TARGETS) {
58
+ const target = world.named(each.name)
59
+ if (!target) continue
60
+ // the two that stand in front of it, 5 cells off
61
+ const shooters = [...world.units.values()].filter(
62
+ (unit) => unit.alive && unit.owner === player && Math.abs(unit.pos.x - each.at.x) < 3 && Math.abs(unit.pos.z - (each.at.z - 5)) < 2
63
+ )
64
+ world.issue({ type: 'order', player, entities: shooters.map((unit) => unit.id), order: { type: 'attack', target: target.id } })
65
+ }
66
+ })
67
+
68
+ export const resetDamage = defineCommand('mechanics.resetDamage', {}, (world) => layOut(world))
69
+
70
+ export const damage: ExampleDef = {
71
+ id: 'mechanics-damage',
72
+ category: 'mechanics',
73
+ title: 'Armor and damage types',
74
+ summary:
75
+ 'Arrows deal physical damage, which armor lessens; a sorceress’s bolts deal magic, which magic resist lessens. Press Fire and watch what each post loses: the plated one shrugs off arrows, the warded one bolts.',
76
+ tags: ['armor', 'magic resist', 'damage type', 'physical', 'magic', 'mitigation', 'combat'],
77
+ map: () => exampleField({ name: 'Range', players: 2 }),
78
+ start: (world) => layOut(world),
79
+ source: ['examples/mechanics/damage.ts', 'examples/mechanics/looks.ts']
80
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Flying units: a unit type with `flying: true` goes straight over cliffs, trees and buildings, and is hit only by
3
+ * attacks that name the air in their `targets`. A ridge cuts the field in two, with one gap at its far end: walkers go
4
+ * round by the gap, the gryphon flies over. A footman's sword cannot reach a flyer; a crossbow can
5
+ */
6
+ import { createMap, defineCommand, type GameMap, type UnitTypeDef, type World } from '@v12-engine/engine'
7
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, tiles } from '../define'
9
+
10
+ /** A gryphon rider: it flies, and its bolts hit the ground and the air */
11
+ export const gryphon: UnitTypeDef = {
12
+ id: 'mechanicsGryphon',
13
+ name: 'Gryphon Rider',
14
+ kind: 'unit',
15
+ radius: 0.6,
16
+ flying: true,
17
+ stats: { health: 500, attackDamage: 22, attackSpeed: 0.8, attackRange: 4.5, armor: 10, moveSpeed: 5 },
18
+ sight: 12,
19
+ turnRate: 6,
20
+ attacks: [{ damagePoint: 0.5, acquisitionRange: 8, projectileSpeed: 14, targets: ['ground', 'air'] }]
21
+ }
22
+
23
+ /** A crossbowman: an archer whose bolts reach the air as well as the ground. The shared archer's reach the ground alone */
24
+ export const crossbowman: UnitTypeDef = {
25
+ id: 'mechanicsCrossbowman',
26
+ name: 'Crossbowman',
27
+ kind: 'unit',
28
+ radius: 0.42,
29
+ stats: { health: 300, attackDamage: 16, attackSpeed: 1, attackRange: 7, moveSpeed: 4.2 },
30
+ sight: 11,
31
+ attacks: [{ damagePoint: 0.5, acquisitionRange: 8, projectileSpeed: 26, targets: ['ground', 'air'] }]
32
+ }
33
+
34
+ const SIZE = 40
35
+ /** The ridge: the cells between these, all the way down the field but for the gap at its far end */
36
+ const RIDGE = { x0: 18, x1: 22, gapZ0: 33, gapZ1: 37 }
37
+ const WEST = { x: 8, z: 10 }
38
+ const EAST = { x: 32, z: 10 }
39
+
40
+ /** A field cut by a rocky ridge one level up, its faces cliffs, with a gap through it at the far end */
41
+ function ridgeMap(): GameMap {
42
+ const map = createMap({ name: 'Ridge', width: SIZE, depth: SIZE, players: 2, game: 'examples', tileset: tiles.map((each) => each.id) })
43
+ map.tiles.fill(map.tileset.indexOf('grass') + 1)
44
+ const rock = map.tileset.indexOf('rock') + 1
45
+ for (let z = 0; z < SIZE; z++) {
46
+ if (z >= RIDGE.gapZ0 && z < RIDGE.gapZ1) continue
47
+ for (let x = RIDGE.x0; x < RIDGE.x1; x++) {
48
+ map.levels[z * SIZE + x] = 1
49
+ map.tiles[z * SIZE + x] = rock
50
+ }
51
+ }
52
+ map.players[0].start = { x: 20, z: 14 }
53
+ map.players[1].start = EAST
54
+ return map
55
+ }
56
+
57
+ /** The player's three on the west side, the enemy's taken away */
58
+ function layOut(world: World): void {
59
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
60
+ world.spawn(UNIT.footman, { at: { x: WEST.x, z: WEST.z - 1.5 }, owner: PLAYER })
61
+ world.spawn(crossbowman.id, { at: { x: WEST.x, z: WEST.z }, owner: PLAYER })
62
+ world.spawn(gryphon.id, { at: { x: WEST.x, z: WEST.z + 1.5 }, owner: PLAYER })
63
+ }
64
+
65
+ /** All three go to the other side of the ridge, or back: each by its own way */
66
+ export const cross = defineCommand('mechanics.cross', {}, (world, player) => {
67
+ for (const unit of world.units.values()) {
68
+ if (!unit.alive || unit.owner !== player) continue
69
+ const to = unit.pos.x < SIZE / 2 ? EAST : WEST
70
+ unit.order('move', { x: to.x, z: to.z + (unit.def.id === UNIT.footman ? -1.5 : unit.def.id === gryphon.id ? 1.5 : 0) })
71
+ }
72
+ })
73
+
74
+ /** Two enemy gryphons fly in at the player's units: the footman can only watch them */
75
+ export const flyIn = defineCommand('mechanics.flyIn', {}, (world, player) => {
76
+ const mine = [...world.units.values()].find((unit) => unit.alive && unit.owner === player)
77
+ const at = mine?.pos ?? WEST
78
+ for (let i = 0; i < 2; i++) world.spawn(gryphon.id, { at: { x: at.x + 10, z: at.z + 14 + i * 2 }, owner: ENEMY }).order('attackMove', at)
79
+ })
80
+
81
+ export const resetFlying = defineCommand('mechanics.resetFlying', {}, (world) => layOut(world))
82
+
83
+ export const flying: ExampleDef = {
84
+ id: 'mechanics-flying',
85
+ category: 'mechanics',
86
+ title: 'Flying units',
87
+ summary:
88
+ 'A gryphon flies straight over the ridge while the footman and the crossbowman walk round by its gap. Send enemy flyers: the footman cannot hit them, the crossbowman and the gryphon can.',
89
+ tags: ['flying', 'air', 'flyer', 'anti-air', 'cliff', 'targets'],
90
+ map: ridgeMap,
91
+ start: (world) => layOut(world),
92
+ source: ['examples/mechanics/flying.ts', 'examples/mechanics/looks.ts']
93
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Formations: a group sent to a point lays itself out as rows, a ring, a filled square or a crowd on the point. The
3
+ * order says which (`formation` of a `move`); the engine gives each unit its place and fits them to their places with
4
+ * the fewest crossings, and those of a formation trade places on the way rather than jostle
5
+ */
6
+ import { defineCommand, oneOf, type World, exactMath } from '@v12-engine/engine'
7
+ import { PLAYER, UNIT } from '../common/units'
8
+ import type { ExampleDef } from '../define'
9
+
10
+ /** The engine's formations, by id: what the card offers */
11
+ export const FORMATIONS = ['rows', 'circle', 'square', 'none'] as const
12
+
13
+ /** The two points the group goes between, across the field */
14
+ const ENDS = [
15
+ { x: 24, z: 14 },
16
+ { x: 24, z: 34 }
17
+ ]
18
+
19
+ /** Twenty footmen and ten archers, scattered round the first point */
20
+ function army(world: World): void {
21
+ for (let i = 0; i < 30; i++) {
22
+ const angle = i * 2.4
23
+ const away = 1.5 + (i % 7) * 0.9
24
+ world.spawn(i < 20 ? UNIT.footman : UNIT.archer, {
25
+ at: { x: ENDS[0].x + exactMath.cos(angle) * away, z: ENDS[0].z + exactMath.sin(angle) * away },
26
+ owner: PLAYER
27
+ })
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Sends the player's units to the far end of the two in a formation: the end that is further from where most of them
33
+ * stand. Through the order command, as a right click would (`world.issue`), so that it is the same order a player gives
34
+ */
35
+ export const march = defineCommand('mechanics.march', { formation: oneOf(...FORMATIONS) }, (world, player, { formation }) => {
36
+ const units = [...world.units.values()].filter((unit) => unit.alive && unit.owner === player && unit.isUnit)
37
+ if (!units.length) return
38
+ const middle = units.reduce((sum, unit) => sum + unit.pos.z, 0) / units.length
39
+ const to = Math.abs(middle - ENDS[0].z) < Math.abs(middle - ENDS[1].z) ? ENDS[1] : ENDS[0]
40
+ world.issue({ type: 'order', player, entities: units.map((unit) => unit.id), order: { type: 'move', point: to, formation } })
41
+ })
42
+
43
+ /** Scatters the group again round the first point */
44
+ export const scatter = defineCommand('mechanics.scatter', {}, (world) => {
45
+ for (const unit of [...world.units.values()]) if (unit.alive && unit.owner === PLAYER) world.kill(unit, undefined, false, 'removed')
46
+ army(world)
47
+ })
48
+
49
+ export const formations: ExampleDef = {
50
+ id: 'mechanics-formations',
51
+ category: 'mechanics',
52
+ title: 'Formations',
53
+ summary:
54
+ 'Thirty units sent across the field in rows, a ring, a filled square or no formation at all. Pick one and press March; your own right-clicks take it too, and a right-click drag turns it.',
55
+ tags: ['formation', 'move', 'group', 'rows', 'circle', 'square', 'order'],
56
+ start: (world) => army(world),
57
+ source: ['examples/mechanics/formations.ts', 'examples/mechanics/looks.ts']
58
+ }