@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,43 @@
1
+ {
2
+ "id": "cinematics.tour",
3
+ "name": "A tour of Elmbrook",
4
+ "length": 18,
5
+ "lanes": [
6
+ {
7
+ "id": "camera",
8
+ "kind": "camera",
9
+ "items": [
10
+ { "at": 0, "value": { "x": 24, "z": 6, "distance": 40, "yaw": 0, "pitch": 0.8 } },
11
+ { "at": 6, "value": { "x": 24, "z": 22, "distance": 20, "yaw": 0.6, "pitch": 0.5 }, "path": "smooth", "ease": "inOutCubic" },
12
+ { "at": 12, "value": { "x": 24, "z": 30, "distance": 18, "yaw": 2.8, "pitch": 0.45 }, "path": "smooth", "ease": "inOutSine" },
13
+ { "at": 16, "value": { "x": 24, "z": 26, "distance": 42, "yaw": 3.6, "pitch": 0.9 }, "ease": "inOutSine" }
14
+ ]
15
+ },
16
+ {
17
+ "id": "screen",
18
+ "kind": "screen",
19
+ "items": [
20
+ { "at": 0, "for": 18, "letterbox": 2.39, "vignette": 0.3, "in": 0.6, "out": 0.8 },
21
+ { "at": 0, "for": 1.5, "fade": 1, "in": 0, "out": 1.5 }
22
+ ]
23
+ },
24
+ {
25
+ "id": "ui",
26
+ "kind": "ui",
27
+ "items": [{ "at": 0.5, "for": 4, "ui": "title", "title": "Elmbrook", "subtitle": "made in a file", "in": 0.6, "out": 0.8 }]
28
+ },
29
+ {
30
+ "id": "dialog",
31
+ "kind": "dialog",
32
+ "items": [
33
+ { "at": 1.5, "for": 4.5, "speaker": "cinematics.narrator", "text": "In the north there is a village where the road ends." },
34
+ { "at": 7.5, "for": 4.5, "speaker": "cinematics.narrator", "text": "Its people keep the fire lit, and the bell rung." }
35
+ ]
36
+ }
37
+ ],
38
+ "markers": [
39
+ { "at": 0, "name": "road", "kind": "section" },
40
+ { "at": 6, "name": "square", "kind": "section" },
41
+ { "at": 12, "name": "bell", "kind": "section" }
42
+ ]
43
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A village to stage scenes in: a field with houses round a bonfire, a well, a market, carts and hay, a dirt road in
3
+ * from the south and woods round the edge. Scenery only (`MapDoodad`): the rules never see it. The cinematics and the
4
+ * dialogue examples play here, each with its own name for the map (`world.map.name`), which their rules check to know
5
+ * the example is theirs
6
+ */
7
+ import { exactMath, type GameMap, type MapDoodad } from '@v12-engine/engine'
8
+ import { exampleField } from '../define'
9
+
10
+ /** The middle of the village: the bonfire */
11
+ export const SQUARE = { x: 24, z: 26 }
12
+ /** Where the road comes in, at the south edge */
13
+ export const ROAD_END = { x: 24, z: 4 }
14
+
15
+ /** A few houses, the well, the market and what lies about, round the square: [model, x, z, facing] */
16
+ const BUILDINGS: [string, number, number, number][] = [
17
+ ['village/bonfire.glb', SQUARE.x, SQUARE.z, 0],
18
+ ['village/fantasy_house.glb', 15, 33, 2.4],
19
+ ['village/fantasy_house_2.glb', 33, 34, -2.4],
20
+ ['village/fantasy_inn.glb', 14, 21, 1.6],
21
+ ['village/fantasy_house_3.glb', 34, 20, -1.6],
22
+ ['village/well.glb', 29.5, 28.5, 0],
23
+ ['village/market_stand.glb', 19.5, 27, 1.2],
24
+ ['village/cart.glb', 28.5, 22, 0.4],
25
+ ['village/hay.glb', 31, 25.5, 0],
26
+ ['village/barrel.glb', 20, 31, 0],
27
+ ['village/crate.glb', 21, 30.2, 0.5],
28
+ ['village/bench.glb', 25.5, 29.5, 0],
29
+ ['village/fence.glb', 21, 14, 0],
30
+ ['village/fence.glb', 27, 14, 0],
31
+ ['village/bell_tower.glb', 24, 40, Math.PI],
32
+ // the old stones north of the fire, in a ring
33
+ ...[0, 1, 2, 3, 4, 5].map((i): [string, number, number, number] => [
34
+ 'village/rocks.glb',
35
+ 24 + exactMath.cos(i * 1.047) * 2.6,
36
+ 33 + exactMath.sin(i * 1.047) * 2.6,
37
+ i * 1.3
38
+ ])
39
+ ]
40
+
41
+ /** The woods round the edge: trees in a ring, a little off from even, the same on every map made */
42
+ function woods(): MapDoodad[] {
43
+ const trees: MapDoodad[] = []
44
+ const models = ['trees/oak_1.glb', 'trees/oak_2.glb', 'trees/columnar_1.glb', 'trees/columnar_3.glb']
45
+ for (let i = 0; i < 44; i++) {
46
+ const angle = (i / 44) * Math.PI * 2
47
+ // a gap where the road comes in
48
+ if (Math.abs(angle - Math.PI * 1.5) < 0.22) continue
49
+ const away = 19 + ((i * 7) % 5)
50
+ const x = 24 + exactMath.cos(angle) * away
51
+ const z = 24 + exactMath.sin(angle) * away
52
+ const size = 0.9 + ((i * 13) % 7) / 14
53
+ trees.push({ id: 100 + i, model: models[i % models.length], x, z, yaw: i * 1.7, scale: [size, size, size] })
54
+ }
55
+ return trees
56
+ }
57
+
58
+ /** The village, called `name` (what the example's rules know it by), with `players` players */
59
+ export function villageMap(name: string, players = 2): GameMap {
60
+ const map = exampleField({ name, players })
61
+ const dirt = map.tileset.indexOf('dirt') + 1
62
+ const darkGrass = map.tileset.indexOf('darkGrass') + 1
63
+ // the road in from the south, and the trodden square round the bonfire
64
+ for (let z = 0; z < map.depth; z++)
65
+ for (let x = 0; x < map.width; x++) {
66
+ const square = exactMath.hypot(x + 0.5 - SQUARE.x, z + 0.5 - SQUARE.z)
67
+ const road = Math.abs(x + 0.5 - 24) < 1.6 && z < SQUARE.z
68
+ if (square < 5.5 || road) map.tiles[z * map.width + x] = dirt
69
+ else if (square > 17) map.tiles[z * map.width + x] = darkGrass
70
+ }
71
+ map.doodads = [...BUILDINGS.map(([model, x, z, yaw], i): MapDoodad => ({ id: i + 1, model, x, z, yaw, scale: [1, 1, 1] })), ...woods()]
72
+ // the road as a line the cinematics can run the camera along (`cut.rail('road')`)
73
+ map.paths = [
74
+ {
75
+ id: 1,
76
+ name: 'road',
77
+ width: 3,
78
+ points: [
79
+ { x: 24, z: 2 },
80
+ { x: 24, z: 14 },
81
+ { x: 20, z: 22 },
82
+ { x: 24, z: 31 },
83
+ { x: 30, z: 27 }
84
+ ]
85
+ }
86
+ ]
87
+ return map
88
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * How the shared units look (`./units.ts`): the shared art's adventurers with their clips and their gear in hand,
3
+ * drawn as their parts while the model loads. A category's own units' looks go in its `looks.ts`
4
+ */
5
+ import type { AttackLook, ModelDef, UnitLook } from '@v12-engine/client'
6
+
7
+ /** A character of the shared art, `scale` times its size, with what it holds in its hands */
8
+ export const character = (name: string, scale: number, own: Partial<ModelDef> = {}): ModelDef => ({ src: `characters/${name}.glb`, scale, ...own })
9
+ /** A piece of the adventurers' gear in a hand: the right one when not said */
10
+ export const hold = (name: string, hand: 'l' | 'r' = 'r') => ({ src: `gear/${name}.glb`, at: `handslot.${hand}` })
11
+
12
+ const SKIN: [number, number, number] = [0.87, 0.68, 0.53]
13
+
14
+ /** A person, as parts: a body in a colour and a head */
15
+ export const person = (height: number, body: [number, number, number] | 'team' = 'team'): UnitLook['shapes'] => [
16
+ { shape: 'cylinder', size: [0.5, height * 0.6, 0.5], offset: [0, height * 0.3, 0], color: body },
17
+ { shape: 'sphere', size: [0.4, 0.4, 0.4], offset: [0, height * 0.75, 0], color: SKIN }
18
+ ]
19
+
20
+ /** A one-handed weapon's swings, in turn at random, a big slice on a critical strike */
21
+ export const ONE_HANDED: AttackLook = {
22
+ clip: ['Melee_1H_Attack_Chop', 'Melee_1H_Attack_Slice_Diagonal', 'Melee_1H_Attack_Stab'],
23
+ order: 'random',
24
+ crit: 'Melee_2H_Attack_Slice'
25
+ }
26
+ /** A two-handed weapon's: a chop and a slice */
27
+ export const TWO_HANDED: AttackLook = { clip: ['Melee_2H_Attack_Chop', 'Melee_2H_Attack_Slice'], crit: 'Melee_2H_Attack_Chop' }
28
+
29
+ /** The shared units' looks, by id */
30
+ export const sharedLooks: Record<string, UnitLook> = {
31
+ footman: {
32
+ height: 1.8,
33
+ model: character('knight', 0.7, { hold: [hold('sword_1handed'), hold('shield_round', 'l')] }),
34
+ attacks: [ONE_HANDED],
35
+ shapes: person(1.8)
36
+ },
37
+ archer: {
38
+ height: 1.7,
39
+ model: character('ranger', 0.68, { hold: [hold('bow')], animations: { attack: 'Ranged_2H_Shoot' } }),
40
+ shot: { color: [0.85, 0.75, 0.55], size: 0.2 },
41
+ shapes: person(1.7)
42
+ },
43
+ mage: {
44
+ height: 1.8,
45
+ model: character('mage', 0.72, { hold: [hold('staff')] }),
46
+ shot: { color: [0.4, 0.6, 1], size: 0.3 },
47
+ shapes: person(1.8)
48
+ },
49
+ knight: {
50
+ height: 2.2,
51
+ model: character('knight', 0.9, { hold: [hold('sword_2handed')], animations: { attack: 'Melee_2H_Attack_Chop' } }),
52
+ attacks: [TWO_HANDED],
53
+ shapes: person(2.2)
54
+ },
55
+ dummy: {
56
+ height: 1.9,
57
+ // the shared art's training dummy, 1.86 high as it is; the parts below show until it has loaded
58
+ model: { src: 'props/dummy.glb', scale: 1 },
59
+ shapes: [
60
+ { shape: 'cylinder', size: [0.15, 1.9, 0.15], offset: [0, 0.95, 0], color: [0.45, 0.31, 0.18] },
61
+ { shape: 'cylinder', size: [0.7, 0.9, 0.7], offset: [0, 1.2, 0], color: [0.78, 0.68, 0.45] },
62
+ { shape: 'sphere', size: [0.45, 0.45, 0.45], offset: [0, 1.85, 0], color: [0.78, 0.68, 0.45] }
63
+ ]
64
+ }
65
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The units the examples share: a footman, an archer, a mage, a knight who is a hero, and a dummy to hit. Each is
3
+ * data (`UnitTypeDef`); how each looks is `./looks.ts`. A category with units of its own registers them in its
4
+ * `index.ts`, its ids starting with the category's (`fx.target`)
5
+ */
6
+ import { defineIds, type Game, type IdOf, type UnitTypeDef } from '@v12-engine/engine'
7
+
8
+ /** The shared units' ids: `UNIT.footman` everywhere else */
9
+ export const UNIT = defineIds('footman', 'archer', 'mage', 'knight', 'dummy')
10
+ export type SharedUnit = IdOf<typeof UNIT>
11
+
12
+ /** The two sides an example sets against each other: the player, and the one it fights */
13
+ export const PLAYER = 1
14
+ export const ENEMY = 2
15
+
16
+ const units: UnitTypeDef[] = [
17
+ {
18
+ id: UNIT.footman,
19
+ name: 'Footman',
20
+ kind: 'unit',
21
+ radius: 0.45,
22
+ stats: { health: 420, attackDamage: 14, attackSpeed: 1.1, attackRange: 0.5, armor: 12, moveSpeed: 4 },
23
+ sight: 10,
24
+ attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }]
25
+ },
26
+ {
27
+ id: UNIT.archer,
28
+ name: 'Archer',
29
+ kind: 'unit',
30
+ radius: 0.42,
31
+ stats: { health: 260, attackDamage: 18, attackSpeed: 1, attackRange: 7, moveSpeed: 4.2 },
32
+ sight: 11,
33
+ attacks: [{ damagePoint: 0.5, acquisitionRange: 8, projectileSpeed: 26 }]
34
+ },
35
+ {
36
+ id: UNIT.mage,
37
+ name: 'Mage',
38
+ kind: 'unit',
39
+ radius: 0.42,
40
+ stats: { health: 300, mana: 400, manaRegen: 4, attackDamage: 16, attackSpeed: 0.9, attackRange: 6, moveSpeed: 4 },
41
+ sight: 11,
42
+ attacks: [{ damagePoint: 0.5, acquisitionRange: 7, projectileSpeed: 18 }]
43
+ },
44
+ {
45
+ id: UNIT.knight,
46
+ name: 'Knight',
47
+ kind: 'unit',
48
+ radius: 0.5,
49
+ stats: { health: 900, healthRegen: 2, mana: 500, manaRegen: 5, attackDamage: 42, attackSpeed: 0.8, attackRange: 0.6, armor: 30, moveSpeed: 4.4 },
50
+ sight: 12,
51
+ attacks: [{ damagePoint: 0.4, acquisitionRange: 8 }],
52
+ hero: { learns: [], growth: { stats: { health: 80, mana: 30, attackDamage: 3, armor: 3 } }, maxLevel: 10 },
53
+ inventory: 6,
54
+ level: 1
55
+ },
56
+ {
57
+ // stands where it is put and takes what it is given: what an effect or an ability is tried on
58
+ id: UNIT.dummy,
59
+ name: 'Training dummy',
60
+ kind: 'unit',
61
+ radius: 0.5,
62
+ stats: { health: 5000, healthRegen: 50, moveSpeed: 0 },
63
+ sight: 4
64
+ }
65
+ ]
66
+
67
+ /** Registers the shared units on the game */
68
+ export function sharedUnits(game: Game<any>): void {
69
+ for (const unit of units) game.unit(unit)
70
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * The construction examples on the rules' side: workers who build, buildings that go up as they work (`construction:
3
+ * 'builder'`), a speed for all of it, sites paused, called off and repaired, a hall upgraded where it stands, a town
4
+ * laid out and built. How each building looks as it rises (grows, rises, fades in, is revealed, stands in scaffolding)
5
+ * is its look's `construction` (`./looks.ts`)
6
+ */
7
+ import {
8
+ cancelConstructionCommand,
9
+ defineCommand,
10
+ defineComponent,
11
+ defineTimer,
12
+ type Entity,
13
+ type Game,
14
+ number,
15
+ text,
16
+ underConstruction,
17
+ type UnitTypeDef,
18
+ upgradeBuildingCommand,
19
+ type Vec2,
20
+ type World
21
+ } from '@v12-engine/engine'
22
+ import { PLAYER } from '../common/units'
23
+ import { type ExampleCategory, type ExampleDef, exampleField } from '../define'
24
+
25
+ /** The construction examples' own unit types */
26
+ export const C = {
27
+ worker: 'constructionWorker',
28
+ hall: 'constructionHall',
29
+ keep: 'constructionKeep',
30
+ castle: 'constructionCastle',
31
+ barracks: 'constructionBarracks',
32
+ tower: 'constructionTower',
33
+ house: 'constructionHouse',
34
+ temple: 'constructionTemple',
35
+ farm: 'constructionFarm',
36
+ mill: 'constructionMill',
37
+ fortress: 'constructionFortress'
38
+ } as const
39
+
40
+ /** How fast everything goes up, 1 as its `buildTime` says: what the Build speed slider sets */
41
+ const speed = defineComponent<{ factor: number }>({ id: 'construction.speed', default: () => ({ factor: 1 }) })
42
+
43
+ /** How fast a site goes up with so many builders: the first counts whole, every other one half, times the example's speed */
44
+ const rate = (builders: number, world: World) => (builders > 0 ? 1 + (builders - 1) * 0.5 : 0) * world.state(speed).factor
45
+
46
+ const building = (id: string, name: string, size: number, buildTime: number, health: number, own: Partial<UnitTypeDef> = {}): UnitTypeDef => ({
47
+ id,
48
+ name,
49
+ kind: 'building',
50
+ radius: size / 2,
51
+ footprint: { w: size, d: size },
52
+ sight: 6,
53
+ stats: { health, armor: 3 },
54
+ // a worker stays and works on it; more workers, faster
55
+ construction: 'builder',
56
+ buildTime,
57
+ buildRate: rate,
58
+ ...own
59
+ })
60
+
61
+ const types: UnitTypeDef[] = [
62
+ {
63
+ id: C.worker,
64
+ name: 'Worker',
65
+ kind: 'unit',
66
+ radius: 0.4,
67
+ sight: 8,
68
+ stats: { health: 220, moveSpeed: 4.5 },
69
+ // what it puts down: every building here. A unit that builds can repair too
70
+ builds: [C.hall, C.barracks, C.tower, C.house, C.temple, C.farm, C.mill, C.fortress]
71
+ },
72
+ building(C.hall, 'Town hall', 4, 30, 2000, { upgradesTo: C.keep }),
73
+ building(C.keep, 'Keep', 4, 10, 2600, { upgradesTo: C.castle }),
74
+ building(C.castle, 'Castle', 4, 12, 3400),
75
+ building(C.barracks, 'Barracks', 3, 18, 1200),
76
+ building(C.tower, 'Watch tower', 2, 14, 700),
77
+ building(C.house, 'House', 2, 10, 500),
78
+ building(C.temple, 'Temple', 3, 24, 1400),
79
+ building(C.farm, 'Farm', 3, 12, 600),
80
+ building(C.mill, 'Windmill', 3, 16, 800),
81
+ building(C.fortress, 'Fortress', 4, 28, 2400)
82
+ ]
83
+
84
+ // ---- helpers the examples share
85
+
86
+ /** A worker of the player's, put down beside a point */
87
+ const worker = (world: World, at: Vec2): Entity => world.spawn(C.worker, { at, owner: PLAYER })
88
+
89
+ /** A site of the player's, put down at a point: at `progress` of the way already, nobody working on it yet */
90
+ function site(world: World, type: string, at: Vec2, progress = 0): Entity {
91
+ const made = world.spawn(type, { at, owner: PLAYER, underConstruction: true })
92
+ if (progress > 0) setProgress(world, made, progress)
93
+ return made
94
+ }
95
+
96
+ /** Puts a site at a share of the way, and its health with it: what it would be had it been built so far */
97
+ function setProgress(world: World, entity: Entity, progress: number): void {
98
+ const state = underConstruction(entity)
99
+ if (!state) return
100
+ state.progress = Math.min(0.999, Math.max(0, progress))
101
+ const start = world.settings.production.constructionHealth
102
+ entity.health = Math.max(1, entity.maxHealth * (start + (1 - start) * state.progress))
103
+ }
104
+
105
+ /** The sites still going up, left to right */
106
+ const sites = (world: World) =>
107
+ world
108
+ .query({ kinds: ['building'], owner: PLAYER })
109
+ .filter((each) => underConstruction(each))
110
+ .sort((a, b) => a.pos.x - b.pos.x)
111
+
112
+ /** Takes away everything an example put down */
113
+ function clear(world: World): void {
114
+ for (const each of [...world.entities.values()]) if (each.alive) world.kill(each, undefined, false, 'removed')
115
+ }
116
+
117
+ // ---- the examples' layouts
118
+
119
+ /** Five sites in a row, each going up its own way, a worker at each */
120
+ function row(world: World): void {
121
+ const kinds = [C.barracks, C.tower, C.house, C.temple, C.fortress]
122
+ kinds.forEach((type, i) => {
123
+ const x = 8 + i * 8
124
+ const made = site(world, type, { x, z: 24 })
125
+ worker(world, { x: x - 1, z: 20.5 }).order('construct', made)
126
+ })
127
+ }
128
+
129
+ /** A hall to be, and six workers waiting by it */
130
+ function yard(world: World): void {
131
+ site(world, C.fortress, { x: 24, z: 26 })
132
+ for (let i = 0; i < 6; i++) worker(world, { x: 19 + i * 2, z: 17 })
133
+ }
134
+
135
+ /** The same building six times, each further along: 0, a fifth, two fifths… and one finished */
136
+ function stages(world: World): void {
137
+ for (let i = 0; i < 6; i++) {
138
+ const at = { x: 6 + i * 7.2, z: 24 }
139
+ if (i === 5) world.spawn(C.fortress, { at, owner: PLAYER })
140
+ else site(world, C.fortress, at, i * 0.2)
141
+ }
142
+ }
143
+
144
+ /** A barracks that is burning, and three workers to mend it */
145
+ function burning(world: World): void {
146
+ world.spawn(C.barracks, { at: { x: 24.5, z: 26.5 }, owner: PLAYER })
147
+ for (let i = 0; i < 3; i++) worker(world, { x: 21 + i * 3, z: 19 })
148
+ // set alight a moment after it stands: the screen has drawn it by then, to put the fire on
149
+ world.timers.after(1.5, alight)
150
+ }
151
+
152
+ /** Every building of the player's hurt to an eighth of its health, with a blast on it */
153
+ const alight = defineTimer({ id: 'construction.alight', run: (world) => hurtTo(world, 0.12) })
154
+
155
+ function hurtTo(world: World, share: number): void {
156
+ for (const each of world.query({ kinds: ['building'], owner: PLAYER })) {
157
+ each.health = Math.max(1, Math.min(each.health, each.maxHealth * share))
158
+ world.fx('explosion', { at: each.pos, params: { size: 0.6, scorch: false } })
159
+ }
160
+ }
161
+
162
+ /** A town hall, standing, to upgrade into a keep and then a castle */
163
+ function hall(world: World): void {
164
+ world.spawn(C.hall, { at: { x: 24, z: 26 }, owner: PLAYER })
165
+ for (let i = 0; i < 2; i++) worker(world, { x: 22 + i * 3, z: 20 })
166
+ }
167
+
168
+ /** Where the town's buildings go, in the order they are built: a hall in the middle, the rest round it */
169
+ const TOWN: [string, number, number][] = [
170
+ [C.hall, 24, 24],
171
+ [C.house, 18, 19],
172
+ [C.house, 30, 19],
173
+ [C.farm, 15.5, 27.5],
174
+ [C.farm, 32.5, 27.5],
175
+ [C.barracks, 18.5, 32.5],
176
+ [C.house, 24, 33],
177
+ [C.temple, 29.5, 33.5],
178
+ [C.tower, 13, 18],
179
+ [C.tower, 35, 18],
180
+ [C.mill, 10.5, 32.5],
181
+ [C.house, 36, 24],
182
+ [C.house, 12, 24],
183
+ [C.tower, 13, 38],
184
+ [C.tower, 35, 38],
185
+ [C.fortress, 37, 33]
186
+ ]
187
+
188
+ /** Workers in the middle of an empty field, a town to be laid out round them */
189
+ function town(world: World): void {
190
+ for (let i = 0; i < 10; i++) worker(world, { x: 20 + (i % 5) * 2, z: 12 + Math.floor(i / 5) * 1.6 })
191
+ }
192
+
193
+ /** Each example's layout, by its id */
194
+ const LAYOUTS: Record<string, (world: World) => void> = {
195
+ 'construction-motions': row,
196
+ 'construction-workers': yard,
197
+ 'construction-stages': stages,
198
+ 'construction-repair': burning,
199
+ 'construction-upgrade': hall,
200
+ 'construction-town': town
201
+ }
202
+
203
+ // ---- commands, sent by the examples' buttons
204
+
205
+ /** How fast everything goes up, from a quarter to eight times */
206
+ export const setSpeed = defineCommand('construction.speed', { factor: number }, (world, _player, { factor }) => {
207
+ world.setState(speed, { factor: Math.min(8, Math.max(0.1, factor)) })
208
+ })
209
+
210
+ /** Workers sent to the first site still going up: `count` more of those idle, or all (0) */
211
+ export const sendWorkers = defineCommand('construction.send', { count: number }, (world, _player, { count }) => {
212
+ const target = sites(world)[0]
213
+ if (!target) return
214
+ const idle = world.query({ type: C.worker, owner: PLAYER, sort: 'nearest', near: target }).filter((each) => each.currentOrder?.type !== 'construct')
215
+ for (const each of count > 0 ? idle.slice(0, count) : idle) each.order('construct', target)
216
+ })
217
+
218
+ /** Every worker stops: the sites stand where they got to */
219
+ export const stopWork = defineCommand('construction.stop', {}, (world) => {
220
+ for (const each of world.query({ type: C.worker, owner: PLAYER })) each.order('stop')
221
+ })
222
+
223
+ /** The first site called off, as its Cancel button would (the engine's `cancelConstruction`): it folds into the ground */
224
+ export const callOff = defineCommand('construction.cancel', {}, (world, player) => {
225
+ const target = sites(world)[0]
226
+ if (target) cancelConstructionCommand.execute(world, player, { building: target.id })
227
+ })
228
+
229
+ /** Every site put at a share of the way (the Progress slider) */
230
+ export const progressAll = defineCommand('construction.progress', { share: number }, (world, _player, { share }) => {
231
+ for (const each of sites(world)) setProgress(world, each, share)
232
+ })
233
+
234
+ /** The workers told to mend every hurt building of the player's */
235
+ export const repairAll = defineCommand('construction.repair', {}, (world) => {
236
+ const hurt = world.query({ kinds: ['building'], owner: PLAYER }).filter((each) => !underConstruction(each) && each.health < each.maxHealth)
237
+ if (!hurt.length) return
238
+ for (const each of world.query({ type: C.worker, owner: PLAYER })) each.order('repair', hurt[0])
239
+ })
240
+
241
+ /** Every building of the player's hurt down to a share of its health */
242
+ export const hurtAll = defineCommand('construction.hurt', { share: number }, (world, _player, { share }) => hurtTo(world, share))
243
+
244
+ /** The hall upgraded into a keep, the keep into a castle, as its card's button would (the engine's `upgradeBuilding`) */
245
+ export const upgrade = defineCommand('construction.upgrade', {}, (world, player) => {
246
+ const standing = world.query({ type: [C.hall, C.keep], owner: PLAYER, limit: 1 })[0]
247
+ if (standing) upgradeBuildingCommand.execute(world, player, { building: standing.id, into: undefined })
248
+ })
249
+
250
+ /** The town laid out: each worker given its share of the plan, one after another (queued `build` orders) */
251
+ export const buildTown = defineCommand('construction.town', {}, (world) => {
252
+ const workers = world.query({ type: C.worker, owner: PLAYER })
253
+ if (!workers.length) return
254
+ // each its share of the plan, in turn: the first the hall, the others a house, a farm… and on to the next
255
+ TOWN.forEach(([type, x, z], i) => {
256
+ const builder = workers[i % workers.length]
257
+ builder.order('build', { unitType: type, point: { x, z } }, { queue: true })
258
+ })
259
+ })
260
+
261
+ /** Takes everything away and lays the example out again */
262
+ export const relay = defineCommand('construction.relay', { example: text }, (world, _player, { example }) => {
263
+ const layout = LAYOUTS[example]
264
+ if (!layout) return
265
+ clear(world)
266
+ layout(world)
267
+ })
268
+
269
+ const example = (id: string, title: string, summary: string, tags: string[]): ExampleDef => ({
270
+ id,
271
+ category: 'construction',
272
+ title,
273
+ summary,
274
+ tags,
275
+ map: () => exampleField({ name: title }),
276
+ start: (world) => {
277
+ world.setState(speed, { factor: 1 })
278
+ LAYOUTS[id]?.(world)
279
+ },
280
+ source: ['examples/construction/index.ts', 'examples/construction/looks.ts']
281
+ })
282
+
283
+ export const category: ExampleCategory = {
284
+ rules: (game: Game<any>) => {
285
+ for (const type of types) game.unit(type)
286
+ game.component(speed)
287
+ game.timer(alight)
288
+ for (const command of [setSpeed, sendWorkers, stopWork, callOff, progressAll, repairAll, hurtAll, upgrade, buildTown, relay])
289
+ game.command(command)
290
+ },
291
+ examples: [
292
+ example(
293
+ 'construction-motions',
294
+ 'Five ways to go up',
295
+ 'Five buildings going up side by side, a worker hammering at each: one grows out of the ground, one rises, one fades in, one is revealed by a glowing cut, and one stands in scaffolding until it is done.',
296
+ ['construction', 'build', 'grow', 'rise', 'fade', 'reveal', 'scaffold', 'stages', 'speed']
297
+ ),
298
+ example(
299
+ 'construction-workers',
300
+ 'Workers at a site',
301
+ 'A fortress to be built and six workers. Send them one at a time and watch it go faster with each; stop them and it waits where it got to; cancel it and it folds back into the ground.',
302
+ ['construction', 'workers', 'builders', 'pause', 'cancel', 'refund']
303
+ ),
304
+ example(
305
+ 'construction-stages',
306
+ 'Every stage at once',
307
+ 'The same fortress six times, each further along: dug earth, low scaffolding, high scaffolding, the building rising inside it, and finished. Drag Progress to move them all.',
308
+ ['construction', 'stages', 'scaffold', 'progress']
309
+ ),
310
+ example(
311
+ 'construction-repair',
312
+ 'Repair a burning barracks',
313
+ 'A barracks is burning down. Send the workers to repair it: its health comes back as they hammer, the fire goes out under a quarter and the smoke over a half.',
314
+ ['repair', 'damaged', 'fire', 'smoke', 'workers']
315
+ ),
316
+ example(
317
+ 'construction-upgrade',
318
+ 'Hall, keep, castle',
319
+ 'A town hall upgraded where it stands: scaffolding goes up round it, and it comes out a keep; upgrade again for a castle. It keeps working all the while.',
320
+ ['upgrade', 'morph', 'hall', 'keep', 'castle', 'scaffold']
321
+ ),
322
+ example(
323
+ 'construction-town',
324
+ 'A whole town',
325
+ 'Ten workers lay out a town: a hall first, then houses, farms, a barracks, a temple, towers and a mill, each worker going from one to the next. Speed it up to watch it all rise.',
326
+ ['town', 'build order', 'workers', 'queue', 'city', 'construction']
327
+ )
328
+ ]
329
+ }