@v12-engine/create 0.1.0 → 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 +2 -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,82 @@
1
+ /**
2
+ * Sight, fog of war and high ground: each side sees only what its units see (`sight`), the rest of the map dark. And
3
+ * nobody sees up a cliff: archers on a plateau shoot down at units below that cannot see them, until those climb the
4
+ * ramp. The map is played with `reveal: false`, so the player sees through their own units' eyes alone
5
+ */
6
+ import { createMap, defineCommand, type GameMap, type World } from '@v12-engine/engine'
7
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, tiles } from '../define'
9
+
10
+ /** The size of the field, and the plateau on its east side: its cells, from and to */
11
+ const SIZE = 40
12
+ const PLATEAU = { x0: 22, x1: 36, z0: 10, z1: 30 }
13
+ /** The ramp up its west face: four cells wide, two below and two on top */
14
+ const RAMP = { x0: 20, x1: 24, z0: 18, z1: 22 }
15
+ /** Where the player's party waits below, and the top of the ramp */
16
+ const BELOW = { x: 12, z: 20 }
17
+ const TOP = { x: 26, z: 20 }
18
+
19
+ /** A field of grass with a rocky plateau of one level up, and a ramp to it */
20
+ function highGroundMap(): GameMap {
21
+ const map = createMap({ name: 'High ground', width: SIZE, depth: SIZE, players: 2, game: 'examples', tileset: tiles.map((each) => each.id) })
22
+ const grass = map.tileset.indexOf('grass') + 1
23
+ const rock = map.tileset.indexOf('rock') + 1
24
+ const dirt = map.tileset.indexOf('dirt') + 1
25
+ map.tiles.fill(grass)
26
+ for (let z = PLATEAU.z0; z < PLATEAU.z1; z++)
27
+ for (let x = PLATEAU.x0; x < PLATEAU.x1; x++) {
28
+ map.levels[z * SIZE + x] = 1
29
+ map.tiles[z * SIZE + x] = rock
30
+ }
31
+ for (let z = RAMP.z0; z < RAMP.z1; z++)
32
+ for (let x = RAMP.x0; x < RAMP.x1; x++) {
33
+ map.ramps[z * SIZE + x] = 1
34
+ map.tiles[z * SIZE + x] = dirt
35
+ }
36
+ map.players[0].start = BELOW
37
+ map.players[1].start = { x: 30, z: 20 }
38
+ return map
39
+ }
40
+
41
+ /** The player's party below the cliff, and the enemy's archers on the plateau above it, all taken away first */
42
+ function layOut(world: World): void {
43
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
44
+ ;[UNIT.footman, UNIT.footman, UNIT.knight, UNIT.archer].forEach((type, i) =>
45
+ world.spawn(type, { at: { x: BELOW.x + 1, z: BELOW.z - 1.5 + i }, owner: PLAYER })
46
+ )
47
+ // they hold their ground (`hold`): they shoot what comes in reach, but never walk down after it
48
+ for (let i = 0; i < 3; i++) world.spawn(UNIT.archer, { at: { x: 24.5, z: 16 + i * 4 }, owner: ENEMY }).order('hold')
49
+ // one more archer deeper on the plateau, out of the reach of anything below: seen only from the top of it
50
+ world.spawn(UNIT.archer, { at: { x: 32, z: 20 }, owner: ENEMY }).order('hold')
51
+ }
52
+
53
+ /** The party walks up the ramp to the top: they see the plateau, and the archers on it, as they get there */
54
+ export const climb = defineCommand('mechanics.climb', {}, (world, player) => {
55
+ const ids = [...world.units.values()].filter((unit) => unit.alive && unit.owner === player).map((unit) => unit.id)
56
+ world.issue({ type: 'order', player, entities: ids, order: { type: 'attackMove', point: TOP } })
57
+ })
58
+
59
+ /**
60
+ * The party walks to the foot of the cliff, by the archers' feet, and holds there (`hold`, queued after the move):
61
+ * shot at from the dark, it cannot see whom to hit back, and holding it does not go looking
62
+ */
63
+ export const underCliff = defineCommand('mechanics.underCliff', {}, (world, player) => {
64
+ const ids = [...world.units.values()].filter((unit) => unit.alive && unit.owner === player).map((unit) => unit.id)
65
+ world.issue({ type: 'order', player, entities: ids, order: { type: 'move', point: { x: 19, z: 14 } } })
66
+ world.issue({ type: 'order', player, entities: ids, order: { type: 'hold' }, queue: true })
67
+ })
68
+
69
+ export const resetHighGround = defineCommand('mechanics.resetHighGround', {}, (world) => layOut(world))
70
+
71
+ export const highGround: ExampleDef = {
72
+ id: 'mechanics-high-ground',
73
+ category: 'mechanics',
74
+ title: 'Fog of war and high ground',
75
+ summary:
76
+ 'You see only what your units see, and nobody sees up a cliff. Walk under the cliff and arrows come from the dark; climb the ramp and the archers on the plateau are seen, and fought.',
77
+ tags: ['fog', 'fog of war', 'sight', 'vision', 'cliff', 'high ground', 'ramp', 'reveal'],
78
+ map: highGroundMap,
79
+ reveal: false,
80
+ start: (world) => layOut(world),
81
+ source: ['examples/mechanics/highGround.ts', 'examples/mechanics/looks.ts']
82
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The core mechanics' examples on the rules' side: their list entries, maps and starts, and what they add to the game
3
+ * (`ExampleCategory`). Each example is a file of its own here: its units, its commands and its start
4
+ */
5
+ import type { Game } from '@v12-engine/engine'
6
+ import type { ExampleCategory } from '../define'
7
+ import { battle, charge, resetBattle } from './battle'
8
+ import { damage, fire, plated, resetDamage, sorceress, warded } from './damage'
9
+ import { cross, crossbowman, flyIn, flying, gryphon, resetFlying } from './flying'
10
+ import { formations, march, scatter } from './formations'
11
+ import { climb, highGround, resetHighGround, underCliff } from './highGround'
12
+ import { crate, maze, newMaze, run } from './maze'
13
+ import { ordersExample, raid, resetOrders, resume, stopAll } from './orders'
14
+ import { selection } from './selection'
15
+
16
+ export { ranks } from './battle'
17
+
18
+ export const category: ExampleCategory = {
19
+ rules: (game: Game<any>) => {
20
+ for (const unit of [crate, plated, warded, sorceress, gryphon, crossbowman]) game.unit(unit)
21
+ for (const command of [
22
+ charge,
23
+ resetBattle,
24
+ march,
25
+ scatter,
26
+ run,
27
+ newMaze,
28
+ climb,
29
+ underCliff,
30
+ resetHighGround,
31
+ raid,
32
+ stopAll,
33
+ resume,
34
+ resetOrders,
35
+ fire,
36
+ resetDamage,
37
+ cross,
38
+ flyIn,
39
+ resetFlying
40
+ ])
41
+ game.command(command)
42
+ },
43
+ examples: [battle, selection, formations, maze, ordersExample, highGround, damage, flying]
44
+ }
@@ -0,0 +1,289 @@
1
+ /** The core mechanics' examples on the client's side: their looks and controls, by id (`CategoryLooks`) */
2
+ import { type GameClientApi, presetPings, type UnitLook } from '@v12-engine/client'
3
+ import { button, readout, select, type CategoryLooks } from '../../shell/look'
4
+ import { character, hold, person } from '../common/looks'
5
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
6
+ import { charge, resetBattle } from './battle'
7
+ import { fire, plated, resetDamage, sorceress, TARGETS, warded } from './damage'
8
+ import { cross, crossbowman, flyIn, gryphon, resetFlying } from './flying'
9
+ import { FORMATIONS, march, scatter } from './formations'
10
+ import { climb, resetHighGround, underCliff } from './highGround'
11
+ import { crate, newMaze, run } from './maze'
12
+ import { raid, resetOrders, resume, stopAll } from './orders'
13
+
14
+ const STEEL: [number, number, number] = [0.62, 0.65, 0.7]
15
+ const WOOD: [number, number, number] = [0.45, 0.31, 0.18]
16
+
17
+ /**
18
+ * A post to shoot at: the shared art's training dummy in a tint, plated grey or warded violet. The parts (a pole and a
19
+ * body in a colour, a band of `band` round it) show until it has loaded
20
+ */
21
+ const post = (body: [number, number, number], band: [number, number, number], tint: [number, number, number]): UnitLook => ({
22
+ height: 1.9,
23
+ blood: false,
24
+ model: { src: 'props/dummy.glb', scale: 1, tint },
25
+ shapes: [
26
+ { shape: 'cylinder', size: [0.16, 1.1, 0.16], offset: [0, 0.55, 0], color: WOOD },
27
+ { shape: 'cylinder', size: [0.75, 1, 0.6], offset: [0, 1.25, 0], color: body },
28
+ { shape: 'cylinder', size: [0.8, 0.14, 0.65], offset: [0, 1.4, 0], color: band },
29
+ { shape: 'sphere', size: [0.5, 0.5, 0.5], offset: [0, 1.95, 0], color: body }
30
+ ]
31
+ })
32
+
33
+ /** The looks of the units the examples here add */
34
+ const looks: Record<string, UnitLook> = {
35
+ [crate.id]: {
36
+ height: 1,
37
+ blood: false,
38
+ model: { src: 'dungeon-kit/crate.glb', scale: 0.95 },
39
+ shapes: [{ shape: 'box', size: [0.95, 0.95, 0.95], offset: [0, 0.48, 0], color: WOOD }]
40
+ },
41
+ [plated.id]: post(STEEL, [0.35, 0.37, 0.42], [0.7, 0.75, 0.85]),
42
+ [warded.id]: post([0.42, 0.3, 0.62], [0.55, 0.85, 1], [0.75, 0.55, 1]),
43
+ [sorceress.id]: {
44
+ height: 1.8,
45
+ model: character('mage', 0.72, { hold: [hold('staff')] }),
46
+ shot: { color: [0.75, 0.4, 1], size: 0.35 },
47
+ shapes: person(1.8)
48
+ },
49
+ [crossbowman.id]: {
50
+ height: 1.7,
51
+ model: character('ranger', 0.68, { hold: [hold('crossbow_2handed')], animations: { attack: 'Ranged_2H_Shoot' } }),
52
+ shot: { color: [0.5, 0.45, 0.4], size: 0.2 },
53
+ shapes: person(1.7)
54
+ },
55
+ // a gryphon and its rider, drawn as parts: its body, head and beak, wings, tail and the rider in the side's colour. The
56
+ // shared art has no beast that flies, so the parts are its look
57
+ [gryphon.id]: {
58
+ height: 1.9,
59
+ altitude: 3.2,
60
+ shot: { color: [0.55, 0.8, 1], size: 0.3 },
61
+ shapes: [
62
+ { shape: 'sphere', size: [0.8, 0.7, 1.5], offset: [0, 0.7, 0], color: [0.72, 0.58, 0.36] },
63
+ { shape: 'sphere', size: [0.5, 0.5, 0.6], offset: [0, 1.05, 0.85], color: [0.93, 0.91, 0.84] },
64
+ { shape: 'cone', size: [0.2, 0.4, 0.2], offset: [0, 0.98, 1.25], color: [1, 0.8, 0.2], tilt: Math.PI / 2 },
65
+ { shape: 'box', size: [1.7, 0.08, 0.8], offset: [1.1, 0.95, 0], color: [0.6, 0.47, 0.28], turn: 0.25 },
66
+ { shape: 'box', size: [1.7, 0.08, 0.8], offset: [-1.1, 0.95, 0], color: [0.6, 0.47, 0.28], turn: -0.25 },
67
+ { shape: 'cone', size: [0.5, 0.9, 0.5], offset: [0, 0.65, -1], color: [0.6, 0.47, 0.28], tilt: -Math.PI / 2 },
68
+ { shape: 'box', size: [0.45, 0.7, 0.35], offset: [0, 1.35, -0.1], color: 'team' },
69
+ { shape: 'sphere', size: [0.34, 0.34, 0.34], offset: [0, 1.85, -0.1], color: STEEL }
70
+ ]
71
+ }
72
+ }
73
+
74
+ /** The live units of the player's, or of `owner`, as the screen knows them */
75
+ const unitsOf = (client: GameClientApi, owner = PLAYER) =>
76
+ client.session.snapshot().entities.filter((each) => each.owner === owner && each.health > 0)
77
+ /** How many of `owner`'s are alive */
78
+ const count = (client: GameClientApi, owner: number) => unitsOf(client, owner).length
79
+ /** Selects the player's units of a type, as a click on each would */
80
+ const selectType = (client: GameClientApi, type: string) =>
81
+ client.controller.select(
82
+ unitsOf(client)
83
+ .filter((each) => each.type === type)
84
+ .map((each) => each.id)
85
+ )
86
+
87
+ /** Looks down at a square of the field `size` cells across round a point: from far enough to see all of it */
88
+ const view = (client: GameClientApi, x: number, z: number, size: number) =>
89
+ client.camera.frame({ minX: x - size / 2, minZ: z - size / 2, maxX: x + size / 2, maxZ: z + size / 2 })
90
+
91
+ /** The formation chosen on the card, which March sends */
92
+ let formation: (typeof FORMATIONS)[number] = 'rows'
93
+ /** The seed of the maze stacked last */
94
+ let seed = 1
95
+
96
+ export const category: CategoryLooks = {
97
+ view: (view) => ({ ...view, units: { ...view.units, looks: { ...view.units?.looks, ...looks } } }),
98
+ looks: {
99
+ 'mechanics-battle': {
100
+ controls: [
101
+ button(
102
+ 'Charge',
103
+ (client) => client.session.send(charge.make({})),
104
+ 'Sends both armies at each other; each unit fights whatever it meets on the way'
105
+ ),
106
+ button(
107
+ 'Lay it out again',
108
+ (client) => client.session.send(resetBattle.make({})),
109
+ 'Takes every unit away and lines both armies up again, whole'
110
+ ),
111
+ readout('Yours · theirs', (client) => `${count(client, PLAYER)} · ${count(client, ENEMY)}`, 'How many units each side has left alive')
112
+ ]
113
+ },
114
+ 'mechanics-selection': {
115
+ // the ready-made pings' looks: a kind with none is sent and drawn nowhere
116
+ view: (view) => ({ ...view, pings: { ...view.pings, kinds: view.pings?.kinds ?? presetPings() } }),
117
+ ready: (client) => view(client, 24, 23, 18),
118
+ controls: [
119
+ button('Footmen', (client) => selectType(client, UNIT.footman), 'Selects every footman of yours, as a double-click on one would'),
120
+ button('Archers', (client) => selectType(client, UNIT.archer), 'Selects every archer of yours'),
121
+ button('The whole army', (client) => client.controller.selectArmy(), 'Selects every unit of yours that fights, as F2 does in the HUD'),
122
+ button('Keep as group 1', (client) => client.controller.assignGroup(1), 'Keeps what is selected as group 1, as Ctrl+1 does'),
123
+ button('Take group 1', (client) => client.controller.recallGroup(1), 'Selects group 1 again, as pressing 1 does'),
124
+ button(
125
+ 'Ping the enemy',
126
+ (client) => {
127
+ const [enemy] = unitsOf(client, ENEMY)
128
+ if (enemy) client.controller.ping({ x: enemy.x, z: enemy.z }, 'danger')
129
+ },
130
+ 'Pings the first enemy unit on the map and the minimap, for your allies and you'
131
+ ),
132
+ readout(
133
+ 'Selected',
134
+ (client) => {
135
+ const ids = new Set(client.controller.selected)
136
+ const types = new Map<string, number>()
137
+ for (const each of client.session.snapshot().entities) if (ids.has(each.id)) types.set(each.type, (types.get(each.type) ?? 0) + 1)
138
+ return [...types].map(([type, n]) => `${n} ${type}`).join(', ') || 'nothing'
139
+ },
140
+ 'What is selected now, by type: the buttons above change it, and so do your clicks'
141
+ )
142
+ ]
143
+ },
144
+ 'mechanics-formations': {
145
+ ready: (client) => {
146
+ client.controller.setFormation(formation)
147
+ view(client, 24, 24, 19)
148
+ // the player's own orders back to the game's formation as the example is left
149
+ return () => client.controller.setFormation(undefined)
150
+ },
151
+ controls: [
152
+ select(
153
+ 'Formation',
154
+ FORMATIONS,
155
+ formation,
156
+ (client, value) => {
157
+ formation = value as typeof formation
158
+ client.controller.setFormation(formation)
159
+ },
160
+ 'The shape the group takes where it is sent: by March, and by your own right-clicks'
161
+ ),
162
+ button(
163
+ 'March',
164
+ (client) => client.session.send(march.make({ formation })),
165
+ 'Sends all thirty to the other end of the field in the formation chosen above'
166
+ ),
167
+ button('Scatter them', (client) => client.session.send(scatter.make({})), 'Puts the thirty back in a loose crowd at the near end')
168
+ ]
169
+ },
170
+ 'mechanics-maze': {
171
+ ready: (client) => view(client, 16, 13, 25),
172
+ controls: [
173
+ button(
174
+ 'Run',
175
+ (client) => {
176
+ // the runners selected, so that their paths are drawn
177
+ client.controller.select(unitsOf(client).map((each) => each.id))
178
+ client.session.send(run.make({}))
179
+ },
180
+ 'Selects the three runners, so that their paths are drawn, and sends them to the far corner of the maze (or back)'
181
+ ),
182
+ button(
183
+ 'New maze',
184
+ (client) => client.session.send(newMaze.make({ seed: ++seed })),
185
+ 'Stacks a different maze of crates and puts the runners back in its first corner'
186
+ )
187
+ ]
188
+ },
189
+ 'mechanics-orders': {
190
+ ready: (client) => view(client, 24, 24, 24),
191
+ controls: [
192
+ button(
193
+ 'Send raiders',
194
+ (client) => client.session.send(raid.make({})),
195
+ 'Three enemy raiders come down the field: watch who fights them, who only shoots, and who walks past'
196
+ ),
197
+ button('Stop everyone', (client) => client.session.send(stopAll.make({})), 'Drops every order: they fight what comes near where they stand'),
198
+ button(
199
+ 'Back to their orders',
200
+ (client) => client.session.send(resume.make({})),
201
+ 'Gives each group its order again: the footmen patrol, the archers hold, the knight walks up and down'
202
+ ),
203
+ button(
204
+ 'Lay it out again',
205
+ (client) => client.session.send(resetOrders.make({})),
206
+ 'Puts your three groups back where they started, on their orders, and takes the raiders away'
207
+ ),
208
+ readout(
209
+ 'Footmen · archers · knight',
210
+ (client) => {
211
+ const mine = unitsOf(client)
212
+ const order = (type: string) => mine.find((each) => each.type === type)?.own?.order ?? 'idle'
213
+ return `${order(UNIT.footman)} · ${order(UNIT.archer)} · ${order(UNIT.knight)}`
214
+ },
215
+ 'The order each group is on now, as the game tells the screen'
216
+ )
217
+ ]
218
+ },
219
+ 'mechanics-high-ground': {
220
+ ready: (client) => view(client, 21, 19, 20),
221
+ controls: [
222
+ button(
223
+ 'Walk under the cliff',
224
+ (client) => client.session.send(underCliff.make({})),
225
+ 'Your party walks to the foot of the cliff and holds there: arrows come down from archers it cannot see'
226
+ ),
227
+ button(
228
+ 'Climb the ramp',
229
+ (client) => client.session.send(climb.make({})),
230
+ 'Your party walks up the ramp: at the top the archers on the plateau come out of the dark, and are fought'
231
+ ),
232
+ button(
233
+ 'Lay it out again',
234
+ (client) => client.session.send(resetHighGround.make({})),
235
+ 'Your party back below the cliff and the archers back on the plateau, all whole'
236
+ ),
237
+ // what the screen is told of the enemy's: only what the player's side sees
238
+ readout(
239
+ 'Enemy archers seen',
240
+ (client) => `${client.session.snapshot().entities.filter((each) => each.owner === ENEMY && each.visible).length} of 4`,
241
+ 'How many of the enemy archers in reach of the ramp your side can see now'
242
+ ),
243
+ readout('Yours left', (client) => `${count(client, PLAYER)}`, 'Your party members still alive')
244
+ ]
245
+ },
246
+ 'mechanics-damage': {
247
+ ready: (client) => view(client, 24, 22, 17),
248
+ controls: [
249
+ button('Fire', (client) => client.session.send(fire.make({})), 'Each pair of shooters attacks the post in front of it'),
250
+ button(
251
+ 'Whole again',
252
+ (client) => client.session.send(resetDamage.make({})),
253
+ 'The posts whole again and the shooters back in place, holding fire'
254
+ ),
255
+ ...TARGETS.map((target) =>
256
+ readout(
257
+ target.name,
258
+ (client) => {
259
+ const post = client.session.snapshot().entities.find((each) => each.name === target.name)
260
+ return post ? `${Math.round((post.maxHealth ?? 4000) - post.health)} lost` : '—'
261
+ },
262
+ 'Health this post has lost: armor lessens arrows, magic resist lessens bolts'
263
+ )
264
+ )
265
+ ]
266
+ },
267
+ 'mechanics-flying': {
268
+ ready: (client) => view(client, 20, 20, 28),
269
+ controls: [
270
+ button(
271
+ 'Cross the ridge',
272
+ (client) => client.session.send(cross.make({})),
273
+ 'Sends your three to the other side: the gryphon flies straight over, the two on foot go round by the gap'
274
+ ),
275
+ button(
276
+ 'Enemy flyers',
277
+ (client) => client.session.send(flyIn.make({})),
278
+ 'Two enemy gryphons fly at your units: the footman cannot reach them, the crossbowman and your gryphon can'
279
+ ),
280
+ button(
281
+ 'Lay it out again',
282
+ (client) => client.session.send(resetFlying.make({})),
283
+ 'Your three back on the west side, the enemy flyers taken away'
284
+ ),
285
+ readout('Enemy flyers', (client) => `${count(client, ENEMY)}`, 'Enemy gryphons still in the air')
286
+ ]
287
+ }
288
+ }
289
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Finding the way: crates stacked into a maze, and units sent from its one corner to the other.
3
+ * Each crate is a building of one cell, so it shuts its cell to walkers; the engine finds a path round them all, and
4
+ * cuts the ground's paths anew where crates are taken away or put down while the game runs
5
+ */
6
+ import { defineCommand, number, type UnitTypeDef, type World } from '@v12-engine/engine'
7
+ import { PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, exampleField } from '../define'
9
+
10
+ /** A crate: a building of one cell that nobody fights, and that cannot be hurt. A maze is made of them */
11
+ export const crate: UnitTypeDef = {
12
+ id: 'mechanicsCrate',
13
+ name: 'Crate',
14
+ kind: 'building',
15
+ radius: 0.5,
16
+ footprint: { w: 1, d: 1 },
17
+ stats: { health: 100 },
18
+ sight: 0,
19
+ invulnerable: true,
20
+ ignored: true
21
+ }
22
+
23
+ /** Rooms of the maze a side, each two cells wide with a cell of wall between: wide enough to walk at full speed */
24
+ const ROOMS = 7
25
+ /** The cell the maze starts at, both ways: the field is 32 a side and the maze 22 */
26
+ const AT = 5
27
+ /** Cells of the maze a side, its outer wall too: shut all round, so that the only way is through */
28
+ const SIDE = ROOMS * 3 + 1
29
+ /** Where the runners start, in the first room of the maze (its south-west corner), and where they go, the last room */
30
+ const FROM = { x: AT + 2, z: AT + 2 }
31
+ const TO = { x: AT + SIDE - 2, z: AT + SIDE - 2 }
32
+
33
+ /**
34
+ * The walls of a maze of `ROOMS` by `ROOMS` rooms, a passage dug from each room to one it had not reached yet (a
35
+ * depth-first walk), drawn from `seed`: true for a cell that is wall. The same seed is the same maze
36
+ */
37
+ export function mazeWalls(seed: number): boolean[] {
38
+ let state = (seed * 2654435761) >>> 0 || 1
39
+ const random = () => (state = (state * 1664525 + 1013904223) >>> 0) / 4294967296
40
+ const wall = new Array<boolean>(SIDE * SIDE).fill(true)
41
+ const dig = (x: number, z: number) => {
42
+ wall[z * SIDE + x] = false
43
+ wall[z * SIDE + x + 1] = false
44
+ wall[(z + 1) * SIDE + x] = false
45
+ wall[(z + 1) * SIDE + x + 1] = false
46
+ }
47
+ const seen = new Set<number>()
48
+ const stack = [0]
49
+ seen.add(0)
50
+ dig(1, 1)
51
+ while (stack.length) {
52
+ const room = stack[stack.length - 1]
53
+ const rx = room % ROOMS
54
+ const rz = Math.floor(room / ROOMS)
55
+ const next = [
56
+ [rx + 1, rz],
57
+ [rx - 1, rz],
58
+ [rx, rz + 1],
59
+ [rx, rz - 1]
60
+ ].filter(([x, z]) => x >= 0 && z >= 0 && x < ROOMS && z < ROOMS && !seen.has(z * ROOMS + x))
61
+ if (!next.length) {
62
+ stack.pop()
63
+ continue
64
+ }
65
+ const [nx, nz] = next[Math.floor(random() * next.length)]
66
+ // the room, and the wall between the two: two cells from the nearer room's inner half into the wall
67
+ dig(nx * 3 + 1, nz * 3 + 1)
68
+ if (nx !== rx) dig(Math.min(rx, nx) * 3 + 2, rz * 3 + 1)
69
+ else dig(rx * 3 + 1, Math.min(rz, nz) * 3 + 2)
70
+ seen.add(nz * ROOMS + nx)
71
+ stack.push(nz * ROOMS + nx)
72
+ }
73
+ return wall
74
+ }
75
+
76
+ /** Takes every crate away and stacks a maze of `seed` */
77
+ function buildMaze(world: World, seed: number): void {
78
+ // of every entity: a crate is no player's and only stands there, scenery, which `world.units` (the entities that act) leaves out
79
+ for (const entity of [...world.entities.values()]) if (entity.alive && entity.def.id === crate.id) world.kill(entity, undefined, false, 'removed')
80
+ const walls = mazeWalls(seed)
81
+ for (let z = 0; z < SIDE; z++)
82
+ for (let x = 0; x < SIDE; x++) if (walls[z * SIDE + x]) world.spawn(crate.id, { at: { x: AT + x + 0.5, z: AT + z + 0.5 }, owner: 0 })
83
+ }
84
+
85
+ /** The three runners in the first room: a footman, an archer and the knight, each finding its own way */
86
+ function runners(world: World): void {
87
+ for (const unit of [...world.units.values()]) if (unit.owner === PLAYER) world.kill(unit, undefined, false, 'removed')
88
+ ;[UNIT.footman, UNIT.archer, UNIT.knight].forEach((type, i) =>
89
+ world.spawn(type, { at: { x: FROM.x + (i % 2) * 0.9 - 0.45, z: FROM.z + (i - 1) * 0.7 }, owner: PLAYER })
90
+ )
91
+ }
92
+
93
+ /** Sends the runners through: to the far corner, or back to the first when they stand past the middle */
94
+ export const run = defineCommand('mechanics.run', {}, (world, player) => {
95
+ const units = [...world.units.values()].filter((unit) => unit.alive && unit.owner === player)
96
+ const past = units.some((unit) => unit.pos.x > AT + SIDE / 2)
97
+ world.issue({ type: 'order', player, entities: units.map((unit) => unit.id), order: { type: 'move', point: past ? FROM : TO } })
98
+ })
99
+
100
+ /** A new maze of the seed, the runners back in the first room */
101
+ export const newMaze = defineCommand('mechanics.maze', { seed: number }, (world, _player, { seed }) => {
102
+ buildMaze(world, seed)
103
+ runners(world)
104
+ })
105
+
106
+ export const maze: ExampleDef = {
107
+ id: 'mechanics-maze',
108
+ category: 'mechanics',
109
+ title: 'Paths through a maze',
110
+ summary:
111
+ 'A maze of crates, and three units in its corner that find their own way to the far one. Press Run, or right-click anywhere in it; New maze stacks another, and the paths are cut again at once.',
112
+ tags: ['pathfinding', 'path', 'navigation', 'maze', 'obstacles', 'walls'],
113
+ map: () => exampleField({ name: 'Maze', size: 32, tile: 'dirt' }),
114
+ start: (world) => {
115
+ buildMaze(world, 1)
116
+ runners(world)
117
+ },
118
+ source: ['examples/mechanics/maze.ts', 'examples/mechanics/looks.ts']
119
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Orders: what a unit is told to do, and what it does about enemies on the way. Footmen patrol between two flags and
3
+ * fight what they meet, then walk on; archers hold their position and shoot what comes in reach, but never chase; a
4
+ * knight on a plain move walks past every enemy. Raiders cross the field to show each of 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
+ /** The patrol's two flags, and the archers' post between them */
11
+ const WEST = { x: 12, z: 24 }
12
+ const EAST = { x: 36, z: 24 }
13
+ const POST = { x: 24, z: 28 }
14
+
15
+ /** Each group of the player's on its order: the patrol, the post held, and the knight walking up and down */
16
+ function orders(world: World): void {
17
+ for (const unit of world.units.values()) {
18
+ if (!unit.alive || unit.owner !== PLAYER) continue
19
+ if (unit.def.id === UNIT.footman) unit.order('patrol', unit.pos.x < 24 ? EAST : WEST)
20
+ else if (unit.def.id === UNIT.archer) unit.order('hold')
21
+ else if (unit.def.id === UNIT.knight) {
22
+ // moves one after another (`queue`): across the field and back, three times
23
+ const far = unit.pos.x < 24 ? 40 : 8
24
+ for (let leg = 0; leg < 6; leg++) unit.order('move', { x: leg % 2 ? 48 - far : far, z: 18 }, { queue: leg > 0 })
25
+ }
26
+ }
27
+ }
28
+
29
+ /** The player's three groups, the enemy's taken away */
30
+ function layOut(world: World): void {
31
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
32
+ for (let i = 0; i < 3; i++) world.spawn(UNIT.footman, { at: { x: WEST.x, z: WEST.z - 1.4 + i * 1.4 }, owner: PLAYER })
33
+ for (let i = 0; i < 4; i++) world.spawn(UNIT.archer, { at: { x: POST.x - 2.1 + i * 1.4, z: POST.z }, owner: PLAYER })
34
+ world.spawn(UNIT.knight, { at: { x: 8, z: 18 }, owner: PLAYER })
35
+ orders(world)
36
+ }
37
+
38
+ /** Three raiders come down the field from the north, fighting what they meet on the way to its south edge */
39
+ export const raid = defineCommand('mechanics.raid', {}, (world) => {
40
+ const x = 16 + world.rng.next() * 16
41
+ for (let i = 0; i < 3; i++) {
42
+ const raider = world.spawn(UNIT.footman, { at: { x: x + i * 1.4, z: 44 }, owner: ENEMY })
43
+ raider.order('attackMove', { x: x + i * 1.4, z: 4 })
44
+ }
45
+ })
46
+
47
+ /** Every unit of the player's stops: what it was told is dropped, and it fights what comes near where it stands */
48
+ export const stopAll = defineCommand('mechanics.stop', {}, (world, player) => {
49
+ const ids = [...world.units.values()].filter((unit) => unit.alive && unit.owner === player).map((unit) => unit.id)
50
+ world.issue({ type: 'stop', player, entities: ids })
51
+ })
52
+
53
+ /** Each group back on its order */
54
+ export const resume = defineCommand('mechanics.resume', {}, (world) => orders(world))
55
+
56
+ export const resetOrders = defineCommand('mechanics.resetOrders', {}, (world) => layOut(world))
57
+
58
+ export const ordersExample: ExampleDef = {
59
+ id: 'mechanics-orders',
60
+ category: 'mechanics',
61
+ title: 'Patrol, hold and move',
62
+ summary:
63
+ 'Footmen patrol between two points, archers hold their post, a knight walks up and down on a plain move. Send raiders and see who fights, who chases and who walks on; Stop drops every order.',
64
+ tags: ['orders', 'patrol', 'hold position', 'stop', 'attack move', 'move'],
65
+ map: () => exampleField({ name: 'Orders', players: 2 }),
66
+ start: (world) => layOut(world),
67
+ source: ['examples/mechanics/orders.ts', 'examples/mechanics/looks.ts']
68
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Selection and control groups, the player's hands: all of it is the client's (`client.controller`), the rules only
3
+ * lay the army out. Click a unit, drag a box, double-click for every one of its type on the screen; Ctrl and a number
4
+ * keeps the selection as a group, the number alone takes it back, twice looks at it. The card does the same from code
5
+ */
6
+ import type { World } from '@v12-engine/engine'
7
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, exampleField } from '../define'
9
+ import { ranks } from './battle'
10
+
11
+ /** The player's army in three blocks, and a few of the enemy's across the field to send them at */
12
+ function army(world: World): void {
13
+ ranks(world, UNIT.footman, PLAYER, { x: 18, z: 18 }, 8, 4)
14
+ ranks(world, UNIT.archer, PLAYER, { x: 30, z: 18 }, 6, 3)
15
+ world.spawn(UNIT.knight, { at: { x: 24, z: 16 }, owner: PLAYER })
16
+ world.spawn(UNIT.mage, { at: { x: 24, z: 19 }, owner: PLAYER })
17
+ ranks(world, UNIT.footman, ENEMY, { x: 24, z: 34 }, 6, 3)
18
+ }
19
+
20
+ export const selection: ExampleDef = {
21
+ id: 'mechanics-selection',
22
+ category: 'mechanics',
23
+ title: 'Selection and control groups',
24
+ summary:
25
+ 'Click, drag a box, double-click for all of a kind, Shift to add. Ctrl+1 keeps a selection as group 1 and 1 takes it back. The card selects by type, makes groups and pings the map from code.',
26
+ tags: ['selection', 'select', 'control group', 'hotkeys', 'box', 'ping', 'minimap'],
27
+ map: () => exampleField({ name: 'Selection', players: 2 }),
28
+ start: (world) => army(world),
29
+ source: ['examples/mechanics/selection.ts', 'examples/mechanics/looks.ts']
30
+ }