@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
@@ -1,4 +1,18 @@
1
- import { type GameMap, type GameModule, hotRules, objectiveVictory, prefabsFrom, withData, withHeroes, type World } from '@v12-engine/engine'
1
+ import {
2
+ defaultHeroSpellSet,
3
+ defineCellLayer,
4
+ foliagePack,
5
+ type GameMap,
6
+ presetFoliageStyles,
7
+ type Rules,
8
+ hotRules,
9
+ objectiveVictory,
10
+ prefabsFrom,
11
+ layData,
12
+ heroesPack,
13
+ useDefaults,
14
+ type World
15
+ } from '@v12-engine/engine'
2
16
  import { ENEMY, PLAYER } from './ids'
3
17
  import { mapKeyOf, maps } from './maps'
4
18
  import { homecoming, homecomingListeners, homecomingSequences, outpost, redHand, reinforcements } from './missions/homecoming'
@@ -6,7 +20,7 @@ import { siege, siegeListeners, siegeSequences } from './missions/siege'
6
20
  import { parley, vargasMen, warlord, warlordListeners, warlordSequences } from './missions/warlord'
7
21
  import { countKills, kills } from './kills'
8
22
  import { tiles } from './tiles'
9
- import { abilities, buffs, effects, items, units } from './units'
23
+ import { abilities, buffs, effects, items, itemTags, units } from './units'
10
24
  import { nextWave, reportWaves, siegeWaves, waveBeaten, waveComing, waveListeners, waves, wavesFile, waveTimers } from './waves'
11
25
  import dataFile from './data.json'
12
26
  import watchpost from './prefabs/watchpost.prefab.json'
@@ -31,75 +45,97 @@ export { builtWaves, readWaves, siegeWaves, type Wave, WAVE_UNITS, wavesFile, wa
31
45
  * over the tower
32
46
  * - missions: objectives, sequences of dialog and cinematics, areas of the map the rules watch, timers, prefabs, and
33
47
  * computer players that are mission brains (`@v12-engine/engine/ai`), each listed so that a save finds it by its id
48
+ * - two of the engine's hero spells for the knight (`useDefaults`: of the set, only those he brings; `all` would take
49
+ * the nine, to give heroes in the Data panel), and the engine's item tags its items say
34
50
  */
35
- export const campaign: GameModule = withData(
36
- {
37
- // the maps of this game say they are its own by this id: change it, and change theirs (`"game"`) with it
38
- id: 'my-game',
39
- name: 'My game',
40
- units,
41
- abilities,
42
- buffs,
43
- items,
44
- effects,
45
- resources: [{ id: 'gold', name: 'Gold', start: 300 }],
46
- currency: 'gold',
47
- // its hero, who earns levels and is brought back at an altar, and the creeps' camps (`withHeroes`)
48
- packs: [withHeroes()],
49
- tiles,
50
- // the maps it ships: a save of a game on one keeps only what changed of its ground
51
- maps: Object.values(maps),
52
- // the outpost in code, and the designers' prefabs (`prefabs/*.prefab.json`), as the rules may spawn them too
53
- prefabs: [outpost, ...prefabsFrom({ 'watchpost.prefab.json': watchpost }, { prefix: 'campaign.' })],
54
- brains: [redHand, vargasMen],
55
- commands: [nextWave],
56
- listeners: [...waveListeners, countKills, ...homecomingListeners, ...siegeListeners, ...warlordListeners],
57
- timers: [...waveTimers, reinforcements],
58
- files: [wavesFile],
59
- sequences: [...homecomingSequences, ...siegeSequences, ...warlordSequences],
60
- // what it keeps of its own: the waves' state, each tower's kills (sent to everyone, pinned over it by the view), and
61
- // the answer at Varga's gate
62
- components: [waves, kills, parley],
63
- events: [waveBeaten, waveComing],
64
- editor: {
65
- // the player and the Red Hand, always: the editor sets where they start, not who they are
66
- players: [
67
- { id: PLAYER, name: 'Aldric', team: 1, color: [0.2, 0.45, 0.95] },
68
- { id: ENEMY, name: 'The Red Hand', team: 2, color: [0.9, 0.2, 0.18], controller: 'computer' }
51
+ export const campaign: Rules = layData(
52
+ useDefaults(
53
+ {
54
+ // the maps of this game say they are its own by this id: change it, and change theirs (`"game"`) with it
55
+ id: 'my-game',
56
+ name: 'My game',
57
+ units,
58
+ abilities,
59
+ buffs,
60
+ items,
61
+ // what its items' tags are called and look like (the shop's filters and search)
62
+ tags: itemTags,
63
+ effects,
64
+ resources: [{ id: 'gold', name: 'Gold', start: 300 }],
65
+ currency: 'gold',
66
+ // its hero, who earns levels and is brought back at an altar, and the creeps' camps (`heroesPack`); the grass of
67
+ // its maps, which hides who stands in the tall of it and burns (`foliagePack`)
68
+ packs: [heroesPack(), foliagePack({ layer: 'grass' })],
69
+ tiles,
70
+ // grass painted on the maps, each value a style of it: a meadow, and tall grass that hides who stands in it.
71
+ // How each looks is the view's (`ground.cellLayers.grass`), by the same names (`presetFoliage`)
72
+ cellLayers: [
73
+ defineCellLayer({
74
+ id: 'grass',
75
+ name: 'Grass',
76
+ color: [0.4, 0.72, 0.28],
77
+ values: [
78
+ { value: 1, name: 'Meadow', foliage: presetFoliageStyles.meadow },
79
+ { value: 2, name: 'Tall grass', color: [0.25, 0.5, 0.18], foliage: presetFoliageStyles.tallGrass }
80
+ ]
81
+ })
69
82
  ],
70
- playersNote: 'Aldric is you. The Red Hand is the computer, as each mission says.',
71
- // where the game names a unit type beyond the map: the waves that send it (Used by, Find references)
72
- references: (ref, _map, registry) =>
73
- 'type' in ref
74
- ? siegeWaves({ registry }).flatMap((wave, i) =>
75
- wave.units.some((unit) => unit.type === ref.type) ? [{ what: `wave ${i + 1}`, where: 'waves.json' }] : []
76
- )
77
- : []
83
+ // the maps it ships: a save of a game on one keeps only what changed of its ground
84
+ maps: Object.values(maps),
85
+ // the outpost in code, and the designers' prefabs (`prefabs/*.prefab.json`), as the rules may spawn them too
86
+ prefabs: [outpost, ...prefabsFrom({ 'watchpost.prefab.json': watchpost }, { prefix: 'campaign.' })],
87
+ brains: [redHand, vargasMen],
88
+ commands: [nextWave],
89
+ listeners: [...waveListeners, countKills, ...homecomingListeners, ...siegeListeners, ...warlordListeners],
90
+ timers: [...waveTimers, reinforcements],
91
+ files: [wavesFile],
92
+ sequences: [...homecomingSequences, ...siegeSequences, ...warlordSequences],
93
+ // what it keeps of its own: the waves' state, each tower's kills (sent to everyone, pinned over it by the view), and
94
+ // the answer at Varga's gate
95
+ components: [waves, kills, parley],
96
+ events: [waveBeaten, waveComing],
97
+ editor: {
98
+ // the player and the Red Hand, always: the editor sets where they start, not who they are
99
+ players: [
100
+ { id: PLAYER, name: 'Aldric', team: 1, color: [0.2, 0.45, 0.95] },
101
+ { id: ENEMY, name: 'The Red Hand', team: 2, color: [0.9, 0.2, 0.18], controller: 'computer' }
102
+ ],
103
+ playersNote: 'Aldric is you. The Red Hand is the computer, as each mission says.',
104
+ // where the game names a unit type beyond the map: the waves that send it (Used by, Find references)
105
+ references: (ref, _map, registry) =>
106
+ 'type' in ref
107
+ ? siegeWaves({ registry }).flatMap((wave, i) =>
108
+ wave.units.some((unit) => unit.type === ref.type) ? [{ what: `wave ${i + 1}`, where: 'waves.json' }] : []
109
+ )
110
+ : []
111
+ },
112
+ victory: objectiveVictory,
113
+ report: (world) => reportWaves(world),
114
+ // a save made before a patch is loaded after it (`saves.migrate` takes the step)
115
+ saves: { version: 1 },
116
+ // what the editor's Data panel may change, kept in `data.json`: numbers and prices, never what a unit is
117
+ editable: {
118
+ units: (def) =>
119
+ def.kind === 'building'
120
+ ? ['name', { path: 'stats.health', min: 1 }, 'stats.armor', { path: 'cost.gold', kind: 'integer', min: 0 }, 'buildTime']
121
+ : def.kind === 'unit'
122
+ ? [
123
+ 'name',
124
+ { path: 'stats.health', min: 1 },
125
+ 'stats.attackDamage',
126
+ 'stats.attackSpeed',
127
+ 'stats.armor',
128
+ 'stats.moveSpeed',
129
+ { path: 'cost.gold', kind: 'integer', min: 0 }
130
+ ]
131
+ : null,
132
+ abilities: [{ path: 'cooldown', min: 0 }, { path: 'manaCost', min: 0 }, 'description'],
133
+ items: [{ path: 'cost.gold', kind: 'integer', min: 0 }, 'description']
134
+ }
78
135
  },
79
- victory: objectiveVictory,
80
- report: (world) => reportWaves(world),
81
- // a save made before a patch is loaded after it (`saves.migrate` takes the step)
82
- saves: { version: 1 },
83
- // what the editor's Data panel may change, kept in `data.json`: numbers and prices, never what a unit is
84
- editable: {
85
- units: (def) =>
86
- def.kind === 'building'
87
- ? ['name', { path: 'stats.health', min: 1 }, 'stats.armor', { path: 'cost.gold', kind: 'integer', min: 0 }, 'buildTime']
88
- : def.kind === 'unit'
89
- ? [
90
- 'name',
91
- { path: 'stats.health', min: 1 },
92
- 'stats.attackDamage',
93
- 'stats.attackSpeed',
94
- 'stats.armor',
95
- 'stats.moveSpeed',
96
- { path: 'cost.gold', kind: 'integer', min: 0 }
97
- ]
98
- : null,
99
- abilities: [{ path: 'cooldown', min: 0 }, { path: 'manaCost', min: 0 }, 'description'],
100
- items: [{ path: 'cost.gold', kind: 'integer', min: 0 }, 'description']
101
- }
102
- },
136
+ [defaultHeroSpellSet()],
137
+ { heroSpells: { only: ['barrier', 'purify'] } }
138
+ ),
103
139
  dataFile
104
140
  )
105
141
 
@@ -3,7 +3,7 @@ import { defineComponent, defineListener } from '@v12-engine/engine'
3
3
  import { ENEMY, PLAYER } from './ids'
4
4
 
5
5
  /** How many of the Red Hand a tower of the fort has finished off. Everyone sees it: the view puts it over the tower */
6
- export const kills = defineComponent<number>('campaign.kills', { replicate: 'everyone' })
6
+ export const kills = defineComponent<number>({ id: 'campaign.kills', replicate: 'everyone' })
7
7
 
8
8
  /** One of the Red Hand that dies counts for the building of the player's that killed it */
9
9
  export const countKills = defineListener({
@@ -65,18 +65,18 @@ export const redHand = missionBrain({
65
65
  })
66
66
 
67
67
  /** Every so often, two raiders more at the camp: the brain sends them */
68
- export const reinforcements = defineTimer('campaign.reinforcements', (world) => {
68
+ export const reinforcements = defineTimer({ id: 'campaign.reinforcements', run: (world) => {
69
69
  if (world.winner !== null) return
70
70
  const camp = world.map.point('camp')
71
71
  if (!enemyBuildings(world).length) return
72
72
  for (const dx of [-1.5, 1.5]) world.spawn(UNIT.raider, ENEMY, { x: camp.x + dx, z: camp.z + 1 })
73
- })
73
+ } })
74
74
 
75
75
  /** The Red Hand's buildings, standing */
76
76
  export const enemyBuildings = (world: World) => [...world.index.ownedBy(ENEMY)].filter((entity) => entity.alive && entity.def.kind === 'building')
77
77
 
78
78
  /** The intro, then Brann's briefing: a sequence, skippable a line at a time, saved halfway like everything else */
79
- export const briefing = defineSequence('campaign.briefing', [
79
+ export const briefing = defineSequence({ id: 'campaign.briefing', steps: [
80
80
  // the screen lets the first tick pass before it draws: the intro is the first thing seen, the game waiting
81
81
  wait(0),
82
82
  cinematic('campaign.intro', { hold: true, wait: 'ended' }),
@@ -85,17 +85,17 @@ export const briefing = defineSequence('campaign.briefing', [
85
85
  { speaker: BRANN, text: MSG.homeStart, seconds: 5 },
86
86
  { speaker: ALDRIC, text: MSG.homeCamp, seconds: 5 }
87
87
  ])
88
- ])
88
+ ] })
89
89
 
90
90
  /** At the ford, a word, and the camp shown for a moment: whoever of the player's walks in first */
91
- export const atTheFord = defineSequence('campaign.atTheFord', [
91
+ export const atTheFord = defineSequence({ id: 'campaign.atTheFord', steps: [
92
92
  waitFor(enteredArea, (_world, { area, entity }) => area === FORD && entity.owner === PLAYER),
93
93
  (world) => {
94
94
  const camp = world.map.point('camp')
95
95
  world.reveal(world.player(PLAYER).team, camp, 12, 10)
96
96
  },
97
97
  ...talk(PLAYER, [{ speaker: BRANN, text: MSG.homeFord, seconds: 5 }])
98
- ])
98
+ ] })
99
99
 
100
100
  /** The caravan found: its gold, and the optional objective done */
101
101
  export const caravanFound = defineListener({
@@ -130,7 +130,7 @@ export const buildingFell = defineListener({
130
130
  }
131
131
  })
132
132
 
133
- const wellDone = defineSequence('campaign.homeDone', talk(PLAYER, [{ speaker: ALDRIC, text: MSG.homeDone, seconds: 4 }]))
133
+ const wellDone = defineSequence({ id: 'campaign.homeDone', steps: talk(PLAYER, [{ speaker: ALDRIC, text: MSG.homeDone, seconds: 4 }]) })
134
134
 
135
135
  export const homecomingSequences = [briefing, atTheFord, wellDone]
136
136
  export const homecomingListeners = [caravanFound, buildingFell]
@@ -18,13 +18,13 @@ export const keepTowers = defineObjective({ id: 'campaign.towers', text: MSG.obj
18
18
  export const GATE = 'gate'
19
19
 
20
20
  /** The briefing: what is coming, and what the gate does */
21
- export const siegeBriefing = defineSequence('campaign.siegeBriefing', [
21
+ export const siegeBriefing = defineSequence({ id: 'campaign.siegeBriefing', steps: [
22
22
  wait(0.5),
23
23
  ...talk(PLAYER, [
24
24
  { speaker: BRANN, text: MSG.siegeStart, seconds: 5 },
25
25
  { speaker: ALDRIC, text: MSG.siegeGate, seconds: 5 }
26
26
  ])
27
- ])
27
+ ] })
28
28
 
29
29
  /** A wave sets out: the gate shuts, the camp flares (an effect fired by the rules, seen by every screen) */
30
30
  export const waveSetsOut = defineListener({
@@ -32,13 +32,13 @@ export const waveSetsOut = defineListener({
32
32
  id: 'campaign.waveSetsOut',
33
33
  run(world, { x, z }) {
34
34
  if (!world.objectives.has(PLAYER, holdOut.id)) return
35
- world.effect('spark-burst', { at: { x, z }, params: { size: 2 } })
35
+ world.fx('spark-burst', { at: { x, z }, params: { size: 2 } })
36
36
  if (setGate(world, GATE, false)) world.message(PLAYER, MSG.gateShut, 'info')
37
37
  }
38
38
  })
39
39
 
40
- const halfway = defineSequence('campaign.siegeHalfway', talk(PLAYER, [{ speaker: BRANN, text: MSG.siegeHalfway, seconds: 4 }]))
41
- const beforeTheLast = defineSequence('campaign.siegeLast', talk(PLAYER, [{ speaker: ALDRIC, text: MSG.siegeLast, seconds: 5 }]))
40
+ const halfway = defineSequence({ id: 'campaign.siegeHalfway', steps: talk(PLAYER, [{ speaker: BRANN, text: MSG.siegeHalfway, seconds: 4 }]) })
41
+ const beforeTheLast = defineSequence({ id: 'campaign.siegeLast', steps: talk(PLAYER, [{ speaker: ALDRIC, text: MSG.siegeLast, seconds: 5 }]) })
42
42
 
43
43
  /** A wave beaten: the gate opens, the objective moves along; after the last, the mission is won */
44
44
  export const waveHeld = defineListener({
@@ -66,7 +66,7 @@ export const fortBuildingFell = defineListener({
66
66
  id: 'campaign.fortBuildingFell',
67
67
  run(world, { entity }) {
68
68
  if (entity.owner !== PLAYER || entity.def.kind !== 'building' || !world.objectives.has(PLAYER, holdOut.id)) return
69
- world.effect('explosion', { at: { x: entity.pos.x, z: entity.pos.z }, params: { size: entity.def.id === UNIT.wall ? 0.8 : 1.5 } })
69
+ world.fx('explosion', { at: { x: entity.pos.x, z: entity.pos.z }, params: { size: entity.def.id === UNIT.wall ? 0.8 : 1.5 } })
70
70
  if (entity.def.id === UNIT.tower) world.objectives.fail(PLAYER, keepTowers.id)
71
71
  if (entity.def.id === UNIT.hall) world.objectives.fail(PLAYER, keepTheHall.id, MSG.hallFell)
72
72
  }
@@ -44,7 +44,7 @@ export const PARLEY_CHOICE = 'parley'
44
44
  * What the player answered at the gate, kept in the world (so a save keeps it): what the campaign carries on as the
45
45
  * mission is won (`carry.ts`). Nothing for a talk skipped, or not had yet
46
46
  */
47
- export const parley = defineComponent<{ choice?: 'duel' | 'charge' }>('campaign.parley', { default: () => ({}) })
47
+ export const parley = defineComponent<{ choice?: 'duel' | 'charge' }>({ id: 'campaign.parley', default: () => ({}) })
48
48
 
49
49
  /** Varga's men: they hold the hold, and his guard stays by him */
50
50
  export const vargasMen = missionBrain({
@@ -53,27 +53,27 @@ export const vargasMen = missionBrain({
53
53
  })
54
54
 
55
55
  /** The briefing, and the trader pointed out */
56
- export const passBriefing = defineSequence('campaign.passBriefing', [
56
+ export const passBriefing = defineSequence({ id: 'campaign.passBriefing', steps: [
57
57
  wait(0.5),
58
58
  ...talk(PLAYER, [
59
59
  { speaker: BRANN, text: MSG.passStart, seconds: 5 },
60
60
  { speaker: BRANN, text: MSG.passShop, seconds: 5 }
61
61
  ])
62
- ])
62
+ ] })
63
63
 
64
64
  /**
65
65
  * At the gate: the talk, as a cinematic the rules play for the player (`playCinematic`, handed who talks), the game
66
66
  * held while it plays. The choice in it comes back as a command of the player's (`cinematicChoice`), which
67
67
  * `theChoice` hears; a cinematic with no data to hand over is the `cinematic` step, waited for or not
68
68
  */
69
- export const atTheGate = defineSequence('campaign.atTheGate', [
69
+ export const atTheGate = defineSequence({ id: 'campaign.atTheGate', steps: [
70
70
  waitFor(enteredArea, (_world, { area, entity }) => area === GATE && entity.owner === PLAYER),
71
71
  (world) => {
72
72
  const hero = world.named('hero')
73
73
  const warlord = world.named('warlord')
74
74
  playCinematic(world, PARLEY, { to: PLAYER, hold: true, params: { hero: hero?.id, warlord: warlord?.id } })
75
75
  }
76
- ])
76
+ ] })
77
77
 
78
78
  /** Varga's guards, by the names the map gives them */
79
79
  const guards = (world: World) => ['guard1', 'guard2', 'guard3'].flatMap((name) => world.named(name) ?? [])
@@ -95,15 +95,14 @@ export const theChoice = defineListener({
95
95
  if (warlord?.alive) applyBuff(world, warlord, 'rallied', 60)
96
96
  world.sequences.start(duel)
97
97
  } else {
98
- if (hero?.alive)
99
- for (const man of guards(world)) world.issue({ type: 'order', player: ENEMY, entities: [man.id], order: { type: 'attack', target: hero.id } })
98
+ if (hero?.alive) for (const man of guards(world)) man.order('attack', hero)
100
99
  world.sequences.start(charge)
101
100
  }
102
101
  }
103
102
  })
104
103
 
105
- const duel = defineSequence('campaign.duel', talk(PLAYER, [{ speaker: ALDRIC, text: MSG.passDuel, seconds: 4 }]))
106
- const charge = defineSequence('campaign.charge', talk(PLAYER, [{ speaker: BRANN, text: MSG.passCharge, seconds: 4 }]))
104
+ const duel = defineSequence({ id: 'campaign.duel', steps: talk(PLAYER, [{ speaker: ALDRIC, text: MSG.passDuel, seconds: 4 }]) })
105
+ const charge = defineSequence({ id: 'campaign.charge', steps: talk(PLAYER, [{ speaker: BRANN, text: MSG.passCharge, seconds: 4 }]) })
107
106
 
108
107
  /** The prisoners' cells reached: they are the player's from then on */
109
108
  export const cellsOpened = defineListener({
@@ -119,13 +118,13 @@ export const cellsOpened = defineListener({
119
118
  }
120
119
  })
121
120
 
122
- const freed = defineSequence('campaign.freed', talk(PLAYER, [{ speaker: BRANN, text: MSG.passPrisoners, seconds: 4 }]))
121
+ const freed = defineSequence({ id: 'campaign.freed', steps: talk(PLAYER, [{ speaker: BRANN, text: MSG.passPrisoners, seconds: 4 }]) })
123
122
 
124
123
  /**
125
124
  * Varga falls: the outro plays, the game held while it does, and then it is won (the end screen comes once the
126
125
  * outro is over, as the game stands still under it). Aldric falls: it is lost
127
126
  */
128
- export const theEnd = defineSequence('campaign.theEnd', [
127
+ export const theEnd = defineSequence({ id: 'campaign.theEnd', steps: [
129
128
  waitFor(entityDied, (_world, { entity }) => entity.def.id === UNIT.warlord),
130
129
  wait(1.5),
131
130
  cinematic('campaign.outro', { hold: true }),
@@ -136,7 +135,7 @@ export const theEnd = defineSequence('campaign.theEnd', [
136
135
  objectives.complete(PLAYER, keepHero.id)
137
136
  if (objectives.get(PLAYER, freePrisoners.id).status === 'open') objectives.fail(PLAYER, freePrisoners.id)
138
137
  }
139
- ])
138
+ ] })
140
139
 
141
140
  export const heroFell = defineListener({
142
141
  event: entityDied,
@@ -168,8 +167,8 @@ export function warlord(world: World, fresh: boolean, difficulty = 'normal'): vo
168
167
  const purse = world.player(ENEMY).resources
169
168
  for (const key of Object.keys(purse)) purse[key] = 0
170
169
  const varga = world.named('warlord')
171
- if (varga && difficulty === 'easy') varga.hp = Math.round(varga.maxHp * 0.75)
172
- if (varga && difficulty === 'hard') world.spawn(UNIT.brute, ENEMY, { x: varga.pos.x + 2, z: varga.pos.z + 1 }, { name: 'guard4' })
170
+ if (varga && difficulty === 'easy') varga.health = Math.round(varga.maxHealth * 0.75)
171
+ if (varga && difficulty === 'hard') world.spawn(UNIT.brute, { at: { x: varga.pos.x + 2, z: varga.pos.z + 1 }, owner: ENEMY, name: 'guard4' })
173
172
  world.computers.seat(ENEMY, { brain: vargasMen })
174
173
  world.sequences.start(passBriefing)
175
174
  world.sequences.start(atTheGate)
@@ -1,6 +1,7 @@
1
1
  import { defineCampaign } from '@v12-engine/client/app'
2
+ import { campaignGame } from './game'
2
3
  import campaignFile from './campaign.json'
3
- import { campaign, campaignMap, homecoming, homecomingCarry, type RoadData, siege, siegeCarriedIn, warlord, warlordCarry } from './index'
4
+ import { campaignMap, homecoming, homecomingCarry, type RoadData, siege, siegeCarriedIn, warlord, warlordCarry } from './index'
4
5
 
5
6
  /**
6
7
  * The campaign of three missions, Aldric carried from one to the next: `defineCampaign` gives the menu (where the
@@ -13,9 +14,9 @@ import { campaign, campaignMap, homecoming, homecomingCarry, type RoadData, sieg
13
14
  */
14
15
  export const theLongRoad = defineCampaign<RoadData>({
15
16
  file: campaignFile,
16
- game: campaign,
17
- // its look (and the client that draws it) fetched as its menu opens: not part of the start screen's code
18
- view: () => import('./view').then((module) => module.campaignView),
17
+ // the game (`./game.ts`): its rules, and its look (and the client that draws it) fetched as the campaign's menu
18
+ // opens, not part of the start screen's code
19
+ game: campaignGame,
19
20
  // a mission's map by the name the file gives it: `homestead` is `src/maps/homestead.vgmap.json`
20
21
  maps: campaignMap,
21
22
  missions: [
@@ -23,13 +24,13 @@ export const theLongRoad = defineCampaign<RoadData>({
23
24
  id: 'homecoming',
24
25
  // its debrief's words are the file's; its rating, by how quick it was, is code
25
26
  debrief: { rating: ({ seconds }) => (seconds < 600 ? 3 : seconds < 1200 ? 2 : 1) },
26
- start: (world, { fresh }) => homecoming(world, fresh),
27
+ onStart: (world, { fresh }) => homecoming(world, fresh),
27
28
  // the footmen who live through it, and whether the caravan was found
28
29
  carry: (world) => homecomingCarry(world)
29
30
  },
30
31
  {
31
32
  id: 'siege',
32
- start: (world, { fresh, map, progress }) => {
33
+ onStart: (world, { fresh, map, progress }) => {
33
34
  siege(world, fresh)
34
35
  // the footmen of Homecoming man the fort; a game loaded from a save has them already
35
36
  if (fresh) siegeCarriedIn(world, map, progress.data)
@@ -38,7 +39,7 @@ export const theLongRoad = defineCampaign<RoadData>({
38
39
  {
39
40
  id: 'warlord',
40
41
  // harder or easier with the campaign's difficulty, chosen in its menu
41
- start: (world, { fresh, difficulty }) => warlord(world, fresh, difficulty),
42
+ onStart: (world, { fresh, difficulty }) => warlord(world, fresh, difficulty),
42
43
  // the answer at the gate: the last mission, so the campaign keeps it for whatever comes after
43
44
  carry: (world) => warlordCarry(world)
44
45
  }
@@ -1,5 +1,15 @@
1
1
  import type { EffectFields } from '@v12-engine/engine'
2
- import { type AbilityDef, type BuffDef, defineEffect, type EffectKind, type ItemDef, type Scaled, scaled, type UnitTypeDef } from '@v12-engine/engine'
2
+ import {
3
+ type AbilityDef,
4
+ type BuffDef,
5
+ defaultItemTags,
6
+ defineEffect,
7
+ effectAmount,
8
+ type EffectType,
9
+ type ItemDef,
10
+ type TagDef,
11
+ type UnitTypeDef
12
+ } from '@v12-engine/engine'
3
13
  import { UNIT } from './ids'
4
14
 
5
15
  // the game's own effects, each with the fields it reads (`EffectTypes`)
@@ -62,9 +72,9 @@ export const archer: UnitTypeDef = {
62
72
  }
63
73
 
64
74
  /**
65
- * The hero: Aldric, the map names him `hero`. He grows with every level (`hero.growth`), learns three abilities with
66
- * his skill points, carries six items, and goes from one mission to the next with all of it (the campaign carries
67
- * him: `carryOut`, `carryIn`)
75
+ * The hero: Aldric, the map names him `hero`. He grows with every level (`hero.growth`), is a Vanguard and a Keeper
76
+ * (`hero.callings`), learns three abilities with his skill points, carries six items, and goes from one mission to
77
+ * the next with all of it (the campaign carries him: `carryOut`, `carryIn`)
68
78
  */
69
79
  export const knight: UnitTypeDef = {
70
80
  id: UNIT.knight,
@@ -86,7 +96,12 @@ export const knight: UnitTypeDef = {
86
96
  attacks: [{ damagePoint: 0.4, acquisitionRange: 8 }],
87
97
  hero: {
88
98
  growth: { stats: { health: 80, mana: 30, attackDamage: 3, armor: 3 } },
99
+ // what he is good at (the engine's callings, `defaultCallings`): in front, taking the blows; keeping his men up
100
+ callings: { vanguard: 3, keeper: 2 },
89
101
  learns: ['holyLight', 'warCry', 'judgement'],
102
+ // two of the engine's hero spells, on D and F (the set taken in `index.ts`, `defaultHeroSpellSet`): a shield, and
103
+ // a way out of a stun
104
+ spells: ['barrier', 'purify'],
90
105
  maxLevel: 10
91
106
  },
92
107
  inventory: 6,
@@ -274,7 +289,7 @@ export const goldmine: UnitTypeDef = {
274
289
  stats: { health: 1 },
275
290
  sight: 0,
276
291
  invulnerable: true,
277
- resource: { kind: 'gold', amount: 8000, slots: 1, entered: true }
292
+ resource: { id: 'gold', amount: 8000, slots: 1, entered: true }
278
293
  }
279
294
 
280
295
  export const units = [
@@ -304,23 +319,24 @@ export const buffs: BuffDef[] = [
304
319
  { id: 'rallied', name: 'Rallied', description: 'Hits 20% harder and 15% faster', percent: { attackDamage: 0.2, attackSpeed: 0.15 } }
305
320
  ]
306
321
 
307
- /** A number a level (`Scaled`): the level's, or the last for a level past the list */
308
- const atLevel = (amount: Scaled | undefined, level: number): number =>
309
- Array.isArray(amount) ? amount[Math.min(level, amount.length) - 1] : (amount ?? 0)
310
-
311
322
  /**
312
323
  * Effects the engine does not have, by the type an ability's effect names (`effects` of the module): a function given
313
324
  * the caster, the target and the level, with the world's own damage and healing, so that armour, death, bounty and
314
325
  * experience happen as for any hit
315
326
  */
316
- export const effects: EffectKind[] = [
327
+ export const effects: EffectType[] = [
317
328
  /** Judgement: the enemy struck, and the caster healed for what it took */
318
- defineEffect('campaign.judgement', ({ world, caster, target, level }, effect) => {
319
- if (!target?.alive) return
320
- const amount = scaled(world, caster, atLevel(effect.amount, level), effect.scaling, level)
321
- const before = target.hp
322
- world.damage(target, amount, caster, { damageType: 'magic' })
323
- world.heal(caster, Math.max(0, before - Math.max(target.hp, 0)), caster)
329
+ defineEffect({
330
+ type: 'campaign.judgement',
331
+ run: (context, effect) => {
332
+ const { world, caster, target } = context
333
+ if (!target?.alive) return
334
+ // its amount at its level, with what the caster's stats add, as the engine's damage works it out
335
+ const amount = effectAmount(context, effect, target)
336
+ const before = target.health
337
+ world.damage(target, amount, caster, { damageType: 'magic' })
338
+ world.heal(caster, Math.max(0, before - Math.max(target.health, 0)), caster)
339
+ }
324
340
  })
325
341
  ]
326
342
 
@@ -375,7 +391,10 @@ export const abilities: AbilityDef[] = [
375
391
  }
376
392
  ]
377
393
 
378
- /** What the trader sells (`trader.sells`) */
394
+ /**
395
+ * What the trader sells (`trader.sells`): each with its tags (what the shop's search and filters find it by) and the
396
+ * callings it suits (what the shop puts first for the knight)
397
+ */
379
398
  export const items: ItemDef[] = [
380
399
  {
381
400
  id: 'healingPotion',
@@ -383,8 +402,28 @@ export const items: ItemDef[] = [
383
402
  description: 'Drunk, heals 200. Three in the bottle',
384
403
  cost: { gold: 60 },
385
404
  charges: 3,
386
- ability: 'drinkPotion'
405
+ ability: 'drinkPotion',
406
+ tier: 'consumable',
407
+ tags: ['consumable', 'health']
408
+ },
409
+ {
410
+ id: 'steelSword',
411
+ name: 'Steel Sword',
412
+ description: '+12 attack damage',
413
+ cost: { gold: 180 },
414
+ bonus: { stats: { attackDamage: 12 } },
415
+ tags: ['attack'],
416
+ suits: ['vanguard']
387
417
  },
388
- { id: 'steelSword', name: 'Steel Sword', description: '+12 attack damage', cost: { gold: 180 }, bonus: { stats: { attackDamage: 12 } } },
389
- { id: 'swiftBoots', name: 'Swift Boots', description: '+0.8 move speed', cost: { gold: 120 }, bonus: { stats: { moveSpeed: 0.8 } } }
418
+ {
419
+ id: 'swiftBoots',
420
+ name: 'Swift Boots',
421
+ description: '+0.8 move speed',
422
+ cost: { gold: 120 },
423
+ bonus: { stats: { moveSpeed: 0.8 } },
424
+ tags: ['movement']
425
+ }
390
426
  ]
427
+
428
+ /** The tags its items say, named and coloured: the engine's own (`defaultItemTags`), those its items use */
429
+ export const itemTags: TagDef[] = defaultItemTags.filter((tag) => items.some((item) => item.tags?.includes(tag.id)))
@@ -0,0 +1,19 @@
1
+ import type { AttackLook } from '@v12-engine/client'
2
+
3
+ // The swings of the shared characters' rig (`UnitLook.attacks`): several each, so a fight does not play the same blow
4
+ // over and over. At random, never the same twice running; a critical strike ends in a big two-handed slice
5
+
6
+ /** A one-handed weapon's swings: a chop, a slice, a stab */
7
+ export const ONE_HANDED: AttackLook = {
8
+ clip: ['Melee_1H_Attack_Chop', 'Melee_1H_Attack_Slice_Diagonal', 'Melee_1H_Attack_Stab'],
9
+ order: 'random',
10
+ crit: 'Melee_2H_Attack_Slice'
11
+ }
12
+ /** A two-handed weapon's: a chop and a slice, in turn */
13
+ export const TWO_HANDED: AttackLook = { clip: ['Melee_2H_Attack_Chop', 'Melee_2H_Attack_Slice'], crit: 'Melee_2H_Attack_Chop' }
14
+ /** Two weapons': both together, and each alone */
15
+ export const DUAL_WIELD: AttackLook = {
16
+ clip: ['Melee_Dualwield_Attack_Chop', 'Melee_1H_Attack_Slice_Diagonal', 'Melee_1H_Attack_Stab'],
17
+ order: 'random',
18
+ crit: 'Melee_Dualwield_Attack_Chop'
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { Vector3 } from '@babylonjs/core/Maths/math.vector'
2
- import { type EffectHandle, type Rgb, Sparks, type SpellStyleFactory } from '@v12-engine/client'
2
+ import { type FxHandle, type Rgb, Sparks, type SpellStyleFactory } from '@v12-engine/client'
3
3
 
4
4
  /*
5
5
  * A spell style of the campaign's own (`styles` in the view, `style: 'campaign.holyFire'` on Judgement's look), as an
@@ -24,7 +24,7 @@ export const holyFire: SpellStyleFactory = (context) => {
24
24
  const sparks = new Sparks(context.fx)
25
25
  /** What is still to happen, by the time it happens at (`context.time`) */
26
26
  const later: { at: number; run: () => void }[] = []
27
- const burning: { until: number; stream: EffectHandle }[] = []
27
+ const burning: { until: number; stream: FxHandle }[] = []
28
28
 
29
29
  const groundAt = (x: number, z: number) => context.terrain.heightAt(x, z)
30
30