@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,402 @@
1
+ /**
2
+ * The heroes, tested with no page on a small flat map: their callings and innates, what each of their abilities does,
3
+ * the altars that train them and the tavern that hires them
4
+ */
5
+ import {
6
+ applyBuff,
7
+ callingsOf,
8
+ checkRules,
9
+ damaged,
10
+ type Entity,
11
+ fighting,
12
+ hasBuff,
13
+ heroOf,
14
+ hire,
15
+ isPhasing,
16
+ learn,
17
+ MAP_CREEPS,
18
+ setHeroLevel,
19
+ shieldOf,
20
+ statsOf,
21
+ type World
22
+ } from '@v12-engine/engine'
23
+ import { run, TEST_PLAYERS, testWorld } from '@v12-engine/engine/testing'
24
+ import { describe, expect, it } from 'vitest'
25
+ import { game as melee } from '../src/game'
26
+ import { harvested, heroes, HOLLOW_HEROES, KINGDOM_HEROES } from '../src/melee/heroes'
27
+
28
+ const rules = melee.toRules()
29
+
30
+ const ME = 1
31
+ const FOE = 2
32
+ const NEUTRAL = 0
33
+ /** The camps' creeps: everyone's enemy */
34
+ const CREEPS = MAP_CREEPS
35
+
36
+ /** What each hero is, as the plan has it: its callings and its innate */
37
+ const PLANNED: Record<string, { callings: Record<string, number>; innate: string }> = {
38
+ oathbreaker: { callings: { vanguard: 3, keeper: 1 }, innate: 'bulwark' },
39
+ dawnPriest: { callings: { keeper: 3, arcanist: 1 }, innate: 'mercysEcho' },
40
+ stormcaller: { callings: { arcanist: 3, binder: 2 }, innate: 'staticCharge' },
41
+ galeHunter: { callings: { ranger: 3, roamer: 1 }, innate: 'steadyAim' },
42
+ gravelord: { callings: { reaver: 3, vanguard: 1, keeper: 1 }, innate: 'bloodTithe' },
43
+ rimeWitch: { callings: { binder: 3, arcanist: 2 }, innate: 'shatter' },
44
+ duskblade: { callings: { stalker: 3, roamer: 2 }, innate: 'shadowStep' },
45
+ boneReaver: { callings: { roamer: 3, reaver: 2 }, innate: 'harvest' }
46
+ }
47
+
48
+ async function arena(): Promise<World> {
49
+ const world = await testWorld(rules, {
50
+ size: 64,
51
+ players: [...TEST_PLAYERS.map((player) => ({ ...player })), { id: CREEPS, name: 'Creeps', team: 1000, color: [0.42, 0.36, 0.3], creeps: true }]
52
+ })
53
+ for (const player of [ME, FOE]) world.player(player).resources.gold = 10000
54
+ return world
55
+ }
56
+
57
+ /** Picks no fight of its own: what it does is the test's */
58
+ function calm(unit: Entity): Entity {
59
+ unit.get(fighting)!.passive = true
60
+ return unit
61
+ }
62
+
63
+ /** A hero of a type at a level, every point it has put into what it learns, in order, full of mana; picking no fights */
64
+ function hero(world: World, type: string, owner = ME, at = { x: 30, z: 30 }, level = 10): Entity {
65
+ const made = calm(world.spawn(type, owner, at))
66
+ setHeroLevel(world, made, level)
67
+ for (let round = 0; round < 3; round++) for (const id of made.def.hero!.learns) learn(world, made, id)
68
+ made.mana = made.maxMana
69
+ return made
70
+ }
71
+
72
+ /** Units of the enemy's that stand where they are put and pick no fights: what the abilities are tried on */
73
+ const target = (world: World, at: { x: number; z: number }, type = 'footman', owner = FOE): Entity => calm(world.spawn(type, owner, at))
74
+
75
+ /** A tick on, so that a player knows of what was just made and may aim at it */
76
+ const seen = (world: World) => run(world, 0.1)
77
+
78
+ const cast = (world: World, caster: Entity, ability: string, aim: { target?: Entity; point?: { x: number; z: number } } = {}) =>
79
+ world.issue({
80
+ type: 'cast',
81
+ player: caster.owner,
82
+ entities: [caster.id],
83
+ ability,
84
+ ...(aim.target && { target: aim.target.id }),
85
+ ...(aim.point && { point: { x: aim.point.x, z: aim.point.z } })
86
+ })
87
+
88
+ const hurt = (unit: Entity) => unit.health < unit.maxHealth
89
+
90
+ describe('the heroes', () => {
91
+ it('are eight, each with its callings and innate as planned, of the game’s callings, the rules whole', () => {
92
+ expect(checkRules(rules).filter((problem) => problem.level === 'error')).toEqual([])
93
+ expect(heroes.map((def) => def.id).sort()).toEqual(Object.keys(PLANNED).sort())
94
+ const known = new Set(callingsOf(rules).map((calling) => calling.id))
95
+ for (const def of heroes) {
96
+ expect(def.hero!.callings, def.id).toEqual(PLANNED[def.id].callings)
97
+ expect(def.hero!.innate, def.id).toBe(PLANNED[def.id].innate)
98
+ for (const calling of Object.keys(def.hero!.callings!)) expect(known.has(calling), calling).toBe(true)
99
+ // three abilities and an ultimate, one of them at least a passive or a toggle, and two of the hero spells
100
+ const learns = def.hero!.learns.map((id) => rules.abilities!.find((ability) => ability.id === id)!)
101
+ expect(learns, def.id).toHaveLength(4)
102
+ expect(learns.filter((ability) => ability.ultimate), def.id).toHaveLength(1)
103
+ expect(
104
+ learns.some((ability) => ability.target === 'passive' || ability.effects.some((effect) => effect.type === 'toggle')),
105
+ def.id
106
+ ).toBe(true)
107
+ expect(def.hero!.spells, def.id).toHaveLength(2)
108
+ }
109
+ })
110
+
111
+ it('of the Kingdom are trained at its altar, of the Restless at theirs, and the tavern hires any', async () => {
112
+ const unit = (id: string) => rules.units.find((def) => def.id === id)!
113
+ expect(unit('altar').trains).toEqual(KINGDOM_HEROES)
114
+ expect(unit('darkAltar').trains).toEqual(HOLLOW_HEROES)
115
+ expect([...unit('tavern').hires!].sort()).toEqual([...KINGDOM_HEROES, ...HOLLOW_HEROES].sort())
116
+ const world = await arena()
117
+ const tavern = world.spawn('tavern', NEUTRAL, { x: 30, z: 30 })
118
+ world.spawn('footman', ME, { x: 27, z: 30 })
119
+ const gold = world.player(ME).resources.gold
120
+ // a hero of the other side, hired once
121
+ const hired = hire(world, tavern, 'duskblade', ME)!
122
+ expect(hired.def.id).toBe('duskblade')
123
+ expect(heroOf(hired)?.level).toBe(1)
124
+ expect(world.player(ME).resources.gold).toBe(gold - 425)
125
+ expect(hire(world, tavern, 'duskblade', ME)).toBeUndefined()
126
+ expect(hire(world, tavern, 'oathbreaker', ME)?.def.id).toBe('oathbreaker')
127
+ expect(hire(world, tavern, 'stormcaller', ME)?.def.id).toBe('stormcaller')
128
+ // three heroes at most, however they come
129
+ expect(hire(world, tavern, 'gravelord', ME)).toBeUndefined()
130
+ })
131
+ })
132
+
133
+ describe('what their abilities do', () => {
134
+ it('Oathbreaker: charges in and taunts, shields allies, makes a last stand', async () => {
135
+ const world = await arena()
136
+ const me = hero(world, 'oathbreaker')
137
+ const ally = hero(world, 'dawnPriest', ME, { x: 28, z: 32 })
138
+ const foe = target(world, { x: 36, z: 30 })
139
+ seen(world)
140
+ cast(world, me, 'oathCharge', { point: foe.pos })
141
+ run(world, 1.5)
142
+ expect(hurt(foe)).toBe(true)
143
+ expect(hasBuff(foe, 'taunted')).toBe(true)
144
+ cast(world, me, 'shieldwall')
145
+ run(world, 0.2)
146
+ expect(shieldOf(me)).toBeGreaterThan(0)
147
+ run(world, 1)
148
+ cast(world, me, 'lastStand')
149
+ run(world, 0.5)
150
+ expect(hasBuff(me, 'lastStand')).toBe(true)
151
+ expect(hasBuff(foe, 'taunted')).toBe(true)
152
+ void ally
153
+ })
154
+
155
+ it('Oathbreaker’s Bulwark: he takes a fifth of what an allied hero near him takes; Iron Resolve strikes back', async () => {
156
+ const world = await arena()
157
+ const me = hero(world, 'oathbreaker')
158
+ const ally = hero(world, 'galeHunter', ME, { x: 33, z: 30 })
159
+ run(world, 0.5)
160
+ expect(hasBuff(ally, 'bulwarkWard')).toBe(true)
161
+ const [mine, theirs] = [me.health, ally.health]
162
+ world.damage(ally, 100, undefined, { damageType: 'true' })
163
+ expect(theirs - ally.health).toBeCloseTo(80, 0)
164
+ expect(mine - me.health).toBeCloseTo(20, 0)
165
+ // far from him, none of it
166
+ world.place(ally, { x: 55, z: 55 })
167
+ run(world, 2)
168
+ const far = ally.health
169
+ world.damage(ally, 100, undefined, { damageType: 'true' })
170
+ expect(far - ally.health).toBeCloseTo(100, 0)
171
+ // a footman striking him is struck back
172
+ const footman = world.spawn('footman', FOE, { x: 31, z: 30 })
173
+ run(world, 4)
174
+ expect(hurt(footman)).toBe(true)
175
+ })
176
+
177
+ it('Dawn Priest: heals, burns and cleanses, and Sanctuary saves a life; Mercy’s Echo shields what overflows', async () => {
178
+ const world = await arena()
179
+ const me = hero(world, 'dawnPriest')
180
+ const ally = hero(world, 'oathbreaker', ME, { x: 33, z: 30 })
181
+ const foe = target(world, { x: 34, z: 32 })
182
+ run(world, 0.5)
183
+ // the aura
184
+ expect(hasBuff(ally, 'dawnlight')).toBe(true)
185
+ // healing a whole ally: what spills over is a shield
186
+ cast(world, me, 'holyLight', { target: ally })
187
+ run(world, 1)
188
+ expect(shieldOf(ally)).toBeGreaterThan(0)
189
+ applyBuff(world, ally, 'iceRooted', 5, foe)
190
+ cast(world, me, 'cleansingFlame', { point: { x: 34, z: 31 } })
191
+ run(world, 1)
192
+ expect(hasBuff(ally, 'iceRooted')).toBe(false)
193
+ expect(hurt(foe)).toBe(true)
194
+ cast(world, me, 'sanctuary', { target: ally })
195
+ run(world, 1)
196
+ expect(hasBuff(ally, 'sanctuary')).toBe(true)
197
+ world.damage(ally, 100000, foe, { damageType: 'true' })
198
+ expect(ally.alive).toBe(true)
199
+ expect(ally.health).toBeCloseTo(ally.maxHealth * 0.3, -1)
200
+ expect(hasBuff(ally, 'hallowed')).toBe(true)
201
+ })
202
+
203
+ it('Stormcaller: a tornado, lightning that leaps, a storm that throws up who crosses it; every fourth spell free and arcing', async () => {
204
+ const world = await arena()
205
+ const me = hero(world, 'stormcaller')
206
+ const foes = [target(world, { x: 35, z: 30 }), target(world, { x: 36, z: 32 }), target(world, { x: 37, z: 29 })]
207
+ seen(world)
208
+ cast(world, me, 'tornado', { point: foes[0].pos })
209
+ run(world, 2.5)
210
+ expect(hurt(foes[0])).toBe(true)
211
+ expect(me.buffs.find((buff) => buff.id === 'staticCharge')?.stacks).toBe(1)
212
+ for (const foe of foes) foe.health = foe.maxHealth
213
+ cast(world, me, 'chainLightning', { target: foes[0] })
214
+ run(world, 1.5)
215
+ expect(foes.filter(hurt).length).toBeGreaterThanOrEqual(2)
216
+ // Gale Ward: what his abilities hurt is slowed
217
+ expect(foes.some((foe) => hasBuff(foe, 'buffeted'))).toBe(true)
218
+ cast(world, me, 'eyeOfTheStorm', { point: { x: 36, z: 30 } })
219
+ run(world, 1.5)
220
+ expect(hasBuff(foes[0], 'stormSlow')).toBe(true)
221
+ // that was the third: the fourth costs nothing, and arcs to one more
222
+ expect(me.buffs.find((buff) => buff.id === 'staticCharge')?.stacks).toBe(3)
223
+ // once Chain Lightning is ready again
224
+ run(world, 9)
225
+ const outside = target(world, { x: 30, z: 36 })
226
+ const near = target(world, { x: 33, z: 37 })
227
+ seen(world)
228
+ me.mana = 100
229
+ const struck: number[] = []
230
+ world.events.on(damaged, ({ target: hit, ability }) => {
231
+ if (ability === 'staticCharge') struck.push(hit.id)
232
+ })
233
+ cast(world, me, 'chainLightning', { target: outside })
234
+ run(world, 1.5)
235
+ expect(me.mana).toBeGreaterThanOrEqual(100)
236
+ expect(struck).toEqual([near.id])
237
+ expect(hasBuff(me, 'staticCharge')).toBe(false)
238
+ })
239
+
240
+ it('Gale Hunter: a bolt through a line, a roll and a harder shot, a storm of arrows; Steady Aim standing still', async () => {
241
+ const world = await arena()
242
+ const me = hero(world, 'galeHunter')
243
+ const foes = [target(world, { x: 35, z: 30 }), target(world, { x: 39, z: 30 })]
244
+ seen(world)
245
+ const range = statsOf(world, me).attackRange
246
+ run(world, 2.7)
247
+ expect(me.buffs.find((buff) => buff.id === 'steadied')?.stacks).toBe(4)
248
+ expect(statsOf(world, me).attackRange).toBeCloseTo(range + 2, 3)
249
+ cast(world, me, 'piercingBolt', { point: { x: 45, z: 30 } })
250
+ run(world, 1.5)
251
+ expect(foes.every(hurt)).toBe(true)
252
+ cast(world, me, 'tumble', { point: { x: 30, z: 26 } })
253
+ run(world, 0.3)
254
+ expect(me.pos.z).toBeLessThan(28)
255
+ expect(hasBuff(me, 'tumbleShot')).toBe(true)
256
+ // standing, the aim steadies again; walking loses it (an order made him pick fights again: not now)
257
+ calm(me)
258
+ world.issue({ type: 'order', player: ME, entities: [me.id], order: { type: 'stop' } })
259
+ run(world, 2)
260
+ expect(hasBuff(me, 'steadied')).toBe(true)
261
+ world.issue({ type: 'order', player: ME, entities: [me.id], order: { type: 'move', point: { x: 10, z: 10 } } })
262
+ run(world, 1)
263
+ expect(hasBuff(me, 'steadied')).toBe(false)
264
+ // back into range, and the arrows fall
265
+ cast(world, me, 'arrowstorm', { point: foes[1].pos })
266
+ expect(run(world, 4, () => hasBuff(foes[1], 'pinned'))).toBeLessThan(4)
267
+ })
268
+
269
+ it('Gravelord: pays with health, drains, and raises the dead; his aura for his allies', async () => {
270
+ const world = await arena()
271
+ const me = hero(world, 'gravelord')
272
+ const ally = world.spawn('ghoul', ME, { x: 32, z: 32 })
273
+ const foe = target(world, { x: 34, z: 30 })
274
+ run(world, 0.5)
275
+ expect(me.maxMana).toBe(0)
276
+ expect(hasBuff(ally, 'deathless')).toBe(true)
277
+ const health = me.health
278
+ cast(world, me, 'graveLash', { target: foe })
279
+ run(world, 0.8)
280
+ expect(hurt(foe)).toBe(true)
281
+ expect(hasBuff(foe, 'graveChill')).toBe(true)
282
+ // 60 of health paid for it
283
+ expect(me.health).toBeLessThan(health)
284
+ foe.health = foe.maxHealth
285
+ me.health = 300
286
+ cast(world, me, 'siphon', { target: foe })
287
+ run(world, 3.5)
288
+ expect(hurt(foe)).toBe(true)
289
+ // the dead around him rise
290
+ for (let i = 0; i < 4; i++) world.damage(target(world, { x: 28 + i, z: 27 }), 100000, me, { damageType: 'true' })
291
+ run(world, 0.5)
292
+ cast(world, me, 'armyOfTheGrave')
293
+ run(world, 1)
294
+ expect(world.query({ type: 'skeleton', owner: ME }).length).toBeGreaterThan(0)
295
+ expect(hasBuff(ally, 'graveRallied')).toBe(true)
296
+ })
297
+
298
+ it('Rime Witch: freezes, roots, a blizzard that freezes who stays; Shatter for her allies’ blows', async () => {
299
+ const world = await arena()
300
+ const me = hero(world, 'rimeWitch')
301
+ const ally = hero(world, 'oathbreaker', ME, { x: 31, z: 32 })
302
+ const foes = [target(world, { x: 35, z: 30 }), target(world, { x: 35, z: 33 })]
303
+ seen(world)
304
+ cast(world, me, 'rimeBolt', { target: foes[0] })
305
+ run(world, 0.6)
306
+ expect(hasBuff(foes[0], 'frozen')).toBe(true)
307
+ // her own blow does not shatter it; an ally's does, harder
308
+ world.damage(foes[0], 10, me, { damageType: 'true' })
309
+ expect(hasBuff(foes[0], 'frozen')).toBe(true)
310
+ const before = foes[0].health
311
+ world.damage(foes[0], 100, ally, { damageType: 'true' })
312
+ expect(before - foes[0].health).toBeCloseTo(135, 0)
313
+ expect(hasBuff(foes[0], 'frozen')).toBe(false)
314
+ cast(world, me, 'grasp', { point: foes[1].pos })
315
+ run(world, 0.8)
316
+ expect(hasBuff(foes[1], 'iceRooted')).toBe(true)
317
+ cast(world, me, 'whiteout', { point: { x: 35, z: 31 } })
318
+ run(world, 3)
319
+ expect(foes.some((foe) => hasBuff(foe, 'frozen'))).toBe(true)
320
+ })
321
+
322
+ it('Duskblade: in the shadows after three quiet seconds, a mark that bursts, a fade through a crowd, an eclipse', async () => {
323
+ const world = await arena()
324
+ const me = hero(world, 'duskblade')
325
+ const foe = target(world, { x: 36, z: 30 })
326
+ seen(world)
327
+ run(world, 3.5)
328
+ expect(hasBuff(me, 'shadowed')).toBe(true)
329
+ // hurt, out of them
330
+ world.damage(me, 10, foe)
331
+ expect(hasBuff(me, 'shadowed')).toBe(false)
332
+ cast(world, me, 'duskMark', { target: foe })
333
+ run(world, 1)
334
+ expect(hasBuff(foe, 'duskMark')).toBe(true)
335
+ const marked = foe.health
336
+ run(world, 4)
337
+ // it burst as it ran out
338
+ expect(foe.health).toBeLessThan(marked)
339
+ cast(world, me, 'fade')
340
+ run(world, 0.3)
341
+ expect(isPhasing(world, me)).toBe(true)
342
+ run(world, 3)
343
+ const other = target(world, { x: 34, z: 34 })
344
+ seen(world)
345
+ cast(world, me, 'eclipse', { target: other })
346
+ run(world, 0.5)
347
+ expect(hasBuff(other, 'eclipsed')).toBe(true)
348
+ expect(Math.hypot(me.pos.x - other.pos.x, me.pos.z - other.pos.z)).toBeLessThan(3)
349
+ })
350
+
351
+ it('Duskblade: her first strike from the shadows deals more and silences', async () => {
352
+ const world = await arena()
353
+ const me = hero(world, 'duskblade')
354
+ run(world, 3.5)
355
+ expect(hasBuff(me, 'shadowed')).toBe(true)
356
+ me.get(fighting)!.passive = false
357
+ const foe = target(world, { x: 31.5, z: 30 })
358
+ const blows: number[] = []
359
+ world.events.on(damaged, ({ target: hit, source, amount }) => {
360
+ if (hit === foe && source === me) blows.push(amount)
361
+ })
362
+ // her first strike: out of the shadows, the foe silenced
363
+ run(world, 3, () => !hasBuff(me, 'shadowed'))
364
+ run(world, 0.1)
365
+ expect(hasBuff(foe, 'hushed')).toBe(true)
366
+ run(world, 3)
367
+ expect(blows.length).toBeGreaterThanOrEqual(3)
368
+ })
369
+
370
+ it('Bone Reaver: cleaves, gorges on a creep, a pit of bones; Harvest grows with kills and halves on death', async () => {
371
+ const world = await arena()
372
+ const me = hero(world, 'boneReaver')
373
+ const foe = target(world, { x: 32, z: 30 })
374
+ const creep = calm(world.spawn('ogre', CREEPS, { x: 29, z: 32 }))
375
+ // what is nobody's, not a camp's: left alone
376
+ const nobodys = calm(world.spawn('ogre', NEUTRAL, { x: 31, z: 32 }))
377
+ seen(world)
378
+ cast(world, me, 'boneCleaver')
379
+ run(world, 0.8)
380
+ expect(hurt(foe)).toBe(true)
381
+ expect(hurt(creep)).toBe(true)
382
+ expect(hurt(nobodys)).toBe(false)
383
+ const health = statsOf(world, me).health
384
+ // what is left of it, Gorge's to finish
385
+ creep.health = 300
386
+ cast(world, me, 'gorge', { target: creep })
387
+ run(world, 1.5)
388
+ expect(creep.alive).toBe(false)
389
+ expect(me.get(harvested)?.count).toBe(1)
390
+ expect(statsOf(world, me).health).toBe(health + 6)
391
+ // a hero is five
392
+ const rival = hero(world, 'galeHunter', FOE, { x: 40, z: 40 }, 1)
393
+ world.damage(rival, 100000, me, { damageType: 'true' })
394
+ expect(me.get(harvested)?.count).toBe(6)
395
+ expect(me.buffs.find((buff) => buff.id === 'harvest')?.stacks).toBe(6)
396
+ cast(world, me, 'bonePit', { point: me.pos })
397
+ run(world, 0.5)
398
+ expect(hasBuff(me, 'bonePitFrenzy')).toBe(true)
399
+ world.damage(me, 100000, foe, { damageType: 'true' })
400
+ expect(me.get(harvested)?.count).toBe(3)
401
+ })
402
+ })
@@ -2,14 +2,16 @@
2
2
  import { checkRules, createRegistry, createWorldFromMap, generateMap, type World } from '@v12-engine/engine'
3
3
  import { describe, expect, it } from 'vitest'
4
4
  import { DEFAULT_MAP, mapNamed, maps } from '../src/maps'
5
- import { rules, start } from '../src/rules'
5
+ import { game as melee } from '../src/game'
6
6
  import { checkMeleeMap, HALLS } from '../src/melee/maps'
7
7
 
8
- /** A game on a map, started as the page starts it: you are the first player, the computer the others */
8
+ const rules = melee.toRules()
9
+
10
+ /** A game on a map, started as the page starts it (`game.onStart`): you are the first player, the computer the others */
9
11
  async function game(name = DEFAULT_MAP): Promise<World> {
10
- const map = mapNamed(name)
11
- const world = await createWorldFromMap({ map, game: rules, seed: 1 })
12
- start(world, { map, player: map.players[0].id })
12
+ const map = await mapNamed(name)
13
+ const world = await createWorldFromMap({ map, rules, seed: 1 })
14
+ await melee.toStart()?.(world, { map, player: map.players[0].id })
13
15
  return world
14
16
  }
15
17
 
@@ -20,7 +22,7 @@ describe('the rules', () => {
20
22
 
21
23
  it('give every player with a start a hall and workers, and seat the computer at all but you', async () => {
22
24
  const world = await game()
23
- const starts = mapNamed().players.filter((player) => player.start)
25
+ const starts = (await mapNamed()).players.filter((player) => player.start)
24
26
  for (const player of starts) {
25
27
  const own = [...world.index.ownedBy(player.id)]
26
28
  expect(
@@ -42,10 +44,10 @@ describe('the rules', () => {
42
44
  world.dispose()
43
45
  }, 30_000)
44
46
 
45
- it('read every map, each one melee can be played on', () => {
47
+ it('read every map, each one melee can be played on', async () => {
46
48
  for (const [name, map] of Object.entries(maps))
47
49
  expect(
48
- checkMeleeMap(map).filter((problem) => problem.error),
50
+ checkMeleeMap(await map()).filter((problem) => problem.error),
49
51
  name
50
52
  ).toEqual([])
51
53
  })
@@ -1,2 +1,32 @@
1
- /** The game, whole: melee's rules and how it looks (`src/melee/game.ts`, its view in `src/melee/view`) */
2
- export { meleeGame as game } from './melee/game'
1
+ /**
2
+ * The game, whole, as every part of the project takes it (`createGame`): melee's rules (`src/melee`), its look
3
+ * (`src/melee/view`, fetched as the page first plays it), its maps (`src/maps.ts`) and what it does as a game starts.
4
+ * The page, the server, the editor and the tests all take this
5
+ */
6
+ import { maps } from './maps'
7
+ import { meleeGame } from './melee/game'
8
+
9
+ export const game = meleeGame.maps(maps)
10
+
11
+ /**
12
+ * What the game does once a world is made from a map, before anyone plays. Every player with a start is given a hall
13
+ * and five workers by the rules themselves; in the page, the computer plays everyone but you, each against the other
14
+ * teams' starts. A server seats its computers itself (`server.ts`), so there is no one to seat here
15
+ */
16
+ game.onStart((world, { map, player }) => {
17
+ if (!map || player === undefined) return
18
+ const players = map.players.filter((other) => other.start)
19
+ for (const other of players) {
20
+ if (other.id === player) continue
21
+ const enemyBases = players.filter((rival) => rival.team !== other.team).map((rival) => rival.start!)
22
+ // remembered, so that a game loaded from a save of this one plays on against them
23
+ world.computers.seat(other.id, { enemyBases, settings: { army: { firstAttack: 360, waveSize: 6 } } })
24
+ }
25
+ })
26
+
27
+ // @if desktop
28
+ // its look with what the desktop app adds to it: a Windowed setting, Quit on the pause menu
29
+ game.view(() => Promise.all([import('./melee/view'), import('./desktop')]).then(([{ meleeView }, { desktopView }]) => desktopView(meleeView)))
30
+ // @end
31
+ // (saved while it runs, melee's rules and its look take their new versions with no new page: `src/melee/index.ts`
32
+ // and `src/melee/view/index.ts` say so. A change here loads the page again)
@@ -21,8 +21,8 @@ export type BayPaths = never
21
21
  /** The lights of the map that are named, for `client.lights.named(...)` */
22
22
  export type BayLights = never
23
23
 
24
- /** The effects of the map that are named, for `client.mapEffects.named(...)` */
25
- export type BayEffects = never
24
+ /** The effects of the map that are named, for `client.mapFx.named(...)` */
25
+ export type BayFx = never
26
26
 
27
27
  /** The ids of the regions, points and paths by name: what a field that names one keeps (it is handed the name) */
28
28
  export const BayIds = { regions: { }, points: { }, paths: { } } as const