@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,714 @@
1
+ /**
2
+ * The showcases on the rules' side (`ExampleCategory`): larger scenes, each made of several parts of the engine at once.
3
+ * A boss fight in phases (a system that watches its health, a buff, a slam, the dead raised), a tower defense (waves on
4
+ * a timer, a road, lives and gold kept on the keep), two computer generals against each other (`defineBrain`), and a
5
+ * battle that turns back time (the world's state recorded, then laid out again as it was)
6
+ */
7
+ import {
8
+ applyBuff,
9
+ bool,
10
+ type BuffDef,
11
+ defineCommand,
12
+ defineComponent,
13
+ defineIds,
14
+ type Entity,
15
+ entityDied,
16
+ type Game,
17
+ number,
18
+ oneOf,
19
+ type PlayerId,
20
+ type UnitTypeDef,
21
+ type Vec2,
22
+ type World,
23
+ exactMath
24
+ } from '@v12-engine/engine'
25
+ import { type AiModule, defineBrain } from '@v12-engine/engine/ai'
26
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
27
+ import { type ExampleCategory, exampleField, tiles } from '../define'
28
+
29
+ /** The showcases' own units */
30
+ export const SHOW = defineIds('showcaseBoss', 'showcaseRisen', 'showcaseTower', 'showcaseRaider', 'showcaseBrute', 'showcaseKeep')
31
+
32
+ const units: UnitTypeDef[] = [
33
+ {
34
+ id: SHOW.showcaseBoss,
35
+ name: 'The Bone King',
36
+ kind: 'unit',
37
+ radius: 1.1,
38
+ stats: { health: 6000, armor: 15, attackDamage: 55, attackSpeed: 0.75, attackRange: 1.4, moveSpeed: 3 },
39
+ sight: 12,
40
+ attacks: [{ damagePoint: 0.5, acquisitionRange: 12 }]
41
+ },
42
+ {
43
+ id: SHOW.showcaseRisen,
44
+ name: 'Risen dead',
45
+ kind: 'unit',
46
+ radius: 0.4,
47
+ stats: { health: 160, attackDamage: 9, attackSpeed: 1.1, attackRange: 0.5, moveSpeed: 3.6 },
48
+ sight: 10,
49
+ attacks: [{ damagePoint: 0.4, acquisitionRange: 10 }]
50
+ },
51
+ {
52
+ id: SHOW.showcaseTower,
53
+ name: 'Arrow tower',
54
+ kind: 'unit',
55
+ radius: 0.9,
56
+ stats: { health: 1500, attackDamage: 24, attackSpeed: 1.3, attackRange: 7.5, moveSpeed: 0, armor: 20 },
57
+ sight: 9,
58
+ attacks: [{ damagePoint: 0.2, acquisitionRange: 7.5, projectileSpeed: 26 }]
59
+ },
60
+ // the raiders walk the road and fight nothing: they only want to get through
61
+ { id: SHOW.showcaseRaider, name: 'Raider', kind: 'unit', radius: 0.4, stats: { health: 110, moveSpeed: 2.8 }, sight: 4 },
62
+ { id: SHOW.showcaseBrute, name: 'Brute', kind: 'unit', radius: 0.55, stats: { health: 520, armor: 20, moveSpeed: 1.9 }, sight: 4 },
63
+ { id: SHOW.showcaseKeep, name: 'Keep', kind: 'unit', radius: 2, stats: { health: 5000, moveSpeed: 0 }, sight: 8 }
64
+ ]
65
+
66
+ /** Every unit of the world taken away */
67
+ const clearAll = (world: World) => {
68
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
69
+ }
70
+ /** The alive units of the world a test says yes to */
71
+ const alive = (world: World, test: (unit: Entity) => boolean) => [...world.units.values()].filter((unit) => unit.alive && test(unit))
72
+ /** One order to many, the way a player gives it */
73
+ const orderAll = (world: World, player: PlayerId, units: readonly Entity[], order: { type: string; point?: Vec2; target?: number }) => {
74
+ if (units.length) world.issue({ type: 'order', player, entities: units.map((unit) => unit.id), order: { ...order, formation: 'none' } as never })
75
+ }
76
+
77
+ // ---- the boss: phases by its health
78
+
79
+ /** Where the boss fight stands, kept on the boss and told to every screen: its phase, and when it slams next */
80
+ export const bossState = defineComponent<{ phase: 1 | 2 | 3; slamAt: number }>({
81
+ id: 'showcase.bossFight',
82
+ default: () => ({ phase: 1, slamAt: 0 }),
83
+ replicate: 'everyone'
84
+ })
85
+
86
+ /** The fury of the boss's last phase: faster blows, faster feet */
87
+ const enraged: BuffDef = { id: 'showcase.enraged', name: 'Enraged', percent: { attackSpeed: 0.7, moveSpeed: 0.35, attackDamage: 0.25 } }
88
+
89
+ /** The party: a knight, footmen, archers and a mage, south of the boss */
90
+ function party(world: World): void {
91
+ world.spawn(UNIT.knight, { at: { x: 24, z: 15 }, owner: PLAYER, facing: 0 })
92
+ for (let i = 0; i < 5; i++) world.spawn(UNIT.footman, { at: { x: 20 + i * 2, z: 16.5 }, owner: PLAYER, facing: 0 })
93
+ for (let i = 0; i < 4; i++) world.spawn(UNIT.archer, { at: { x: 20.5 + i * 2.3, z: 12.5 }, owner: PLAYER, facing: 0 })
94
+ for (let i = 0; i < 2; i++) world.spawn(UNIT.mage, { at: { x: 22 + i * 4, z: 11 }, owner: PLAYER, facing: 0 })
95
+ }
96
+
97
+ /** The boss fight laid out: the party to the south, the boss on its dais to the north */
98
+ function bossArena(world: World): void {
99
+ party(world)
100
+ const boss = world.spawn(SHOW.showcaseBoss, { at: { x: 24, z: 31 }, owner: ENEMY, facing: Math.PI })
101
+ boss.set(bossState, { phase: 1, slamAt: 0 })
102
+ }
103
+
104
+ /** Raises the dead round the boss: `count` of them out of the ground, in a ring */
105
+ function raiseDead(world: World, at: Vec2, count: number): void {
106
+ for (let i = 0; i < count; i++) {
107
+ const angle = (i / count) * Math.PI * 2
108
+ const point = { x: at.x + exactMath.cos(angle) * 3, z: at.z + exactMath.sin(angle) * 3 }
109
+ world.spawn(SHOW.showcaseRisen, { at: point, owner: ENEMY, facing: angle })
110
+ world.fx('dust', { at: point })
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The boss's phases, watched a few times a second: at two thirds of its health it roars and raises the dead; at a
116
+ * third it is enraged (`enraged`), raises more, and slams the ground every few seconds, hurting all round it
117
+ */
118
+ const bossPhases = {
119
+ name: 'showcase.bossPhases',
120
+ every: 3,
121
+ update: (world: World) => {
122
+ for (const boss of world.units.values()) {
123
+ if (!boss.alive || boss.def.id !== SHOW.showcaseBoss) continue
124
+ const fight = boss.get(bossState) ?? { phase: 1, slamAt: 0 }
125
+ const share = boss.health / boss.maxHealth
126
+ if (fight.phase === 1 && share < 2 / 3) {
127
+ boss.set(bossState, { ...fight, phase: 2 })
128
+ world.fx('shockwave', { at: boss.pos, params: { size: 2 } })
129
+ raiseDead(world, boss.pos, 6)
130
+ world.hint(PLAYER, 'The Bone King calls up his dead!')
131
+ } else if (fight.phase === 2 && share < 1 / 3) {
132
+ boss.set(bossState, { phase: 3, slamAt: world.time + 2 })
133
+ world.fx('explosion', { at: boss.pos, params: { size: 1.4 } })
134
+ applyBuff(world, boss, enraged.id, 600, boss)
135
+ raiseDead(world, boss.pos, 5)
136
+ world.hint(PLAYER, 'The Bone King is enraged!')
137
+ } else if (fight.phase === 3 && world.time >= fight.slamAt) {
138
+ boss.set(bossState, { ...fight, slamAt: world.time + 5 })
139
+ // the slam: all of the other side within 4 of it hurt, more the nearer they stand
140
+ world.fx('ground-crack', { at: boss.pos, params: { size: 1.6 } })
141
+ world.fx('shockwave', { at: boss.pos, params: { size: 1.5 } })
142
+ for (const unit of world.units.values()) {
143
+ if (!unit.alive || unit.owner !== PLAYER) continue
144
+ const away = exactMath.hypot(unit.pos.x - boss.pos.x, unit.pos.z - boss.pos.z)
145
+ if (away < 4) world.damage(unit, 90 * (1 - away / 5), boss)
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ /** The fight again from the start */
152
+ export const bossReset = defineCommand('showcase.bossReset', {}, (world) => {
153
+ clearAll(world)
154
+ bossArena(world)
155
+ })
156
+ /** The party sent at the boss, fighting what is in the way */
157
+ export const bossCharge = defineCommand('showcase.bossCharge', {}, (world) => {
158
+ orderAll(
159
+ world,
160
+ PLAYER,
161
+ alive(world, (unit) => unit.owner === PLAYER),
162
+ { type: 'attackMove', point: { x: 24, z: 33 } }
163
+ )
164
+ })
165
+
166
+ // ---- the tower defense: a road, waves, towers, lives and gold
167
+
168
+ /** The road the raiders walk, from the west to the keep in the east */
169
+ export const ROAD: Vec2[] = [
170
+ { x: 1, z: 38 },
171
+ { x: 14, z: 38 },
172
+ { x: 14, z: 22 },
173
+ { x: 32, z: 22 },
174
+ { x: 32, z: 10 },
175
+ { x: 42, z: 10 }
176
+ ]
177
+ /** Where towers may stand, along the road, in the order they are built */
178
+ const SPOTS: Vec2[] = [
179
+ { x: 17.5, z: 30 },
180
+ { x: 10.5, z: 30 },
181
+ { x: 23, z: 25.5 },
182
+ { x: 23, z: 18.5 },
183
+ { x: 28.5, z: 15 },
184
+ { x: 35.5, z: 15 },
185
+ { x: 8, z: 41.5 },
186
+ { x: 17.5, z: 41.5 },
187
+ { x: 35.5, z: 6.5 },
188
+ { x: 28.5, z: 6.5 }
189
+ ]
190
+ /** A time that never comes: a wave not yet beaten has its next one here (the state is sent as plain data, which has no Infinity) */
191
+ const NEVER = 1e9
192
+ /** What a tower costs, and what a raider and a brute are worth */
193
+ export const TOWER_COST = 60
194
+ const BOUNTY = { [SHOW.showcaseRaider]: 6, [SHOW.showcaseBrute]: 20 } as Record<string, number>
195
+
196
+ /** How the defense stands, kept on the keep and told to every screen */
197
+ interface Defense {
198
+ wave: number
199
+ lives: number
200
+ gold: number
201
+ /** Raiders still to come out this wave, and when the next does */
202
+ toCome: number
203
+ nextAt: number
204
+ /** When the next wave starts, once this one is beaten */
205
+ waveAt: number
206
+ }
207
+ export const defenseState = defineComponent<Defense>({
208
+ id: 'showcase.defense',
209
+ default: () => ({ wave: 0, lives: 20, gold: 120, toCome: 0, nextAt: 0, waveAt: 3 }),
210
+ replicate: 'everyone'
211
+ })
212
+
213
+ /** The keep, where the defense is kept */
214
+ const keepOf = (world: World) => alive(world, (unit) => unit.def.id === SHOW.showcaseKeep)[0]
215
+
216
+ /** The map of the tower defense: the field with the road painted on it */
217
+ function defenseMap() {
218
+ const map = exampleField({ name: 'Hold the road', players: 2 })
219
+ const road = tiles.findIndex((tile) => tile.id === 'road') + 1 || tiles.findIndex((tile) => tile.id === 'dirt') + 1
220
+ for (let i = 0; i + 1 < ROAD.length; i++) {
221
+ const a = ROAD[i]!
222
+ const b = ROAD[i + 1]!
223
+ for (let x = Math.min(a.x, b.x) - 1; x <= Math.max(a.x, b.x) + 1; x++)
224
+ for (let z = Math.min(a.z, b.z) - 1; z <= Math.max(a.z, b.z) + 1; z++)
225
+ if (x >= 0 && z >= 0 && x < map.width && z < map.depth) map.tiles[Math.floor(z) * map.width + Math.floor(x)] = road
226
+ }
227
+ return map
228
+ }
229
+
230
+ /** How far along the road a point is, as the leg it is on (0 the first), to send it on to the next corner */
231
+ function legOf(point: Vec2): number {
232
+ let best = 0
233
+ let nearest = Infinity
234
+ for (let i = 0; i + 1 < ROAD.length; i++) {
235
+ const a = ROAD[i]!
236
+ const b = ROAD[i + 1]!
237
+ const dx = b.x - a.x
238
+ const dz = b.z - a.z
239
+ const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.z - a.z) * dz) / (dx * dx + dz * dz)))
240
+ const away = exactMath.hypot(a.x + dx * t - point.x, a.z + dz * t - point.z)
241
+ if (away < nearest - 0.01) {
242
+ nearest = away
243
+ best = i
244
+ }
245
+ }
246
+ return best
247
+ }
248
+
249
+ /** The defense laid out: the keep at the road's end, two towers to start with */
250
+ function defenseStart(world: World): void {
251
+ const keep = world.spawn(SHOW.showcaseKeep, { at: { x: 44.5, z: 10 }, owner: PLAYER, facing: -Math.PI / 2 })
252
+ keep.set(defenseState, { wave: 0, lives: 20, gold: 120, toCome: 0, nextAt: 0, waveAt: world.time + 4 })
253
+ for (const spot of SPOTS.slice(0, 2)) world.spawn(SHOW.showcaseTower, { at: spot, owner: PLAYER })
254
+ }
255
+
256
+ /**
257
+ * The waves, ten times a second: a raider out every so often while the wave has some to come; the next wave a few
258
+ * seconds after the last is beaten, bigger, every third with brutes. Each raider is sent on to the next corner of the
259
+ * road; one that gets to the keep is taken away, and a life with it
260
+ */
261
+ const waves = {
262
+ name: 'showcase.waves',
263
+ every: 3,
264
+ update: (world: World) => {
265
+ const keep = keepOf(world)
266
+ const defense = keep?.get(defenseState)
267
+ if (!keep || !defense) return
268
+ const raiders = alive(world, (unit) => unit.owner === ENEMY && (unit.def.id === SHOW.showcaseRaider || unit.def.id === SHOW.showcaseBrute))
269
+ const now = { ...defense }
270
+ if (now.lives > 0 && now.toCome > 0 && world.time >= now.nextAt) {
271
+ const brute = now.wave % 3 === 0 && now.toCome % 3 === 0
272
+ world.spawn(brute ? SHOW.showcaseBrute : SHOW.showcaseRaider, { at: ROAD[0]!, owner: ENEMY, facing: Math.PI / 2 })
273
+ now.toCome--
274
+ now.nextAt = world.time + (brute ? 1.6 : 0.8)
275
+ } else if (now.lives > 0 && !now.toCome && !raiders.length && world.time >= now.waveAt) {
276
+ now.wave++
277
+ now.toCome = 6 + now.wave * 3
278
+ now.nextAt = world.time
279
+ now.waveAt = NEVER
280
+ world.hint(PLAYER, `Wave ${now.wave}`)
281
+ } else if (!now.toCome && !raiders.length && now.waveAt === NEVER) now.waveAt = world.time + 5
282
+ const end = ROAD[ROAD.length - 1]!
283
+ for (const raider of raiders) {
284
+ if (exactMath.hypot(raider.pos.x - end.x, raider.pos.z - end.z) < 1.5) {
285
+ world.kill(raider, undefined, false, 'removed')
286
+ world.fx('impact-ring', { at: end })
287
+ now.lives = Math.max(0, now.lives - (raider.def.id === SHOW.showcaseBrute ? 3 : 1))
288
+ if (!now.lives) world.hint(PLAYER, 'The keep has fallen. Start again to hold the road anew.')
289
+ continue
290
+ }
291
+ const next = ROAD[legOf(raider.pos) + 1]!
292
+ const going = raider.orders[0]?.init.point as Vec2 | undefined
293
+ if (!going || going.x !== next.x || going.z !== next.z) raider.order('move', next)
294
+ }
295
+ keep.set(defenseState, now)
296
+ }
297
+ }
298
+
299
+ /** A tower built at the next free spot, if the gold is there */
300
+ export const buildTower = defineCommand('showcase.buildTower', {}, (world, player) => {
301
+ const keep = keepOf(world)
302
+ const defense = keep?.get(defenseState)
303
+ if (!keep || !defense) return
304
+ if (defense.gold < TOWER_COST) return world.message(player, 'Not enough gold')
305
+ const towers = alive(world, (unit) => unit.def.id === SHOW.showcaseTower)
306
+ const spot = SPOTS.find((each) => !towers.some((tower) => exactMath.hypot(tower.pos.x - each.x, tower.pos.z - each.z) < 1))
307
+ if (!spot) return world.message(player, 'Every spot has a tower')
308
+ world.spawn(SHOW.showcaseTower, { at: spot, owner: player })
309
+ world.fx('dust', { at: spot })
310
+ keep.set(defenseState, { ...defense, gold: defense.gold - TOWER_COST })
311
+ })
312
+ /** The road held again from the first wave */
313
+ export const defenseReset = defineCommand('showcase.defenseReset', {}, (world) => {
314
+ clearAll(world)
315
+ defenseStart(world)
316
+ })
317
+ /** The next wave now, for gold */
318
+ export const callWave = defineCommand('showcase.callWave', {}, (world) => {
319
+ const keep = keepOf(world)
320
+ const defense = keep?.get(defenseState)
321
+ if (keep && defense && defense.waveAt !== NEVER) keep.set(defenseState, { ...defense, waveAt: world.time, gold: defense.gold + 15 })
322
+ })
323
+
324
+ // ---- two generals: a computer on each side, with tactics of its own
325
+
326
+ /** The first of a set a test likes best, by a score: the lowest wins */
327
+ const best = <T>(list: readonly T[], score: (each: T) => number): T | undefined => {
328
+ let found: T | undefined
329
+ let low = Infinity
330
+ for (const each of list) {
331
+ const s = score(each)
332
+ if (s < low) {
333
+ low = s
334
+ found = each
335
+ }
336
+ }
337
+ return found
338
+ }
339
+
340
+ /**
341
+ * A general's tactics, a module of a computer's brain (`AiModule`): it reads the game as its seat sees it (`ai.view`)
342
+ * and orders its army as a player would (`ai.send`). Footmen hit the weakest foe near the front together, and fall
343
+ * back to the archers when badly hurt; archers keep out of a blade's reach and shoot the weakest in range; the knight
344
+ * goes round the side for the archers. An order is given again only when it changes, so swings are not cut off
345
+ */
346
+ const tactics = (): AiModule => {
347
+ const given = new Map<number, string>()
348
+ return {
349
+ id: 'showcase.tactics',
350
+ think(ai) {
351
+ const mine = ai.view.mine.units().filter((each) => each.health > 0)
352
+ const foes = ai.view.enemies.units().filter((each) => each.health > 0 && each.visible)
353
+ const order = (id: number, key: string, command: { type: string; point?: Vec2; target?: number }) => {
354
+ if (given.get(id) === key) return
355
+ given.set(id, key)
356
+ ai.send({ type: 'order', entities: [id], order: command } as never)
357
+ }
358
+ /** The foe a unit was set on last, while it lives and is still within `reach`: kept, so a swing is never cut off for another */
359
+ const still = (unit: { id: number; x: number; z: number }, reach: number) => {
360
+ const was = Number(given.get(unit.id)?.replace(/^\D+/, ''))
361
+ const foe = foes.find((each) => each.id === was)
362
+ return foe && exactMath.hypot(foe.x - unit.x, foe.z - unit.z) < reach ? foe : undefined
363
+ }
364
+ const front = ai.view.enemies.centre() ?? { x: 32, z: 32 }
365
+ const home = ai.view.mine.centre() ?? front
366
+ const archers = mine.filter((each) => each.type === UNIT.archer)
367
+ const rear = archers.length
368
+ ? { x: archers.reduce((s, a) => s + a.x, 0) / archers.length, z: archers.reduce((s, a) => s + a.z, 0) / archers.length }
369
+ : home
370
+ for (const unit of mine) {
371
+ const hurt = unit.health / (unit.maxHealth ?? unit.health) < 0.3
372
+ const near = best(foes, (foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z))
373
+ const nearAway = near ? exactMath.hypot(near.x - unit.x, near.z - unit.z) : Infinity
374
+ if (unit.type === UNIT.archer) {
375
+ // a blade too near: step back, away from it
376
+ if (near && nearAway < 3 && near.type !== UNIT.archer) {
377
+ const dx = unit.x - near.x
378
+ const dz = unit.z - near.z
379
+ const k = 4 / (exactMath.hypot(dx, dz) || 1)
380
+ order(unit.id, `back${Math.round(unit.x)},${Math.round(unit.z)}`, { type: 'move', point: { x: unit.x + dx * k, z: unit.z + dz * k } })
381
+ continue
382
+ }
383
+ const target =
384
+ still(unit, 8) ??
385
+ best(
386
+ foes.filter((foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z) < 8),
387
+ (foe) => foe.health
388
+ )
389
+ if (target) order(unit.id, `shoot${target.id}`, { type: 'attack', target: target.id })
390
+ else order(unit.id, 'advance', { type: 'attackMove', point: front })
391
+ continue
392
+ }
393
+ if (unit.type === UNIT.knight) {
394
+ const hunted = still(unit, 30)
395
+ const prey =
396
+ (hunted?.type === UNIT.archer ? hunted : undefined) ??
397
+ best(
398
+ foes.filter((foe) => foe.type === UNIT.archer),
399
+ (foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z)
400
+ )
401
+ if (prey) order(unit.id, `hunt${prey.id}`, { type: 'attack', target: prey.id })
402
+ else order(unit.id, 'advance', { type: 'attackMove', point: front })
403
+ continue
404
+ }
405
+ if (hurt && nearAway < 6) {
406
+ order(unit.id, 'fall back', { type: 'move', point: rear })
407
+ continue
408
+ }
409
+ // the footmen together on the weakest foe near them
410
+ const target =
411
+ still(unit, 7) ??
412
+ best(
413
+ foes.filter((foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z) < 7),
414
+ (foe) => foe.health + exactMath.hypot(foe.x - unit.x, foe.z - unit.z) * 10
415
+ )
416
+ if (target) order(unit.id, `hit${target.id}`, { type: 'attack', target: target.id })
417
+ else order(unit.id, 'advance', { type: 'attackMove', point: front })
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ /** The general: a brain of one module, thinking twice a second */
424
+ export const general = defineBrain({ id: 'showcase.general', modules: [tactics], every: 0.5 })
425
+
426
+ /** The two armies of the generals: footmen in front, archers behind, a knight on a wing */
427
+ function generalsArmies(world: World): void {
428
+ for (const [owner, z, back] of [
429
+ [PLAYER, 18, -1],
430
+ [ENEMY, 46, 1]
431
+ ] as const) {
432
+ for (let i = 0; i < 14; i++)
433
+ world.spawn(UNIT.footman, { at: { x: 23 + (i % 7) * 1.5, z: z + Math.floor(i / 7) * 1.4 * back }, owner, facing: back < 0 ? 0 : Math.PI })
434
+ for (let i = 0; i < 8; i++)
435
+ world.spawn(UNIT.archer, {
436
+ at: { x: 24.5 + (i % 4) * 2.2, z: z + (3.6 + Math.floor(i / 4) * 1.4) * back },
437
+ owner,
438
+ facing: back < 0 ? 0 : Math.PI
439
+ })
440
+ world.spawn(UNIT.knight, { at: { x: owner === PLAYER ? 18 : 46, z: z + 1.5 * back }, owner, facing: back < 0 ? 0 : Math.PI })
441
+ }
442
+ }
443
+
444
+ /** Both generals seated, each playing its side */
445
+ function seatGenerals(world: World): void {
446
+ world.computers.seat(PLAYER, { brain: general })
447
+ world.computers.seat(ENEMY, { brain: general })
448
+ }
449
+
450
+ /** Both armies laid out again, and the generals at them */
451
+ export const generalsReset = defineCommand('showcase.generalsReset', {}, (world) => {
452
+ clearAll(world)
453
+ generalsArmies(world)
454
+ seatGenerals(world)
455
+ })
456
+
457
+ // ---- a battle that turns back time: the world recorded, and laid out again as it was
458
+
459
+ /** One unit as it was at a moment: enough to make it again */
460
+ interface Kept {
461
+ type: string
462
+ owner: PlayerId
463
+ x: number
464
+ z: number
465
+ facing: number
466
+ health: number
467
+ }
468
+ /** What the world looked like, twice a second for the last fifteen seconds: kept in the world's state, so a save has it */
469
+ const historyState = defineComponent<{ frames: { time: number; units: Kept[] }[] }>({ id: 'showcase.history', default: () => ({ frames: [] }) })
470
+
471
+ /** The armies of the rewind battle */
472
+ function rewindArmies(world: World): void {
473
+ for (const [owner, z, back] of [
474
+ [PLAYER, 18, -1],
475
+ [ENEMY, 30, 1]
476
+ ] as const) {
477
+ for (let i = 0; i < 12; i++)
478
+ world.spawn(UNIT.footman, { at: { x: 20 + (i % 6) * 1.6, z: z + Math.floor(i / 6) * 1.4 * back }, owner, facing: back < 0 ? 0 : Math.PI })
479
+ for (let i = 0; i < 6; i++) world.spawn(UNIT.archer, { at: { x: 21 + (i % 6) * 1.4, z: z + 4 * back }, owner, facing: back < 0 ? 0 : Math.PI })
480
+ }
481
+ }
482
+
483
+ /** Both sides at each other */
484
+ function fight(world: World): void {
485
+ for (const owner of [PLAYER, ENEMY])
486
+ orderAll(
487
+ world,
488
+ owner,
489
+ alive(world, (unit) => unit.owner === owner),
490
+ { type: 'attackMove', point: { x: 24, z: owner === PLAYER ? 36 : 12 } }
491
+ )
492
+ }
493
+
494
+ /** The world recorded twice a second, on the rewind example's field alone */
495
+ const recording = {
496
+ name: 'showcase.recording',
497
+ every: 15,
498
+ update: (world: World) => {
499
+ if (world.map?.name !== 'Turn back time') return
500
+ const history = world.state(historyState)
501
+ const units = alive(world, () => true).map((unit) => ({
502
+ type: unit.def.id,
503
+ owner: unit.owner,
504
+ x: unit.pos.x,
505
+ z: unit.pos.z,
506
+ facing: unit.facing,
507
+ health: unit.health
508
+ }))
509
+ world.setState(historyState, { frames: [...history.frames.slice(-29), { time: world.time, units }] })
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Time turned back `seconds`: every unit taken away and laid out again as the record of that moment has it, where it
515
+ * stood, as hurt as it was, the fallen of since standing again; then the fight goes on from there
516
+ */
517
+ export const rewind = defineCommand('showcase.rewind', { seconds: number }, (world, _player, { seconds }) => {
518
+ const { frames } = world.state(historyState)
519
+ const then = best(frames, (frame) => Math.abs(frame.time - (world.time - seconds)))
520
+ if (!then) return
521
+ clearAll(world)
522
+ for (const kept of then.units) {
523
+ const unit = world.spawn(kept.type, { at: { x: kept.x, z: kept.z }, owner: kept.owner, facing: kept.facing })
524
+ unit.health = Math.min(kept.health, unit.maxHealth)
525
+ }
526
+ world.setState(historyState, { frames: frames.filter((frame) => frame.time <= then.time) })
527
+ world.fx('shockwave', { at: { x: 24, z: 24 }, params: { size: 3, color: '#9fd6ff' } })
528
+ fight(world)
529
+ })
530
+ /** The battle from the start */
531
+ export const rewindReset = defineCommand('showcase.rewindReset', {}, (world) => {
532
+ clearAll(world)
533
+ world.setState(historyState, { frames: [] })
534
+ rewindArmies(world)
535
+ })
536
+ /** The two sides sent at each other */
537
+ export const rewindFight = defineCommand('showcase.rewindFight', {}, (world) => fight(world))
538
+
539
+ // ---- a march: companies in formation behind a leader who walks a road round the field
540
+
541
+ /** The leader's road: round the field and back, for ever */
542
+ export const MARCH: Vec2[] = [
543
+ { x: 16, z: 16 },
544
+ { x: 48, z: 16 },
545
+ { x: 52, z: 32 },
546
+ { x: 48, z: 48 },
547
+ { x: 16, z: 48 },
548
+ { x: 12, z: 32 }
549
+ ]
550
+ /** How the companies march, kept in the world's state: their shape, and whether they march or stand */
551
+ export const marching = defineComponent<{ shape: 'rows' | 'square' | 'circle'; halted: boolean }>({
552
+ id: 'showcase.marching',
553
+ default: () => ({ shape: 'rows', halted: false })
554
+ })
555
+
556
+ /** The companies, by what they are, and how far behind the leader each marches */
557
+ const COMPANIES = [
558
+ { type: UNIT.footman, count: 36, behind: 5 },
559
+ { type: UNIT.archer, count: 24, behind: 12 },
560
+ { type: UNIT.mage, count: 12, behind: 18 }
561
+ ]
562
+
563
+ /** The leader, and the companies in ranks behind it */
564
+ function column(world: World): void {
565
+ world.spawn(UNIT.knight, { at: MARCH[0]!, owner: PLAYER, facing: Math.PI / 2 })
566
+ for (const company of COMPANIES)
567
+ for (let i = 0; i < company.count; i++)
568
+ world.spawn(company.type, {
569
+ at: { x: MARCH[0]!.x - company.behind - Math.floor(i / 6) * 1.4, z: MARCH[0]!.z - 3.5 + (i % 6) * 1.4 },
570
+ owner: PLAYER,
571
+ facing: Math.PI / 2
572
+ })
573
+ }
574
+
575
+ /**
576
+ * The march, every two seconds: the leader walks on to the next point of its road; each company is sent, in one order and in
577
+ * the formation chosen, to a place behind the leader on the way it goes, facing that way (`OrderInit.facing`). The
578
+ * formation lays each company out anew round its place; the units find their spots in it as they walk
579
+ */
580
+ const march = {
581
+ name: 'showcase.march',
582
+ every: 60,
583
+ update: (world: World) => {
584
+ if (world.map?.name !== 'March') return
585
+ const leader = alive(world, (unit) => unit.def.id === UNIT.knight)[0]
586
+ if (!leader) return
587
+ const how = world.state(marching)
588
+ if (how.halted) return
589
+ const goal = leader.orders[0]?.init.point as Vec2 | undefined
590
+ if (!goal || exactMath.hypot(goal.x - leader.pos.x, goal.z - leader.pos.z) < 3) {
591
+ const near = MARCH.reduce(
592
+ (best, each, i) =>
593
+ exactMath.hypot(each.x - leader.pos.x, each.z - leader.pos.z) <
594
+ exactMath.hypot(MARCH[best]!.x - leader.pos.x, MARCH[best]!.z - leader.pos.z)
595
+ ? i
596
+ : best,
597
+ 0
598
+ )
599
+ leader.order('move', MARCH[(near + 1) % MARCH.length]!)
600
+ }
601
+ const to = (leader.orders[0]?.init.point as Vec2 | undefined) ?? leader.pos
602
+ const away = exactMath.hypot(to.x - leader.pos.x, to.z - leader.pos.z) || 1
603
+ const forward = { x: (to.x - leader.pos.x) / away, z: (to.z - leader.pos.z) / away }
604
+ const facing = exactMath.atan2(forward.x, forward.z)
605
+ for (const company of COMPANIES) {
606
+ const units = alive(world, (unit) => unit.def.id === company.type)
607
+ const place = { x: leader.pos.x - forward.x * company.behind, z: leader.pos.z - forward.z * company.behind }
608
+ if (units.length)
609
+ world.issue({
610
+ type: 'order',
611
+ player: PLAYER,
612
+ entities: units.map((unit) => unit.id),
613
+ order: { type: 'move', point: place, formation: how.shape, facing } as never
614
+ })
615
+ }
616
+ }
617
+ }
618
+
619
+ /** The companies' shape, or a halt and on again */
620
+ export const marchAs = defineCommand('showcase.marchAs', { shape: oneOf('rows', 'square', 'circle') }, (world, _player, { shape }) => {
621
+ world.setState(marching, { ...world.state(marching), shape })
622
+ })
623
+ export const halt = defineCommand('showcase.halt', { halted: bool }, (world, player, { halted }) => {
624
+ world.setState(marching, { ...world.state(marching), halted })
625
+ if (halted)
626
+ orderAll(
627
+ world,
628
+ player,
629
+ alive(world, (unit) => unit.owner === player),
630
+ { type: 'stop' }
631
+ )
632
+ })
633
+
634
+ export const category: ExampleCategory = {
635
+ rules: (game: Game<any>) => {
636
+ for (const unit of units) game.unit(unit)
637
+ for (const component of [bossState, defenseState, historyState, marching]) game.component(component)
638
+ game.buff(enraged)
639
+ game.brain(general)
640
+ game.system(bossPhases)
641
+ game.system(waves)
642
+ game.system(recording)
643
+ game.system(march)
644
+ for (const command of [bossReset, bossCharge, buildTower, defenseReset, callWave, generalsReset, rewind, rewindReset, rewindFight, marchAs, halt])
645
+ game.command(command)
646
+ // a raider's bounty, to the keep's gold
647
+ game.on(entityDied, (world, { entity, reason }) => {
648
+ const bounty = BOUNTY[entity.def.id]
649
+ const keep = bounty && reason !== 'removed' ? keepOf(world) : undefined
650
+ const defense = keep?.get(defenseState)
651
+ if (keep && defense) keep.set(defenseState, { ...defense, gold: defense.gold + bounty! })
652
+ })
653
+ },
654
+ examples: [
655
+ {
656
+ id: 'showcase-boss',
657
+ category: 'showcase',
658
+ title: 'The Bone King: a boss in phases',
659
+ summary:
660
+ 'A party against a boss that changes as it is hurt: at two thirds it raises the dead, at a third it is enraged and slams the ground. Charge, or lead the party yourself (select, right-click).',
661
+ tags: ['boss', 'phases', 'raid', 'adds', 'enrage', 'system', 'buff', 'fight'],
662
+ map: () => exampleField({ name: 'Bone King', players: 2 }),
663
+ start: (world) => bossArena(world),
664
+ source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
665
+ },
666
+ {
667
+ id: 'showcase-defense',
668
+ category: 'showcase',
669
+ title: 'Hold the road: a tower defense',
670
+ summary:
671
+ 'Raiders come down the road in waves, bigger each time, brutes every third; towers shoot them as they pass. Build towers with the gold they bring, and keep them from the keep.',
672
+ tags: ['tower defense', 'waves', 'road', 'lives', 'gold', 'towers', 'td'],
673
+ map: () => defenseMap(),
674
+ start: (world) => defenseStart(world),
675
+ source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
676
+ },
677
+ {
678
+ id: 'showcase-generals',
679
+ category: 'showcase',
680
+ title: 'Two computer generals',
681
+ summary:
682
+ 'Both sides played by a computer with tactics of its own: footmen gang up on the weakest and fall back hurt, archers keep their distance, the knight hunts the archers. Sit back and watch, then lay them out again.',
683
+ tags: ['ai', 'computer player', 'brain', 'defineBrain', 'tactics', 'kiting', 'focus fire'],
684
+ map: () => exampleField({ name: 'Generals', size: 64, players: 2 }),
685
+ start: (world) => {
686
+ generalsArmies(world)
687
+ seatGenerals(world)
688
+ },
689
+ source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
690
+ },
691
+ {
692
+ id: 'showcase-rewind',
693
+ category: 'showcase',
694
+ title: 'Turn back time',
695
+ summary:
696
+ 'A battle the world records twice a second. Let them fight, then turn back time: every unit stands where it stood, as hurt as it was, the fallen up again, and the fight goes on from there.',
697
+ tags: ['rewind', 'time', 'record', 'state', 'save', 'replay', 'component'],
698
+ map: () => exampleField({ name: 'Turn back time', players: 2 }),
699
+ start: (world) => rewindArmies(world),
700
+ source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
701
+ },
702
+ {
703
+ id: 'showcase-march',
704
+ category: 'showcase',
705
+ title: 'Companies on the march',
706
+ summary:
707
+ 'A knight walks a road round the field, and seventy-two behind him follow in three companies, each in formation and facing the way they go. Change their shape as they march, or halt them.',
708
+ tags: ['formation', 'march', 'follow', 'leader', 'column', 'army', 'maneuver'],
709
+ map: () => exampleField({ name: 'March', size: 64 }),
710
+ start: (world) => column(world),
711
+ source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
712
+ }
713
+ ]
714
+ }