@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,158 @@
1
+ /**
2
+ * Crowd control: buffs with a flag, put on by the `buff` effect. Stun, root, silence, disarm, knocked up, knocked
3
+ * down, fear, charm, taunt, sleep, polymorph, suppression and a slow; then cleanse and dispel to take them off. The
4
+ * footmen walk up and down so that what holds them, slows them or turns them shows
5
+ */
6
+ import { type AbilityDef, type BuffDef, defineCommand, type World } from '@v12-engine/engine'
7
+ import { exampleField } from '../define'
8
+ import { ENEMY } from '../common/units'
9
+ import type { Family } from './family'
10
+ import { caster, lineUp, put, sparring } from './kit'
11
+
12
+ /** A crowd control buff of `seconds`, harmful, with its flags */
13
+ const held = (id: string, name: string, duration: number, flags: Partial<BuffDef>): BuffDef => ({ id, name, harmful: true, duration, ...flags })
14
+
15
+ const buffs: BuffDef[] = [
16
+ held('abilitiesStunned', 'Stunned', 2, { stun: true }),
17
+ held('abilitiesRooted', 'Rooted', 3, { root: true }),
18
+ held('abilitiesSilenced', 'Silenced', 3, { silence: true }),
19
+ held('abilitiesDisarmed', 'Disarmed', 3, { disarm: true }),
20
+ // a stun that throws it up, and one that lays it down: stuns with a look
21
+ held('abilitiesKnockedUp', 'Airborne', 1, { stun: true, airborne: true }),
22
+ held('abilitiesKnockedDown', 'Knocked down', 2, { stun: true, down: true }),
23
+ // controls: who carries one walks away from (fear), toward (charm) or attacks (taunt) whoever put it on
24
+ held('abilitiesFeared', 'Terrified', 2.5, { control: 'fear' }),
25
+ held('abilitiesCharmed', 'Charmed', 2.5, { control: 'charm' }),
26
+ held('abilitiesTaunted', 'Taunted', 3, { control: 'taunt' }),
27
+ // a stun broken by the first damage
28
+ held('abilitiesAsleep', 'Asleep', 6, { stun: true, breaksOn: ['damage'] }),
29
+ // a polymorph: can't attack or cast, slower, and drawn as a sheep (`drawnAs` on its look)
30
+ held('abilitiesHexed', 'Hexed', 4, { disarm: true, silence: true, percent: { moveSpeed: -0.4 } }),
31
+ // held like a stun, but tenacity doesn't shorten it nor cleanse take it off
32
+ held('abilitiesSuppressed', 'Suppressed', 2.5, { suppress: true }),
33
+ held('abilitiesFrost', 'Frost', 4, { percent: { moveSpeed: -0.6, attackSpeed: -0.3 } })
34
+ ]
35
+
36
+ /** An ability putting a buff on the enemy it is cast on, or on every enemy within `radius` of the spot */
37
+ const putOn = (id: string, name: string, hotkey: string, buff: string, description: string, radius?: number): AbilityDef => ({
38
+ id,
39
+ name,
40
+ description,
41
+ hotkey,
42
+ target: radius ? 'point' : 'unit',
43
+ ...(!radius && { targets: { enemies: true } }),
44
+ range: 12,
45
+ cooldown: 0.5,
46
+ effects: [{ type: 'buff', buff, ...(radius && { radius }) }]
47
+ })
48
+
49
+ const abilities: AbilityDef[] = [
50
+ putOn('abilitiesThunderBolt', 'Stun', 'Q', 'abilitiesStunned', 'Can not move, attack or cast for 2 seconds; what it was told waits'),
51
+ putOn('abilitiesEntangle', 'Root', 'W', 'abilitiesRooted', 'Can not move for 3 seconds, but still attacks and casts'),
52
+ putOn('abilitiesSilence', 'Silence', 'E', 'abilitiesSilenced', 'Can not cast for 3 seconds, everyone within 3 of the spot', 3),
53
+ putOn('abilitiesDisarm', 'Disarm', 'R', 'abilitiesDisarmed', 'Can not attack for 3 seconds: the brute stops swinging'),
54
+ putOn('abilitiesKnockUp', 'Knock up', 'T', 'abilitiesKnockedUp', 'Thrown in the air for a second, everyone within 2.5 of the spot', 2.5),
55
+ putOn('abilitiesKnockDown', 'Knock down', 'D', 'abilitiesKnockedDown', 'Laid on the ground for 2 seconds'),
56
+ {
57
+ // on everyone near the caster: they run from it
58
+ id: 'abilitiesTerrify',
59
+ name: 'Fear',
60
+ description: 'Every enemy within 12 runs away from the caster for 2.5 seconds',
61
+ hotkey: 'F',
62
+ target: 'none',
63
+ cooldown: 0.5,
64
+ effects: [{ type: 'buff', buff: 'abilitiesFeared', radius: 12 }]
65
+ },
66
+ putOn('abilitiesCharm', 'Charm', 'G', 'abilitiesCharmed', 'Walks helplessly toward the caster for 2.5 seconds'),
67
+ putOn('abilitiesTaunt', 'Taunt', 'Z', 'abilitiesTaunted', 'Must attack the caster for 3 seconds: cast on the brute, it leaves the footman'),
68
+ putOn('abilitiesSleep', 'Sleep', 'X', 'abilitiesAsleep', 'Asleep for 6 seconds, until something hurts it'),
69
+ putOn('abilitiesHex', 'Polymorph', 'C', 'abilitiesHexed', 'A sheep for 4 seconds: can not attack or cast, and slow'),
70
+ putOn('abilitiesNetherGrasp', 'Suppress', 'V', 'abilitiesSuppressed', 'Held for 2.5 seconds; cleanse does not take it off'),
71
+ putOn('abilitiesFrostNova', 'Slow', 'B', 'abilitiesFrost', 'Everyone within 3 of the spot is 60% slower for 4 seconds', 3),
72
+ {
73
+ // cleanse takes off stuns, roots, silences, disarms, controls and slows; allies when not said, the enemies here
74
+ id: 'abilitiesUnbind',
75
+ name: 'Cleanse',
76
+ description: 'Takes stuns, roots, silences, fears and slows off every enemy within 12 (not a suppression)',
77
+ hotkey: 'N',
78
+ target: 'none',
79
+ cooldown: 0.5,
80
+ effects: [{ type: 'cleanse', radius: 12, affects: 'enemies' }]
81
+ },
82
+ {
83
+ // dispel takes off whatever buff may be dispelled, the good and the bad
84
+ id: 'abilitiesPurge',
85
+ name: 'Dispel',
86
+ description: 'Takes every buff that may be dispelled off every unit within 12, a suppression too',
87
+ hotkey: 'Y',
88
+ target: 'none',
89
+ cooldown: 0.5,
90
+ effects: [{ type: 'dispel', radius: 12, affects: 'all' }]
91
+ }
92
+ ]
93
+
94
+ /**
95
+ * Each footman walks up and down its own line, so that what stops, slows or turns it shows; a sentry (the vision
96
+ * example's) across the north. The ranger who hides stays where it is
97
+ */
98
+ function patrolAll(world: World): void {
99
+ const ranger = world.named('ranger')
100
+ for (const unit of world.units.values()) {
101
+ if (!unit.alive || unit.owner !== ENEMY || unit === ranger) continue
102
+ if (unit.def.id === 'abilitiesTarget') unit.order('patrol', { x: unit.pos.x, z: unit.pos.z + 6 })
103
+ if (unit.def.id === 'abilitiesSentry') unit.order('patrol', { x: unit.pos.x + 8, z: unit.pos.z + 2 })
104
+ }
105
+ }
106
+
107
+ /** Sends the footmen walking again: what a control or a stun took away from them */
108
+ export const walkAgain = defineCommand('abilities.walk', {}, (world) => patrolAll(world))
109
+
110
+ export const control: Family = {
111
+ abilities,
112
+ buffs,
113
+ commands: [walkAgain],
114
+ units: [
115
+ caster(
116
+ 'abilitiesBinder',
117
+ 'Binder',
118
+ abilities.map((each) => each.id),
119
+ {
120
+ stats: {
121
+ health: 4000,
122
+ healthRegen: 40,
123
+ mana: 1000,
124
+ manaRegen: 60,
125
+ attackDamage: 30,
126
+ attackSpeed: 1,
127
+ attackRange: 6,
128
+ moveSpeed: 4.2,
129
+ armor: 20
130
+ }
131
+ }
132
+ ),
133
+ // a brute that fights what is next to it, for disarm and taunt
134
+ sparring('abilitiesBrute', 'Brute', {
135
+ radius: 0.55,
136
+ stats: { health: 1500, healthRegen: 30, attackDamage: 12, attackSpeed: 1, attackRange: 0.6, moveSpeed: 3.6, armor: 5 },
137
+ attacks: [{ damagePoint: 0.4, acquisitionRange: 2 }]
138
+ }),
139
+ // what the polymorph draws its target as (`drawnAs`): a type of its own, never put down
140
+ { id: 'abilitiesSheep', name: 'Sheep', kind: 'unit', radius: 0.4, stats: { health: 10, moveSpeed: 2 }, sight: 2 }
141
+ ],
142
+ example: {
143
+ id: 'abilities-control',
144
+ category: 'abilities',
145
+ title: 'Crowd control',
146
+ summary:
147
+ 'Stun, root, silence, disarm, knock up and down, fear, charm, taunt, sleep, polymorph, suppress and slow, then cleanse and dispel to take them off. The footmen walk up and down so each shows; the brute on the right fights a footman of yours.',
148
+ tags: ['crowd control', 'cc', 'stun', 'root', 'silence', 'fear', 'charm', 'taunt', 'polymorph', 'sleep', 'slow', 'cleanse', 'dispel', 'knock up'],
149
+ source: ['examples/abilities/control.ts', 'examples/abilities/kit.ts'],
150
+ map: () => exampleField({ name: 'Crowd control', players: 2 }),
151
+ start: (world) => {
152
+ lineUp(world, 'abilitiesBinder', 'abilitiesTarget', 4)
153
+ // the brute, and a footman of yours by it to fight
154
+ put(world, 'abilitiesBrute', ENEMY, { x: 31, z: 22 }, 'brute', -Math.PI / 2)
155
+ put(world, 'abilitiesWounded', 1, { x: 29.8, z: 22 }, 'ally', Math.PI / 2)
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,78 @@
1
+ /** How the crowd control example looks, and its card: a button for each hold, and one to send the footmen walking */
2
+ import { button } from '../../shell/look'
3
+ import { character, hold, person, sharedLooks, TWO_HANDED } from '../common/looks'
4
+ import { control, walkAgain } from './control'
5
+ import type { FamilyLooks } from './looks'
6
+ import { castButton, look, readyOn, resetButton } from './lookKit'
7
+
8
+ /** A buff's look on its carrier: sparks of `shape` in its colour (`glow`) */
9
+ const glow = (color: [number, number, number], shape: string, more: Record<string, unknown> = {}) =>
10
+ look(color, undefined, { aura: 'glow', options: { shape, ...more } })
11
+
12
+ /** The abilities cast on the brute rather than on the nearest footman */
13
+ const onBrute = new Set(['abilitiesDisarm', 'abilitiesTaunt'])
14
+ /** The abilities cast on a footman, where the brute is as near */
15
+ const onFootman = new Set([
16
+ 'abilitiesHex',
17
+ 'abilitiesThunderBolt',
18
+ 'abilitiesEntangle',
19
+ 'abilitiesKnockDown',
20
+ 'abilitiesCharm',
21
+ 'abilitiesSleep',
22
+ 'abilitiesNetherGrasp'
23
+ ])
24
+
25
+ export const controlLooks: FamilyLooks = {
26
+ units: {
27
+ abilitiesBinder: sharedLooks.mage,
28
+ abilitiesBrute: { height: 2.3, model: character('barbarian', 0.95, { hold: [hold('axe_2handed')] }), attacks: [TWO_HANDED], shapes: person(2.3) },
29
+ // the shared art has no animals: a sheep of parts, a woolly body and a dark head
30
+ abilitiesSheep: {
31
+ height: 0.9,
32
+ shapes: [
33
+ { shape: 'sphere', size: [0.9, 0.7, 1.1], offset: [0, 0.6, 0], color: [0.95, 0.95, 0.9] },
34
+ { shape: 'sphere', size: [0.4, 0.4, 0.45], offset: [0, 0.85, 0.55], color: [0.15, 0.15, 0.15] }
35
+ ]
36
+ }
37
+ },
38
+ abilities: {
39
+ abilitiesThunderBolt: look([1, 0.95, 0.5], 'burst'),
40
+ abilitiesEntangle: look([0.35, 0.8, 0.25], 'burst'),
41
+ abilitiesSilence: look([0.6, 0.4, 1], 'sonar'),
42
+ abilitiesKnockUp: look([0.8, 0.7, 0.5], 'smash'),
43
+ abilitiesTerrify: look([0.55, 0.15, 0.7], 'sonar'),
44
+ abilitiesHex: look([0.4, 1, 0.4], 'burst'),
45
+ abilitiesFrostNova: look([0.6, 0.85, 1], 'slam'),
46
+ abilitiesUnbind: look([1, 1, 1], 'sonar'),
47
+ abilitiesPurge: look([0.5, 1, 0.9], 'sonar'),
48
+ abilitiesStunned: glow([1, 0.95, 0.5], 'ring', { at: 'head' }),
49
+ abilitiesRooted: glow([0.35, 0.8, 0.25], 'column'),
50
+ abilitiesSilenced: glow([0.6, 0.4, 1], 'ring', { at: 'head' }),
51
+ abilitiesDisarmed: glow([0.8, 0.8, 0.8], 'ring', { at: 'head' }),
52
+ abilitiesAsleep: glow([0.5, 0.6, 1], 'rise'),
53
+ // the hexed footman drawn as a sheep
54
+ abilitiesHexed: look([0.4, 1, 0.4], undefined, { drawnAs: 'abilitiesSheep' }),
55
+ abilitiesSuppressed: glow([0.5, 0.1, 0.6], 'shell'),
56
+ abilitiesFrost: glow([0.6, 0.85, 1], 'fall'),
57
+ // the mark over a feared, charmed or taunted unit, in its colour
58
+ abilitiesFeared: look([0.55, 0.15, 0.7], undefined, { marker: true }),
59
+ abilitiesCharmed: look([1, 0.45, 0.75], undefined, { marker: true }),
60
+ abilitiesTaunted: look([1, 0.3, 0.2], undefined, { marker: true })
61
+ },
62
+ examples: {
63
+ 'abilities-control': {
64
+ ready: (client) => {
65
+ readyOn(client, 'abilitiesBinder', { minX: 17, minZ: 14, maxX: 33, maxZ: 31 })
66
+ // the footmen set walking, once the game runs
67
+ client.session.send(walkAgain.make({}))
68
+ },
69
+ controls: [
70
+ ...control.abilities.map((ability) =>
71
+ castButton(ability, onBrute.has(ability.id) ? { at: 'brute' } : onFootman.has(ability.id) ? { at: 'enemy2' } : {})
72
+ ),
73
+ button('Walk again', (client) => client.session.send(walkAgain.make({})), 'Sends the footmen up and down their lines again'),
74
+ resetButton('abilities-control')
75
+ ]
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Damage and heal: the `damage`, `heal` and `mana` effects, how much they do (by level, by the target's health, by
3
+ * chance), and the conditions that pick one or the other (`if`). A mage casts each on a row of sparring footmen and on a
4
+ * wounded ally
5
+ */
6
+ import type { AbilityDef } from '@v12-engine/engine'
7
+ import { exampleField } from '../define'
8
+ import type { Family } from './family'
9
+ import { caster, lineUp, sparring } from './kit'
10
+
11
+ const abilities: AbilityDef[] = [
12
+ {
13
+ // the plainest: magic damage to the unit it is cast on
14
+ id: 'abilitiesFireBolt',
15
+ name: 'Fire Bolt',
16
+ description: 'Burns an enemy for {damage}',
17
+ hotkey: 'Q',
18
+ target: 'unit',
19
+ targets: { enemies: true },
20
+ range: 8,
21
+ manaCost: 40,
22
+ cooldown: 1,
23
+ effects: [{ type: 'damage', amount: 140, damageType: 'magic' }]
24
+ },
25
+ {
26
+ // a radius makes it an area: every enemy within 2.5 cells of the spot
27
+ id: 'abilitiesShockBlast',
28
+ name: 'Shock Blast',
29
+ description: 'Blasts the ground: {damage} to every enemy within {radius}',
30
+ hotkey: 'W',
31
+ target: 'point',
32
+ range: 9,
33
+ manaCost: 60,
34
+ cooldown: 1.5,
35
+ effects: [{ type: 'damage', amount: 90, radius: 2.5 }]
36
+ },
37
+ {
38
+ // an `if` asks about the target: below half its health it takes true damage that ignores armor, more the more it lacks
39
+ id: 'abilitiesExecute',
40
+ name: 'Execute',
41
+ description: 'Below half health, a blow of true damage that grows with the health it lacks; above, a plain cut',
42
+ hotkey: 'E',
43
+ target: 'unit',
44
+ targets: { enemies: true },
45
+ range: 8,
46
+ manaCost: 50,
47
+ cooldown: 1,
48
+ effects: [
49
+ {
50
+ type: 'if',
51
+ target: { healthBelow: 0.5 },
52
+ effects: [{ type: 'damage', amount: 150, byMissingHealth: 0.5, damageType: 'true' }],
53
+ otherwise: [{ type: 'damage', amount: 60, damageType: 'physical' }]
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ // heal reaches allies: the wounded footman beside the mage
59
+ id: 'abilitiesHolyLight',
60
+ name: 'Holy Light',
61
+ description: 'Heals an ally for {heal}',
62
+ hotkey: 'R',
63
+ target: 'unit',
64
+ targets: { allies: true, self: true },
65
+ range: 8,
66
+ manaCost: 50,
67
+ cooldown: 1,
68
+ effects: [{ type: 'heal', amount: 200 }]
69
+ },
70
+ {
71
+ // hurt one, heal the caster: two effects, one with `on: 'caster'`
72
+ id: 'abilitiesDrain',
73
+ name: 'Drain',
74
+ description: 'Takes {damage} from an enemy and heals the caster as much',
75
+ hotkey: 'T',
76
+ target: 'unit',
77
+ targets: { enemies: true },
78
+ range: 8,
79
+ manaCost: 40,
80
+ cooldown: 1,
81
+ effects: [
82
+ { type: 'damage', amount: 80 },
83
+ { type: 'heal', amount: 80, on: 'caster' }
84
+ ]
85
+ },
86
+ {
87
+ // the three most hurt allies around the caster, picked by a condition, sorted and limited
88
+ id: 'abilitiesMend',
89
+ name: 'Mend',
90
+ description: 'Heals the three most hurt allies near for {heal}, and gives them back mana',
91
+ hotkey: 'D',
92
+ target: 'none',
93
+ manaCost: 60,
94
+ cooldown: 2,
95
+ effects: [
96
+ { type: 'heal', amount: 120, radius: 8, affects: 'allies', sort: 'lowestHealthShare', limit: 3 },
97
+ { type: 'mana', amount: 50, radius: 8 }
98
+ ]
99
+ },
100
+ {
101
+ // a zone hurts those inside four times a second: a share of their most health, so it bites big and small alike
102
+ id: 'abilitiesDecay',
103
+ name: 'Decay',
104
+ description: 'Rots the ground for 4 seconds: each second, 4% of the most health of every enemy on it',
105
+ hotkey: 'F',
106
+ target: 'point',
107
+ range: 9,
108
+ manaCost: 70,
109
+ cooldown: 3,
110
+ effects: [{ type: 'zone', radius: 2.5, duration: 4, effects: [{ type: 'damage', amount: 0, byMaxHealth: 0.01, damageType: 'true' }] }]
111
+ },
112
+ {
113
+ // a chance on any effect: here a third of the time it strikes twice
114
+ id: 'abilitiesLuckyStrike',
115
+ name: 'Lucky Strike',
116
+ description: 'Strikes for 70, and a third of the time for 140 more',
117
+ hotkey: 'G',
118
+ target: 'unit',
119
+ targets: { enemies: true },
120
+ range: 8,
121
+ cooldown: 0.5,
122
+ effects: [
123
+ { type: 'damage', amount: 70, damageType: 'physical' },
124
+ { type: 'damage', amount: 140, damageType: 'physical', chance: 0.33 }
125
+ ]
126
+ }
127
+ ]
128
+
129
+ export const damage: Family = {
130
+ abilities,
131
+ units: [
132
+ caster(
133
+ 'abilitiesHealer',
134
+ 'Battle mage',
135
+ abilities.map((each) => each.id)
136
+ ),
137
+ sparring('abilitiesTarget', 'Sparring footman'),
138
+ // an ally to heal: it comes in wounded, and does not heal by itself
139
+ sparring('abilitiesWounded', 'Wounded footman', { stats: { health: 900, moveSpeed: 3.2, armor: 5 } })
140
+ ],
141
+ example: {
142
+ id: 'abilities-damage',
143
+ category: 'abilities',
144
+ title: 'Damage and heal',
145
+ summary:
146
+ 'Damage, heal and mana: plain amounts, areas, a blow that grows with the health the target lacks, a condition, a chance. Press a button to cast it; the wounded footman on the left is the one healed.',
147
+ tags: ['damage', 'heal', 'mana', 'execute', 'if', 'chance', 'zone', 'true damage'],
148
+ source: ['examples/abilities/damage.ts', 'examples/abilities/kit.ts'],
149
+ map: () => exampleField({ name: 'Damage and heal', players: 2 }),
150
+ start: (world) => {
151
+ lineUp(world, 'abilitiesHealer', 'abilitiesTarget', 3, 'abilitiesWounded')
152
+ const wounded = world.named('ally')
153
+ if (wounded) wounded.health = wounded.maxHealth * 0.3
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,37 @@
1
+ /** How the damage and heal example looks, and its card: a button for each ability (`./damage.ts`) */
2
+ import { readout } from '../../shell/look'
3
+ import { sharedLooks } from '../common/looks'
4
+ import { damage } from './damage'
5
+ import type { FamilyLooks } from './looks'
6
+ import { castButton, look, readyOn, resetButton } from './lookKit'
7
+
8
+ export const damageLooks: FamilyLooks = {
9
+ units: {
10
+ abilitiesHealer: sharedLooks.mage,
11
+ abilitiesTarget: sharedLooks.footman,
12
+ abilitiesWounded: sharedLooks.footman
13
+ },
14
+ abilities: {
15
+ abilitiesFireBolt: look([1, 0.45, 0.1], 'burst'),
16
+ abilitiesShockBlast: look([0.55, 0.75, 1], 'smash'),
17
+ abilitiesExecute: look([0.85, 0.1, 0.1], 'burst'),
18
+ abilitiesHolyLight: look([1, 0.9, 0.45], 'burst'),
19
+ abilitiesDrain: look([0.6, 0.1, 0.5], 'tether'),
20
+ abilitiesMend: look([0.45, 1, 0.55], 'sonar'),
21
+ abilitiesDecay: look([0.4, 0.75, 0.2], 'mark'),
22
+ abilitiesLuckyStrike: look([1, 0.8, 0.2], 'burst')
23
+ },
24
+ examples: {
25
+ 'abilities-damage': {
26
+ ready: (client) => readyOn(client, 'abilitiesHealer'),
27
+ controls: [
28
+ ...damage.abilities.map((ability) => castButton(ability)),
29
+ resetButton('abilities-damage'),
30
+ readout('Wounded ally', (client) => {
31
+ const ally = client.session.snapshot().entities.find((each) => each.type === 'abilitiesWounded')
32
+ return ally ? `${Math.round(ally.health)} health` : '—'
33
+ })
34
+ ]
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A family of abilities, as an example shows it: the abilities, the buffs and units they use, and the example that
3
+ * casts them. Each family's file gives one; `./index.ts` registers them all
4
+ */
5
+ import type { AbilityDef, BuffDef, CommandDef, Game, UnitTypeDef } from '@v12-engine/engine'
6
+ import type { ExampleDef } from '../define'
7
+ import { starts } from './kit'
8
+
9
+ export interface Family {
10
+ /** The example that shows them */
11
+ example: ExampleDef
12
+ /** The abilities, in the order the card's buttons cast them */
13
+ abilities: AbilityDef[]
14
+ /** Abilities used but not cast from a button: what a trap or a barrel sets off, a passive of another's */
15
+ more?: AbilityDef[]
16
+ buffs?: BuffDef[]
17
+ units: UnitTypeDef[]
18
+ /** Commands of its own its card sends */
19
+ commands?: CommandDef<any>[]
20
+ }
21
+
22
+ /** Registers a family's definitions on the game, and its start for the reset command */
23
+ export function registerFamily(game: Game<any>, family: Family): void {
24
+ for (const unit of family.units) game.unit(unit)
25
+ for (const ability of [...family.abilities, ...(family.more ?? [])]) game.ability(ability)
26
+ for (const buff of family.buffs ?? []) game.buff(buff)
27
+ for (const command of family.commands ?? []) game.command(command)
28
+ if (family.example.start) starts[family.example.id] = family.example.start as (world: Parameters<(typeof starts)[string]>[0]) => void
29
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The abilities examples on the rules' side: a family of the engine's abilities each (`./family.ts`), every one cast
3
+ * from a button of the card by the command of `./kit.ts`. Their packs, abilities, buffs and units are registered here
4
+ */
5
+ import type { Game } from '@v12-engine/engine'
6
+ import type { ExampleCategory } from '../define'
7
+ import { areas } from './areas'
8
+ import { buffsFamily } from './buffs'
9
+ import { changing } from './changing'
10
+ import { control } from './control'
11
+ import { damage } from './damage'
12
+ import { type Family, registerFamily } from './family'
13
+ import { abilityPacks, kitRules } from './kit'
14
+ import { missiles } from './missiles'
15
+ import { moves } from './moves'
16
+ import { recasts } from './recasts'
17
+ import { resources } from './resources'
18
+ import { summons } from './summons'
19
+ import { triggers } from './triggers'
20
+ import { vision } from './vision'
21
+
22
+ /** The families, in the list's order */
23
+ export const families: Family[] = [damage, missiles, areas, control, buffsFamily, moves, summons, triggers, recasts, resources, vision, changing]
24
+
25
+ export const category: ExampleCategory = {
26
+ rules: (game: Game<any>) => {
27
+ abilityPacks(game)
28
+ kitRules(game)
29
+ for (const family of families) registerFamily(game, family)
30
+ },
31
+ examples: families.map((family) => family.example)
32
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * What the abilities examples share on the rules' side (the MOBA ones too): the packs their abilities need, a caster
3
+ * made of a list of abilities, sparring partners to cast on, and the command a card button sends to cast one. The
4
+ * engine alone
5
+ */
6
+ import {
7
+ abilityChangesPack,
8
+ type AbilityDef,
9
+ aimKind,
10
+ defineCommand,
11
+ type Entity,
12
+ type Game,
13
+ type GamePack,
14
+ groundObjectsPack,
15
+ holdsPack,
16
+ manaBurnPack,
17
+ movesPack,
18
+ optional,
19
+ point,
20
+ releaseHold,
21
+ resourcesPack,
22
+ string,
23
+ type UnitTypeDef,
24
+ type Vec2,
25
+ visionPack,
26
+ type World,
27
+ exactMath
28
+ } from '@v12-engine/engine'
29
+ import { ENEMY, PLAYER } from '../common/units'
30
+
31
+ /**
32
+ * Adds a pack to the game unless it has one of that id already: the game is one for every example, and two of a pack
33
+ * would run its rules twice
34
+ */
35
+ export function packOnce(game: Game<any>, pack: GamePack): void {
36
+ if ((game.toRules().packs ?? []).some((each) => each.id === pack.id)) return
37
+ game.pack(pack)
38
+ }
39
+
40
+ /**
41
+ * The packs the engine's abilities need beyond its core: moves (dash to a unit, swap, attach), ground objects (traps,
42
+ * walls, barrels), holds (hold to charge, aimed channels), pools (energy, fury, heat, ammo), vision (marks, camouflage,
43
+ * true sight), changing abilities (grant, swap, steal) and mana burn. Each does nothing to a unit that uses none of it
44
+ */
45
+ export function abilityPacks(game: Game<any>): void {
46
+ for (const pack of [movesPack(), groundObjectsPack(), holdsPack(), resourcesPack(), visionPack(), abilityChangesPack(), manaBurnPack()])
47
+ packOnce(game, pack)
48
+ }
49
+
50
+ /**
51
+ * A caster for an example: a mage of plenty of mana, quick to get it back, who knows `abilities` (each at level 1). It
52
+ * strikes nothing of its own (no reach to find a fight), so what it does is what it is told
53
+ */
54
+ export const caster = (id: string, name: string, abilities: readonly string[], own: Partial<UnitTypeDef> = {}): UnitTypeDef => ({
55
+ id,
56
+ name,
57
+ kind: 'unit',
58
+ radius: 0.45,
59
+ stats: { health: 1200, healthRegen: 4, mana: 1000, manaRegen: 60, attackDamage: 30, attackSpeed: 1, attackRange: 6, moveSpeed: 4.2 },
60
+ sight: 12,
61
+ attacks: [{ damagePoint: 0.5, acquisitionRange: 0, projectileSpeed: 20 }],
62
+ abilities: [...abilities],
63
+ ...own
64
+ })
65
+
66
+ /**
67
+ * Something to cast on: a footman of a side who never strikes back (no attack), holds its ground until moved, and heals
68
+ * fast, so a cast can be tried again and again
69
+ */
70
+ export const sparring = (id: string, name: string, own: Partial<UnitTypeDef> = {}): UnitTypeDef => ({
71
+ id,
72
+ name,
73
+ kind: 'unit',
74
+ radius: 0.45,
75
+ stats: { health: 900, healthRegen: 30, mana: 300, manaRegen: 5, moveSpeed: 3.2, armor: 5 },
76
+ sight: 8,
77
+ ...own
78
+ })
79
+
80
+ /** The point `distance` from `from` toward `to` */
81
+ export function toward(from: Vec2, to: Vec2, distance: number): Vec2 {
82
+ const dx = to.x - from.x
83
+ const dz = to.z - from.z
84
+ const length = exactMath.hypot(dx, dz) || 1
85
+ return { x: from.x + (dx / length) * distance, z: from.z + (dz / length) * distance }
86
+ }
87
+
88
+ /** The living enemy of `of` nearest to it, of the other side */
89
+ export function nearestEnemy(world: World, of: Entity): Entity | undefined {
90
+ let best: Entity | undefined
91
+ let far = Infinity
92
+ for (const unit of world.units.values()) {
93
+ if (!unit.alive || unit.hidden || !world.areEnemies(of.owner, unit.owner)) continue
94
+ const d = exactMath.hypot(unit.pos.x - of.pos.x, unit.pos.z - of.pos.z)
95
+ if (d < far) {
96
+ far = d
97
+ best = unit
98
+ }
99
+ }
100
+ return best
101
+ }
102
+
103
+ /**
104
+ * Casts an ability as a player would, aimed for them: the unit named `by` (`caster` when not said) casts `ability` on
105
+ * the unit named `at`, or at `point`. With neither, it aims as the ability asks: an enemy's ability at the nearest
106
+ * enemy, an ally's at the unit named `ally` (itself where there is none), a point or a direction at the nearest enemy,
107
+ * nothing at nothing. Cast again while its window is open, it is the recast
108
+ */
109
+ export function castFor(world: World, player: number, ability: string, options: { by?: string; at?: string; point?: Vec2 } = {}): void {
110
+ const unit = world.named(options.by ?? 'caster')
111
+ if (!unit || unit.owner !== player || !world.registry.abilities.has(ability)) return
112
+ const def: AbilityDef = world.registry.ability(ability)
113
+ const kind = aimKind(world.registry, def)
114
+ const named = options.at ? world.named(options.at) : undefined
115
+ const friendly = def.targets && !def.targets.enemies && (def.targets.allies || def.targets.self)
116
+ const target = named ?? (friendly ? (world.named('ally') ?? unit) : nearestEnemy(world, unit))
117
+ const at = options.point ?? target?.pos
118
+ switch (kind) {
119
+ case 'none':
120
+ return unit.order('cast', { ability })
121
+ case 'unit':
122
+ return target ? unit.order('cast', { ability, target: target.id }) : undefined
123
+ case 'point':
124
+ case 'direction':
125
+ return at ? unit.order('cast', { ability, point: { x: at.x, z: at.z } }) : undefined
126
+ case 'vector': {
127
+ // from beside the target, across it
128
+ if (!at) return
129
+ const side = { x: at.x - (at.z - unit.pos.z) * 0.6, z: at.z + (at.x - unit.pos.x) * 0.6 }
130
+ return unit.order('cast', { ability, point: side, to: { x: at.x, z: at.z } })
131
+ }
132
+ }
133
+ }
134
+
135
+ /** The command a card button sends: casts an ability for its player (`castFor`) */
136
+ export const cast = defineCommand(
137
+ 'abilities.cast',
138
+ { ability: string, by: optional(string), at: optional(string), point: optional(point) },
139
+ (world, player, { ability, by, at, point }) => castFor(world, player, ability, { by, at, point })
140
+ )
141
+
142
+ /** Lets go of a held ability (hold to charge, release to fire) of the unit named `by`, toward the nearest enemy */
143
+ export const release = defineCommand('abilities.release', { by: optional(string) }, (world, player, { by }) => {
144
+ const unit = world.named(by ?? 'caster')
145
+ if (!unit || unit.owner !== player) return
146
+ const enemy = nearestEnemy(world, unit)
147
+ releaseHold(world, unit, enemy ? { x: enemy.pos.x, z: enemy.pos.z } : undefined)
148
+ })
149
+
150
+ /** Each example's start by its id, for the reset command to lay one out again */
151
+ export const starts: Record<string, (world: World) => void> = {}
152
+
153
+ /** Takes every unit away and lays the example out again as it started (`starts`) */
154
+ export const reset = defineCommand('abilities.reset', { example: string }, (world, _player, { example }) => {
155
+ const start = starts[example]
156
+ if (!start) return
157
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
158
+ start(world)
159
+ })
160
+
161
+ /** Puts a unit down for a side, named when it is to be found again, facing `facing` (radians; toward +z is 0) */
162
+ export function put(world: World, type: string, owner: number, at: Vec2, name?: string, facing = 0): Entity {
163
+ return world.spawn(type, { at, owner, facing, ...(name && { name }) })
164
+ }
165
+
166
+ /** The usual lay-out of a family: the caster at the south, an ally by it, enemies in a row to the north */
167
+ export function lineUp(world: World, casterType: string, enemyType: string, count = 3, ally?: string): void {
168
+ put(world, casterType, PLAYER, { x: 24, z: 18 }, 'caster')
169
+ if (ally) put(world, ally, PLAYER, { x: 21.5, z: 17 }, 'ally')
170
+ for (let i = 0; i < count; i++)
171
+ put(world, enemyType, ENEMY, { x: 24 + (i - (count - 1) / 2) * 2.5, z: 26 + (i % 2) * 1.2 }, `enemy${i + 1}`, Math.PI)
172
+ }
173
+
174
+ /** Registers the shared commands once on the game */
175
+ export function kitRules(game: Game<any>): void {
176
+ game.command(cast)
177
+ game.command(release)
178
+ game.command(reset)
179
+ }