@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,1025 @@
1
+ import {
2
+ type AbilityDef,
3
+ type AttackDef,
4
+ applyBuff,
5
+ type BuffDef,
6
+ type BuffInstance,
7
+ defineComponent,
8
+ defineEffect,
9
+ defineHook,
10
+ type EffectDef,
11
+ type EffectType,
12
+ type EffectValueContext,
13
+ type Entity,
14
+ hasBuff,
15
+ heroOf,
16
+ holdsUp,
17
+ type HookDef,
18
+ invalidateStats,
19
+ removeBuff,
20
+ type UnitTypeDef,
21
+ type World
22
+ } from '@v12-engine/engine'
23
+
24
+ /*
25
+ * The heroes: four of the Kingdom, trained at its altar, and four of the Restless at theirs; a tavern hires any of them
26
+ * (items-and-spells plan, phase 4). Each has callings (what it is good at, 1 to 3), an innate it has from level 1 that
27
+ * changes how it is played, three abilities and an ultimate that changes the shape of a fight, and two of the engine's
28
+ * hero spells (`defaultHeroSpells`). What they do is said in general words (heroes, creeps, buildings, allies, kills),
29
+ * so a hero fits a game of bases, a campaign and one of lanes alike. What the engine's fields do not say is done here in
30
+ * code: the hooks and the effect at the end of this file
31
+ */
32
+
33
+ // the game's own effects, each with the fields it reads (`EffectTypes`)
34
+ declare module '@v12-engine/engine' {
35
+ interface EffectTypes {
36
+ /** Lightning that leaps from where a spell went off to the nearest other enemy within `radius`: Static Charge */
37
+ staticArc: { amount: number; perLevel: number; radius: number }
38
+ }
39
+ }
40
+
41
+ /** What every hero grows by, a level: the stats of a fighter up close, of one from afar, of a caster */
42
+ const GROWTH = {
43
+ melee: { stats: { health: 99, healthRegen: 0.1, mana: 55, manaRegen: 0.2, attackDamage: 3.5, armor: 4.3, magicResist: 2.05 }, percent: { attackSpeed: 0.02 } },
44
+ ranged: { stats: { health: 88, healthRegen: 0.09, mana: 40, manaRegen: 0.15, attackDamage: 3.4, armor: 3.8, magicResist: 1.5 }, percent: { attackSpeed: 0.03 } },
45
+ caster: { stats: { health: 85, healthRegen: 0.1, mana: 64, manaRegen: 0.2, attackDamage: 3, armor: 4, magicResist: 1.6 }, percent: { attackSpeed: 0.02 } }
46
+ }
47
+
48
+ /** What a hero of each side costs and takes to train or hire */
49
+ const HERO = { kind: 'unit', radius: 0.5, sight: 12, inventory: 6, cost: { gold: 425, lumber: 100 }, supply: 5, buildTime: 45, threat: 3 } as const
50
+
51
+ /** A hero's ranged attack of magic, a bolt that flies */
52
+ const BOLT: AttackDef = { damageType: 'magic', damagePoint: 0.4, acquisitionRange: 8, projectileSpeed: 18, targets: ['ground', 'air'] }
53
+
54
+ // ---- the Kingdom
55
+
56
+ export const oathbreaker: UnitTypeDef = {
57
+ ...HERO,
58
+ id: 'oathbreaker',
59
+ race: 'human',
60
+ name: 'Oathbreaker',
61
+ stats: { health: 660, healthRegen: 1.3, mana: 280, manaRegen: 1.6, attackDamage: 54, attackSpeed: 0.625, attackRange: 0.6, armor: 42, magicResist: 32, moveSpeed: 4.25 },
62
+ attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }],
63
+ hero: {
64
+ growth: GROWTH.melee,
65
+ callings: { vanguard: 3, keeper: 1 },
66
+ innate: 'bulwark',
67
+ learns: ['oathCharge', 'shieldwall', 'ironResolve', 'lastStand'],
68
+ spells: ['flash', 'waystone']
69
+ },
70
+ hotkey: 'O'
71
+ }
72
+
73
+ export const dawnPriest: UnitTypeDef = {
74
+ ...HERO,
75
+ id: 'dawnPriest',
76
+ race: 'human',
77
+ name: 'Dawn Priest',
78
+ stats: { health: 560, healthRegen: 1.1, mana: 480, manaRegen: 1.9, attackDamage: 46, attackSpeed: 0.65, attackRange: 6.5, armor: 22, magicResist: 32, moveSpeed: 4.1 },
79
+ attacks: [{ ...BOLT }],
80
+ hero: {
81
+ growth: GROWTH.caster,
82
+ callings: { keeper: 3, arcanist: 1 },
83
+ innate: 'mercysEcho',
84
+ innateLevels: [1, 4, 7, 10],
85
+ learns: ['holyLight', 'cleansingFlame', 'dawnlightAura', 'sanctuary'],
86
+ spells: ['secondWind', 'purify']
87
+ },
88
+ hotkey: 'D'
89
+ }
90
+
91
+ export const stormcaller: UnitTypeDef = {
92
+ ...HERO,
93
+ id: 'stormcaller',
94
+ race: 'human',
95
+ name: 'Stormcaller',
96
+ stats: { health: 570, healthRegen: 1.1, mana: 400, manaRegen: 2.1, attackDamage: 52, attackSpeed: 0.625, attackRange: 6.9, armor: 26, magicResist: 30, moveSpeed: 4.1 },
97
+ attacks: [{ ...BOLT }],
98
+ hero: {
99
+ growth: GROWTH.caster,
100
+ callings: { arcanist: 3, binder: 2 },
101
+ innate: 'staticCharge',
102
+ learns: ['tornado', 'chainLightning', 'galeWard', 'eyeOfTheStorm'],
103
+ spells: ['flash', 'purify']
104
+ },
105
+ hotkey: 'S'
106
+ }
107
+
108
+ export const galeHunter: UnitTypeDef = {
109
+ ...HERO,
110
+ id: 'galeHunter',
111
+ race: 'human',
112
+ name: 'Gale Hunter',
113
+ stats: { health: 540, healthRegen: 1, mana: 300, manaRegen: 1.4, attackDamage: 56, attackSpeed: 0.7, attackRange: 7.5, armor: 24, magicResist: 28, moveSpeed: 4.3 },
114
+ attacks: [{ damagePoint: 0.3, acquisitionRange: 9, projectileSpeed: 24, targets: ['ground', 'air'] }],
115
+ hero: {
116
+ growth: GROWTH.ranged,
117
+ callings: { ranger: 3, roamer: 1 },
118
+ innate: 'steadyAim',
119
+ learns: ['piercingBolt', 'tumble', 'huntsman', 'arrowstorm'],
120
+ spells: ['flash', 'barrier']
121
+ },
122
+ hotkey: 'G'
123
+ }
124
+
125
+ // ---- the Restless
126
+
127
+ export const gravelord: UnitTypeDef = {
128
+ ...HERO,
129
+ id: 'gravelord',
130
+ race: 'undead',
131
+ name: 'Gravelord',
132
+ // no mana: what he casts costs him health (Blood Tithe)
133
+ stats: { health: 700, healthRegen: 1.6, attackDamage: 54, attackSpeed: 0.65, attackRange: 0.6, armor: 38, magicResist: 32, moveSpeed: 4.4 },
134
+ attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }],
135
+ hero: {
136
+ growth: { ...GROWTH.melee, stats: { ...GROWTH.melee.stats, mana: 0, manaRegen: 0, health: 108, healthRegen: 0.14 } },
137
+ callings: { reaver: 3, vanguard: 1, keeper: 1 },
138
+ innate: 'bloodTithe',
139
+ innateLevels: [1, 4, 7, 10],
140
+ learns: ['graveLash', 'siphon', 'deathlessAura', 'armyOfTheGrave'],
141
+ spells: ['phantomStride', 'brand']
142
+ },
143
+ hotkey: 'G'
144
+ }
145
+
146
+ export const rimeWitch: UnitTypeDef = {
147
+ ...HERO,
148
+ id: 'rimeWitch',
149
+ race: 'undead',
150
+ name: 'Rime Witch',
151
+ stats: { health: 550, healthRegen: 0.9, mana: 420, manaRegen: 2, attackDamage: 44, attackSpeed: 0.6, attackRange: 6.5, armor: 24, magicResist: 30, moveSpeed: 4.1 },
152
+ attacks: [{ ...BOLT }],
153
+ hero: {
154
+ growth: GROWTH.caster,
155
+ callings: { binder: 3, arcanist: 2 },
156
+ innate: 'shatter',
157
+ innateLevels: [1, 5, 9],
158
+ learns: ['rimeBolt', 'grasp', 'hoarfrost', 'whiteout'],
159
+ spells: ['flash', 'barrier']
160
+ },
161
+ hotkey: 'R'
162
+ }
163
+
164
+ export const duskblade: UnitTypeDef = {
165
+ ...HERO,
166
+ id: 'duskblade',
167
+ race: 'undead',
168
+ name: 'Duskblade',
169
+ stats: { health: 560, healthRegen: 1.1, mana: 300, manaRegen: 1.5, attackDamage: 50, attackSpeed: 0.8, attackRange: 0.6, armor: 30, magicResist: 28, moveSpeed: 4.5 },
170
+ attacks: [{ damagePoint: 0.3, acquisitionRange: 8 }],
171
+ hero: {
172
+ growth: { ...GROWTH.melee, percent: { attackSpeed: 0.03 } },
173
+ callings: { stalker: 3, roamer: 2 },
174
+ innate: 'shadowStep',
175
+ innateLevels: [1, 5, 9],
176
+ learns: ['duskMark', 'fade', 'twinFangs', 'eclipse'],
177
+ spells: ['flash', 'brand']
178
+ },
179
+ hotkey: 'D'
180
+ }
181
+
182
+ export const boneReaver: UnitTypeDef = {
183
+ ...HERO,
184
+ id: 'boneReaver',
185
+ race: 'undead',
186
+ name: 'Bone Reaver',
187
+ stats: { health: 640, healthRegen: 1.4, mana: 260, manaRegen: 1.5, attackDamage: 55, attackSpeed: 0.65, attackRange: 0.6, armor: 36, magicResist: 30, moveSpeed: 4.35 },
188
+ attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }],
189
+ hero: {
190
+ growth: GROWTH.melee,
191
+ callings: { roamer: 3, reaver: 2 },
192
+ innate: 'harvest',
193
+ meter: 'harvest',
194
+ learns: ['boneCleaver', 'gorge', 'carrionHunger', 'bonePit'],
195
+ spells: ['reap', 'phantomStride']
196
+ },
197
+ hotkey: 'B'
198
+ }
199
+
200
+ /** The Kingdom's four, trained at its altar */
201
+ export const KINGDOM_HEROES = [oathbreaker, dawnPriest, stormcaller, galeHunter].map((hero) => hero.id)
202
+ /** The Restless' four, raised at theirs */
203
+ export const HOLLOW_HEROES = [gravelord, rimeWitch, duskblade, boneReaver].map((hero) => hero.id)
204
+
205
+ export const heroes: UnitTypeDef[] = [oathbreaker, dawnPriest, stormcaller, galeHunter, gravelord, rimeWitch, duskblade, boneReaver]
206
+
207
+ // ---- what they do
208
+
209
+ /** Stormcaller's every fourth spell, as the stacks of Static Charge are full: free, and leaping to one more */
210
+ const CHARGED: EffectDef = { type: 'charged', effects: [{ type: 'staticArc', amount: 40, perLevel: 10, radius: 8 }] }
211
+
212
+ /**
213
+ * Whom a hero's areas strike: its enemies, the camps' creeps among them (the creeps' player is everyone's enemy), and
214
+ * not what is nobody's (a shop, a critter). Said outright: a buff's area goes to allies when it says nothing
215
+ */
216
+ const FOES = { affects: 'enemies' } as const
217
+ /** An area's effect on foes that are not heroes */
218
+ const FOES_NOT_HEROES = { affects: 'enemies', where: (_context: EffectValueContext, unit: Entity) => !heroOf(unit) } as const
219
+
220
+ /** How many kills a hero's death counts as, to Harvest */
221
+ const HERO_HARVEST = 5
222
+ /** The most Harvest holds */
223
+ const MOST_HARVEST = 50
224
+
225
+ export const heroBuffs: BuffDef[] = [
226
+ // Oathbreaker
227
+ { id: 'bulwarkWard', name: 'Bulwark', description: 'An Oathbreaker near takes a fifth of the damage this hero takes', tags: ['guard'] },
228
+ { id: 'taunted', name: 'Taunted', description: 'Made to attack whoever taunted it', harmful: true, control: 'taunt', tags: ['control'] },
229
+ { id: 'shieldwall', name: 'Shieldwall', description: 'A shield takes the damage', duration: 4, shield: [80, 140, 200], shieldScaling: { abilityPower: 0.4 }, tags: ['shield'] },
230
+ { id: 'lastStand', name: 'Last Stand', description: 'Takes 30% less damage', stats: { incomingDamage: -0.3 }, tags: ['defence'] },
231
+ // Dawn Priest
232
+ { id: 'mercysEcho', name: 'Mercy’s Echo', description: 'Healing past full health becomes a shield', overheal: { buff: 'echoShield', part: [0.4, 0.6, 0.8, 1], most: 0.25 } },
233
+ { id: 'echoShield', name: 'Echo', description: 'Healing past full health, held as a shield', duration: 5, tags: ['shield'] },
234
+ { id: 'dawnlight', name: 'Dawnlight', description: 'Heals over time, and healing it gets is stronger', stats: { healthRegen: [1, 2, 3], incomingHealing: [0.1, 0.15, 0.2] } },
235
+ {
236
+ id: 'sanctuary',
237
+ name: 'Sanctuary',
238
+ description: 'A blow that would kill it leaves it at 30% health and untouchable for 2 seconds instead',
239
+ duration: 6,
240
+ passive: [{ type: 'onFatal', health: 0.3, cooldown: 6, effects: [{ type: 'buff', buff: 'hallowed', on: 'caster' }, { type: 'removeBuff', buff: 'sanctuary', on: 'caster' }] }],
241
+ tags: ['save']
242
+ },
243
+ { id: 'hallowed', name: 'Hallowed', description: 'Takes no damage', duration: 2, invulnerable: true, dispellable: false },
244
+ // Stormcaller
245
+ { id: 'staticCharge', name: 'Static Charge', description: 'Every fourth spell costs no mana and leaps to one more enemy', maxStacks: 3 },
246
+ { id: 'buffeted', name: 'Buffeted', description: 'Moves slower', harmful: true, duration: 1.5, percent: { moveSpeed: [-0.2, -0.3, -0.4] }, tags: ['slow'] },
247
+ { id: 'stormSlow', name: 'In the Storm', description: 'Moves 35% slower', harmful: true, percent: { moveSpeed: -0.35 }, tags: ['slow'] },
248
+ // Gale Hunter
249
+ {
250
+ id: 'steadyAim',
251
+ name: 'Steady Aim',
252
+ description: 'Standing still steadies the aim: more range and critical chance, lost as soon as it moves',
253
+ // half a second still at a time adds a stack, moving takes them off (`steadyTick`)
254
+ onTick: { every: 0.5, run: steadyTick }
255
+ },
256
+ { id: 'steadied', name: 'Steadied', description: '+0.5 range and +6% critical chance for every half second still', maxStacks: 4, perStack: true, stats: { attackRange: 0.5, critChance: 0.06 } },
257
+ {
258
+ id: 'tumbleShot',
259
+ name: 'Tumble',
260
+ description: 'The next attack deals more',
261
+ duration: 4,
262
+ passive: [{ type: 'onHit', effects: [{ type: 'damage', amount: [40, 70, 100], damageType: 'physical' }] }],
263
+ breaksOn: ['attack']
264
+ },
265
+ { id: 'pinned', name: 'Pinned', description: 'Moves 50% slower', harmful: true, percent: { moveSpeed: -0.5 }, tags: ['slow'] },
266
+ // Gravelord
267
+ { id: 'bloodTithe', name: 'Blood Tithe', description: 'Abilities cost health, not mana, and damage dealt heals', paysWithHealth: 1, stats: { omnivamp: [0.08, 0.1, 0.12, 0.14] } },
268
+ { id: 'graveChill', name: 'Grave Chill', description: 'Moves 30% slower', harmful: true, duration: 1.5, percent: { moveSpeed: -0.3 }, tags: ['slow'] },
269
+ { id: 'deathless', name: 'Deathless Aura', description: 'Moves faster and heals near a Gravelord', percent: { moveSpeed: [0.1, 0.15, 0.2] }, stats: { healthRegen: [1, 2, 3] } },
270
+ { id: 'graveRallied', name: 'Grave Rally', description: 'Its attacks heal it', duration: 8, stats: { lifeSteal: 0.2 } },
271
+ // Rime Witch
272
+ { id: 'shatterSense', name: 'Shatter', description: 'Who she holds takes more from the next blow of her allies, which ends the hold' },
273
+ { id: 'frozen', name: 'Frozen', description: 'Frozen solid: cannot do anything', harmful: true, stun: true, tags: ['hold', 'frost'] },
274
+ { id: 'iceRooted', name: 'Grasping Ice', description: 'Held by the ice: cannot move', harmful: true, root: true, tags: ['hold', 'frost'] },
275
+ { id: 'frostbitten', name: 'Frostbite', description: 'Moves slower', harmful: true, duration: 1.5, percent: { moveSpeed: [-0.2, -0.25, -0.3] }, tags: ['slow', 'frost'] },
276
+ { id: 'whiteoutChill', name: 'Whiteout', description: 'Moves 30% slower', harmful: true, percent: { moveSpeed: -0.3 }, tags: ['slow', 'frost'] },
277
+ {
278
+ id: 'frostbite',
279
+ name: 'Freezing',
280
+ description: 'Two seconds in the storm freezes it',
281
+ harmful: true,
282
+ duration: 0.5,
283
+ maxStacks: 8,
284
+ onMaxStacks: [{ type: 'buff', buff: 'frozen', duration: 1.5 }, { type: 'removeBuff', buff: 'frostbite' }],
285
+ tags: ['frost']
286
+ },
287
+ // Duskblade
288
+ {
289
+ id: 'shadowWatch',
290
+ name: 'Shadow Step',
291
+ description: 'Three seconds out of a fight hides it in the shadows',
292
+ onTick: { every: 0.25, run: (world, carrier, instance) => shadowTick(world, carrier, instance) }
293
+ },
294
+ {
295
+ id: 'shadowed',
296
+ name: 'In the Shadows',
297
+ description: 'Seen only from up close; its first strike from the shadows is a critical one and silences',
298
+ camouflage: 2.5,
299
+ passive: [
300
+ {
301
+ type: 'onHit',
302
+ effects: [
303
+ { type: 'damage', amount: 0, scaling: { attackDamage: [0.75, 1, 1.25] }, damageType: 'physical' },
304
+ { type: 'buff', buff: 'hushed', duration: 1.5 }
305
+ ]
306
+ }
307
+ ],
308
+ breaksOn: ['attack', 'cast']
309
+ },
310
+ { id: 'hushed', name: 'Silenced', description: 'Cannot use its abilities', harmful: true, silence: true, tags: ['hold'] },
311
+ {
312
+ id: 'duskMark',
313
+ name: 'Dusk Mark',
314
+ description: 'Takes more damage from everyone, is seen wherever it goes, and bursts when the mark runs out',
315
+ harmful: true,
316
+ duration: 4,
317
+ revealed: true,
318
+ stats: { incomingDamage: [0.1, 0.15, 0.2] },
319
+ onEnd: [{ type: 'damage', amount: [60, 100, 140], damageType: 'magic', radius: 0.5 }],
320
+ tags: ['mark']
321
+ },
322
+ { id: 'faded', name: 'Fade', description: 'Walks through units, faster, and seen only from up close', duration: 2, phasing: true, camouflage: 1.5, percent: { moveSpeed: 0.4 }, breaksOn: ['attack'] },
323
+ { id: 'eclipsed', name: 'Eclipse', description: 'Silenced, cannot be healed, and seen wherever it goes', harmful: true, duration: 3, silence: true, revealed: true, stats: { incomingHealing: -1 }, tags: ['hold', 'anti-heal'] },
324
+ // Bone Reaver
325
+ {
326
+ id: 'harvest',
327
+ name: 'Harvest',
328
+ description: 'More health and damage for every kill; a hero counts as five, and half of it is lost on death',
329
+ maxStacks: MOST_HARVEST,
330
+ // what it reaped is kept on the hero (`harvested`), through deaths and forms: the stacks and the stats are of it
331
+ onApply: (_world, carrier, instance) => void (instance.stacks = carrier.get(harvested)?.count ?? 0),
332
+ change: (_world, carrier) => {
333
+ const count = carrier.get(harvested)?.count ?? 0
334
+ return count ? { stats: { health: 6 * count, attackDamage: 1 * count } } : undefined
335
+ }
336
+ },
337
+ { id: 'carrionHunger', name: 'Carrion Hunger', description: 'Blows from up close strike those around the target too', cleave: { part: [0.3, 0.45, 0.6], radius: 2.5, angle: 1.6 } },
338
+ { id: 'bonePitFrenzy', name: 'Bone Pit', description: 'Attacks faster, and its attacks heal it', percent: { attackSpeed: 0.3 }, stats: { lifeSteal: 0.25 } }
339
+ ]
340
+
341
+ export const heroAbilities: AbilityDef[] = [
342
+ // ---- Oathbreaker: charges in, taunts, shields who stands behind him
343
+ {
344
+ id: 'bulwark',
345
+ name: 'Bulwark',
346
+ description: 'Takes a fifth of the damage allied heroes near him take, before their armor',
347
+ target: 'passive',
348
+ maxLevel: 1,
349
+ effects: [{ type: 'aura', buff: 'bulwarkWard', radius: 8, only: 'heroes' }]
350
+ },
351
+ {
352
+ id: 'oathCharge',
353
+ name: 'Oath Charge',
354
+ description: 'Charges to a spot: enemies around where he lands take 60, 100 or 140 (+50% attack damage) and are made to attack him for 1.25 to 1.75 seconds',
355
+ hotkey: 'Q',
356
+ target: 'point',
357
+ range: 8,
358
+ manaCost: 80,
359
+ cooldown: [14, 12, 10],
360
+ effects: [
361
+ {
362
+ type: 'dash',
363
+ distance: 8,
364
+ effects: [
365
+ { type: 'damage', amount: [60, 100, 140], scaling: { attackDamage: 0.5 }, damageType: 'physical', radius: 2.5, ...FOES },
366
+ { type: 'buff', buff: 'taunted', duration: [1.25, 1.5, 1.75], radius: 2.5, ...FOES }
367
+ ]
368
+ }
369
+ ]
370
+ },
371
+ {
372
+ id: 'shieldwall',
373
+ name: 'Shieldwall',
374
+ description: 'He and the allies around him get a shield of 80, 140 or 200 (+40% ability power) for 4 seconds',
375
+ hotkey: 'W',
376
+ target: 'none',
377
+ manaCost: 90,
378
+ cooldown: 16,
379
+ effects: [{ type: 'buff', buff: 'shieldwall', radius: 7, affects: 'allies' }]
380
+ },
381
+ {
382
+ id: 'ironResolve',
383
+ name: 'Iron Resolve',
384
+ description: 'Whoever strikes him from up close takes 12, 22 or 32 magic damage back',
385
+ target: 'passive',
386
+ effects: [{ type: 'onDamaged', melee: true, attack: true, effects: [{ type: 'damage', amount: [12, 22, 32], damageType: 'magic' }] }]
387
+ },
388
+ {
389
+ id: 'lastStand',
390
+ name: 'Last Stand',
391
+ description: 'Enemies around him are made to attack him for 2 seconds, and for 6 seconds allies who stay near him take 30% less damage',
392
+ hotkey: 'R',
393
+ target: 'none',
394
+ manaCost: 150,
395
+ cooldown: 100,
396
+ maxLevel: 1,
397
+ ultimate: true,
398
+ effects: [
399
+ { type: 'buff', buff: 'taunted', duration: 2, radius: 6, ...FOES },
400
+ { type: 'zone', follow: 'caster', duration: 6, radius: 7, effects: [{ type: 'buff', buff: 'lastStand', duration: 0.5, radius: 7, affects: 'allies' }] }
401
+ ]
402
+ },
403
+
404
+ // ---- Dawn Priest: heals, cleanses, and an ultimate that saves a life
405
+ {
406
+ id: 'mercysEcho',
407
+ name: 'Mercy’s Echo',
408
+ description: 'Healing she gives past full health becomes a shield on whoever she healed: 40% of it, up to all of it as she grows, at most a quarter of their health',
409
+ target: 'passive',
410
+ maxLevel: 4,
411
+ effects: [{ type: 'buff', buff: 'mercysEcho' }]
412
+ },
413
+ {
414
+ id: 'holyLight',
415
+ name: 'Holy Light',
416
+ description: 'Heals an ally for 200, 350 or 500 (+80% ability power)',
417
+ hotkey: 'Q',
418
+ target: 'unit',
419
+ targets: { allies: true, self: false },
420
+ manaCost: 65,
421
+ cooldown: 5,
422
+ range: 8,
423
+ castTime: 0.4,
424
+ effects: [{ type: 'heal', amount: [200, 350, 500], scaling: { abilityPower: 0.8 } }]
425
+ },
426
+ {
427
+ id: 'cleansingFlame',
428
+ name: 'Cleansing Flame',
429
+ description: 'A flame in an area: allies in it are freed of what holds them and healed 60, 100 or 140, enemies burn for as much (+50% ability power)',
430
+ hotkey: 'W',
431
+ target: 'point',
432
+ range: 8,
433
+ manaCost: 90,
434
+ cooldown: 10,
435
+ castTime: 0.3,
436
+ effects: [
437
+ { type: 'cleanse', radius: 3 },
438
+ { type: 'heal', amount: [60, 100, 140], scaling: { abilityPower: 0.4 }, radius: 3, affects: 'allies' },
439
+ { type: 'damage', amount: [60, 100, 140], scaling: { abilityPower: 0.5 }, damageType: 'magic', radius: 3, ...FOES }
440
+ ]
441
+ },
442
+ {
443
+ id: 'dawnlightAura',
444
+ name: 'Dawnlight Aura',
445
+ description: 'Allies near her heal 1, 2 or 3 a second, and healing they get is 10, 15 or 20% stronger',
446
+ target: 'passive',
447
+ effects: [{ type: 'aura', buff: 'dawnlight', radius: 9 }]
448
+ },
449
+ {
450
+ id: 'sanctuary',
451
+ name: 'Sanctuary',
452
+ description: 'Heals an ally 200 (+60% ability power) and for 6 seconds a blow that would kill it leaves it at 30% health and untouchable for 2 seconds',
453
+ hotkey: 'R',
454
+ target: 'unit',
455
+ targets: { allies: true, self: true },
456
+ range: 8,
457
+ manaCost: 150,
458
+ cooldown: 110,
459
+ maxLevel: 1,
460
+ ultimate: true,
461
+ effects: [
462
+ { type: 'heal', amount: 200, scaling: { abilityPower: 0.6 } },
463
+ { type: 'buff', buff: 'sanctuary' }
464
+ ]
465
+ },
466
+
467
+ // ---- Stormcaller: lightning and wind; every fourth spell free
468
+ {
469
+ id: 'staticCharge',
470
+ name: 'Static Charge',
471
+ description: 'Every fourth spell costs no mana, and lightning leaps from it to one more enemy for 40 (+10 a level)',
472
+ target: 'passive',
473
+ maxLevel: 1,
474
+ effects: [{ type: 'onCast', effects: [{ type: 'buff', buff: 'staticCharge', stack: true, on: 'caster' }] }]
475
+ },
476
+ {
477
+ id: 'tornado',
478
+ name: 'Tornado',
479
+ description:
480
+ 'Whips up a tornado that sets off in a line. Enemies it passes, on the ground and in the air, take 60, 85 or 110 (+35% ability power) and are thrown up for 1 second',
481
+ hotkey: 'Q',
482
+ target: 'direction',
483
+ manaCost: [60, 70, 80],
484
+ cooldown: [12, 11, 10],
485
+ charge: 'staticCharge',
486
+ effects: [
487
+ {
488
+ type: 'missile',
489
+ delay: 0.75,
490
+ speed: 11,
491
+ distance: [11, 12.5, 14],
492
+ radius: 1.4,
493
+ effects: [
494
+ { type: 'damage', amount: [60, 85, 110], scaling: { abilityPower: 0.35 } },
495
+ { type: 'buff', buff: 'knockedUp', duration: 1 }
496
+ ]
497
+ },
498
+ CHARGED
499
+ ]
500
+ },
501
+ {
502
+ id: 'chainLightning',
503
+ name: 'Chain Lightning',
504
+ description: 'Lightning strikes an enemy for 90, 130 or 170 (+50% ability power), then leaps to two more near it, a quarter less each time',
505
+ hotkey: 'W',
506
+ target: 'unit',
507
+ targets: { enemies: true },
508
+ range: 8,
509
+ manaCost: 90,
510
+ cooldown: 9,
511
+ castTime: 0.3,
512
+ charge: 'staticCharge',
513
+ effects: [
514
+ { type: 'missile', homing: true, speed: 30, bounce: { count: 2, range: 5, falloff: 0.25 }, effects: [{ type: 'damage', amount: [90, 130, 170], scaling: { abilityPower: 0.5 } }] },
515
+ CHARGED
516
+ ]
517
+ },
518
+ {
519
+ id: 'galeWard',
520
+ name: 'Gale Ward',
521
+ description: 'Enemies his abilities hurt are slowed 20, 30 or 40% for 1.5 seconds',
522
+ target: 'passive',
523
+ effects: [{ type: 'onAbilityHit', effects: [{ type: 'buff', buff: 'buffeted' }] }]
524
+ },
525
+ {
526
+ id: 'eyeOfTheStorm',
527
+ name: 'Eye of the Storm',
528
+ description:
529
+ 'A storm over an area for 5 seconds: enemies in it are slowed 35% and take 40 a second (+16% ability power), and whoever crosses its edge is thrown up and takes 60',
530
+ hotkey: 'R',
531
+ target: 'point',
532
+ range: 9,
533
+ manaCost: 175,
534
+ cooldown: 100,
535
+ castTime: 0.5,
536
+ maxLevel: 1,
537
+ ultimate: true,
538
+ charge: 'staticCharge',
539
+ effects: [
540
+ {
541
+ type: 'zone',
542
+ duration: 5,
543
+ radius: 4,
544
+ effects: [
545
+ { type: 'damage', amount: 10, scaling: { abilityPower: 0.04 }, radius: 4, ...FOES },
546
+ { type: 'buff', buff: 'stormSlow', duration: 0.5, radius: 4, ...FOES }
547
+ ],
548
+ edge: [
549
+ { type: 'buff', buff: 'knockedUp', duration: 0.75, ...FOES },
550
+ { type: 'damage', amount: 60, ...FOES }
551
+ ]
552
+ },
553
+ CHARGED
554
+ ]
555
+ },
556
+
557
+ // ---- Gale Hunter: long shots, a dash, a volley that pins an area
558
+ {
559
+ id: 'steadyAim',
560
+ name: 'Steady Aim',
561
+ description: 'Each half second standing still adds 0.5 range and 6% critical chance, four times at most; moving loses all of it',
562
+ target: 'passive',
563
+ maxLevel: 1,
564
+ effects: [{ type: 'buff', buff: 'steadyAim' }]
565
+ },
566
+ {
567
+ id: 'piercingBolt',
568
+ name: 'Piercing Bolt',
569
+ description: 'A bolt that flies far in a line, through everyone in its way: 80, 130 or 180 (+80% attack damage) to each',
570
+ hotkey: 'Q',
571
+ target: 'direction',
572
+ manaCost: 70,
573
+ cooldown: [10, 9, 8],
574
+ castTime: 0.25,
575
+ effects: [
576
+ { type: 'missile', speed: 24, distance: 14, radius: 0.8, pierce: true, effects: [{ type: 'damage', amount: [80, 130, 180], scaling: { attackDamage: 0.8 }, damageType: 'physical' }] }
577
+ ]
578
+ },
579
+ {
580
+ id: 'tumble',
581
+ name: 'Tumble',
582
+ description: 'Rolls a short way, and the next attack within 4 seconds deals 40, 70 or 100 more',
583
+ hotkey: 'W',
584
+ target: 'point',
585
+ range: 5,
586
+ manaCost: 40,
587
+ cooldown: [9, 8, 7],
588
+ effects: [
589
+ { type: 'dash', distance: 5 },
590
+ { type: 'buff', buff: 'tumbleShot', on: 'caster' }
591
+ ]
592
+ },
593
+ {
594
+ id: 'huntsman',
595
+ name: 'Huntsman',
596
+ description: 'Attacks on creeps and anything that is not a hero deal 15, 25 or 35 more',
597
+ target: 'passive',
598
+ effects: [{ type: 'onHit', effects: [{ type: 'damage', amount: [15, 25, 35], damageType: 'physical', where: { hero: false } }] }]
599
+ },
600
+ {
601
+ id: 'arrowstorm',
602
+ name: 'Arrowstorm',
603
+ description: 'Arrows fall over an area for 4 seconds: enemies in it are pinned, 50% slower, and take 48 a second (+20% attack damage)',
604
+ hotkey: 'R',
605
+ target: 'point',
606
+ range: 10,
607
+ manaCost: 150,
608
+ cooldown: 90,
609
+ castTime: 0.4,
610
+ maxLevel: 1,
611
+ ultimate: true,
612
+ effects: [
613
+ {
614
+ type: 'zone',
615
+ duration: 4,
616
+ radius: 5,
617
+ effects: [
618
+ { type: 'damage', amount: 12, scaling: { attackDamage: 0.05 }, damageType: 'physical', radius: 5, ...FOES },
619
+ { type: 'buff', buff: 'pinned', duration: 0.5, radius: 5, ...FOES }
620
+ ]
621
+ }
622
+ ]
623
+ },
624
+
625
+ // ---- Gravelord: drains life, an aura for his allies, raises the dead
626
+ {
627
+ id: 'bloodTithe',
628
+ name: 'Blood Tithe',
629
+ description: 'He has no mana: his abilities cost health instead, and 8 to 14% of the damage he deals heals him',
630
+ target: 'passive',
631
+ maxLevel: 4,
632
+ effects: [{ type: 'buff', buff: 'bloodTithe' }]
633
+ },
634
+ {
635
+ id: 'graveLash',
636
+ name: 'Grave Lash',
637
+ description: 'Lashes an enemy for 90, 150 or 210 (+60% ability power) and slows it 30% for 1.5 seconds',
638
+ hotkey: 'Q',
639
+ target: 'unit',
640
+ targets: { enemies: true },
641
+ range: 7,
642
+ manaCost: 60,
643
+ cooldown: 7,
644
+ castTime: 0.3,
645
+ effects: [
646
+ { type: 'damage', amount: [90, 150, 210], scaling: { abilityPower: 0.6 } },
647
+ { type: 'buff', buff: 'graveChill' }
648
+ ]
649
+ },
650
+ {
651
+ id: 'siphon',
652
+ name: 'Siphon',
653
+ description: 'For 3 seconds, drains an enemy of 40, 60 or 80 a second (+20% ability power) and heals as much. Broken off as it gets away',
654
+ hotkey: 'W',
655
+ target: 'unit',
656
+ targets: { enemies: true },
657
+ range: 6,
658
+ manaCost: 70,
659
+ cooldown: 14,
660
+ effects: [],
661
+ channel: {
662
+ duration: 3,
663
+ every: 0.5,
664
+ leash: 8,
665
+ effects: [
666
+ { type: 'damage', amount: [20, 30, 40], scaling: { abilityPower: 0.1 } },
667
+ { type: 'heal', amount: [20, 30, 40], scaling: { abilityPower: 0.1 }, on: 'caster' }
668
+ ]
669
+ }
670
+ },
671
+ {
672
+ id: 'deathlessAura',
673
+ name: 'Deathless Aura',
674
+ description: 'Allies near him move 10, 15 or 20% faster and heal 1, 2 or 3 a second',
675
+ target: 'passive',
676
+ effects: [{ type: 'aura', buff: 'deathless', radius: 9 }]
677
+ },
678
+ {
679
+ id: 'armyOfTheGrave',
680
+ name: 'Army of the Grave',
681
+ description: 'Six of the dead around him rise as skeletons for 40 seconds, and the attacks of his allies near heal them for 8 seconds',
682
+ hotkey: 'R',
683
+ target: 'none',
684
+ manaCost: 150,
685
+ cooldown: 120,
686
+ maxLevel: 1,
687
+ ultimate: true,
688
+ effects: [
689
+ { type: 'raiseDead', unit: 'skeleton', count: 6, radius: 9, duration: 40 },
690
+ { type: 'buff', buff: 'graveRallied', radius: 9, affects: 'allies' }
691
+ ]
692
+ },
693
+
694
+ // ---- Rime Witch: roots and freezes; her team follows her holds up
695
+ {
696
+ id: 'shatter',
697
+ name: 'Shatter',
698
+ description: 'An enemy she holds (frozen, rooted) takes 25 to 35% more from the next blow of an ally of hers, which ends the hold',
699
+ target: 'passive',
700
+ maxLevel: 3,
701
+ effects: [{ type: 'buff', buff: 'shatterSense' }]
702
+ },
703
+ {
704
+ id: 'rimeBolt',
705
+ name: 'Rime Bolt',
706
+ description: 'Hits an enemy for 80, 130 or 180 (+60% ability power) and freezes it for 1, 1.25 or 1.5 seconds',
707
+ hotkey: 'Q',
708
+ target: 'unit',
709
+ targets: { enemies: true },
710
+ range: 8,
711
+ manaCost: 80,
712
+ cooldown: 8,
713
+ castTime: 0.3,
714
+ effects: [
715
+ { type: 'damage', amount: [80, 130, 180], scaling: { abilityPower: 0.6 } },
716
+ { type: 'buff', buff: 'frozen', duration: [1, 1.25, 1.5] }
717
+ ]
718
+ },
719
+ {
720
+ id: 'grasp',
721
+ name: 'Grasping Ice',
722
+ description: 'Ice bursts from the ground in an area: enemies there take 70, 110 or 150 (+50% ability power) and are held where they stand for 1.5 to 2 seconds',
723
+ hotkey: 'W',
724
+ tags: ['ground'],
725
+ target: 'point',
726
+ range: 8,
727
+ manaCost: 90,
728
+ cooldown: 12,
729
+ castTime: 0.4,
730
+ effects: [
731
+ { type: 'damage', amount: [70, 110, 150], scaling: { abilityPower: 0.5 }, radius: 2.5, layers: ['ground'], ...FOES },
732
+ { type: 'buff', buff: 'iceRooted', duration: [1.5, 1.75, 2], radius: 2.5, layers: ['ground'], ...FOES }
733
+ ]
734
+ },
735
+ {
736
+ id: 'hoarfrost',
737
+ name: 'Hoarfrost',
738
+ description: 'Her attacks slow 20, 25 or 30% for 1.5 seconds',
739
+ target: 'passive',
740
+ effects: [{ type: 'onHit', effects: [{ type: 'buff', buff: 'frostbitten' }] }]
741
+ },
742
+ {
743
+ id: 'whiteout',
744
+ name: 'Whiteout',
745
+ description:
746
+ 'A blizzard over an area for 5 seconds: enemies in it are slowed 30% and take 32 a second (+12% ability power), and one that stays 2 seconds is frozen for 1.5',
747
+ hotkey: 'R',
748
+ target: 'point',
749
+ range: 9,
750
+ manaCost: 175,
751
+ cooldown: 100,
752
+ castTime: 0.5,
753
+ maxLevel: 1,
754
+ ultimate: true,
755
+ effects: [
756
+ {
757
+ type: 'zone',
758
+ duration: 5,
759
+ radius: 4.5,
760
+ effects: [
761
+ { type: 'damage', amount: 8, scaling: { abilityPower: 0.03 }, radius: 4.5, ...FOES },
762
+ { type: 'buff', buff: 'whiteoutChill', duration: 0.5, radius: 4.5, ...FOES },
763
+ { type: 'buff', buff: 'frostbite', stack: true, radius: 4.5, ...FOES }
764
+ ]
765
+ }
766
+ ]
767
+ },
768
+
769
+ // ---- Duskblade: waits in the shadows, marks one and bursts it, slips away through a crowd
770
+ {
771
+ id: 'shadowStep',
772
+ name: 'Shadow Step',
773
+ description:
774
+ 'Three seconds out of a fight (neither hurting nor hurt) hide her in the shadows, seen only from up close: her first strike from them deals 75 to 125% more and silences for 1.5 seconds',
775
+ target: 'passive',
776
+ maxLevel: 3,
777
+ effects: [{ type: 'buff', buff: 'shadowWatch' }]
778
+ },
779
+ {
780
+ id: 'duskMark',
781
+ name: 'Dusk Mark',
782
+ description:
783
+ 'Cuts an enemy for 60, 100 or 140 (+50% attack damage) and marks it for 4 seconds: it takes 10 to 20% more from everyone, is seen wherever it goes, and the mark bursts for 60, 100 or 140 at its end',
784
+ hotkey: 'Q',
785
+ target: 'unit',
786
+ targets: { enemies: true },
787
+ range: 7,
788
+ manaCost: 70,
789
+ cooldown: 10,
790
+ effects: [
791
+ { type: 'damage', amount: [60, 100, 140], scaling: { attackDamage: 0.5 }, damageType: 'physical' },
792
+ { type: 'buff', buff: 'duskMark' }
793
+ ]
794
+ },
795
+ {
796
+ id: 'fade',
797
+ name: 'Fade',
798
+ description: 'For 2 seconds she walks through units, 40% faster, seen only from up close. Attacking ends it',
799
+ hotkey: 'W',
800
+ target: 'none',
801
+ manaCost: 60,
802
+ cooldown: [16, 14, 12],
803
+ effects: [{ type: 'buff', buff: 'faded', on: 'caster' }]
804
+ },
805
+ {
806
+ id: 'twinFangs',
807
+ name: 'Twin Fangs',
808
+ description: 'Every third attack strikes again for 20, 35 or 50 (+50% attack damage)',
809
+ target: 'passive',
810
+ effects: [{ type: 'onHit', every: 3, effects: [{ type: 'damage', amount: [20, 35, 50], scaling: { attackDamage: 0.5 }, damageType: 'physical' }] }]
811
+ },
812
+ {
813
+ id: 'eclipse',
814
+ name: 'Eclipse',
815
+ description: 'Steps to an enemy and cuts it off for 3 seconds: it cannot use its abilities, cannot be healed, and is seen wherever it goes',
816
+ hotkey: 'R',
817
+ target: 'unit',
818
+ targets: { enemies: true },
819
+ range: 6,
820
+ manaCost: 120,
821
+ cooldown: 90,
822
+ maxLevel: 1,
823
+ ultimate: true,
824
+ effects: [
825
+ { type: 'blink', distance: 6 },
826
+ { type: 'buff', buff: 'eclipsed' }
827
+ ]
828
+ },
829
+
830
+ // ---- Bone Reaver: clears camps fast, grows with every kill
831
+ {
832
+ id: 'harvest',
833
+ name: 'Harvest',
834
+ description: `Every kill adds 6 health and 1 attack damage for good, a hero's ${HERO_HARVEST} times as much, up to ${MOST_HARVEST}; he loses half of it when he dies`,
835
+ target: 'passive',
836
+ maxLevel: 1,
837
+ effects: [{ type: 'buff', buff: 'harvest' }]
838
+ },
839
+ {
840
+ id: 'boneCleaver',
841
+ name: 'Bone Cleaver',
842
+ description: 'Cleaves all around him for 60, 100 or 140 (+80% attack damage), twice as much to creeps and anything that is not a hero',
843
+ hotkey: 'Q',
844
+ target: 'none',
845
+ manaCost: 70,
846
+ cooldown: [8, 7, 6],
847
+ castTime: 0.3,
848
+ effects: [
849
+ { type: 'damage', amount: [60, 100, 140], scaling: { attackDamage: 0.8 }, damageType: 'physical', radius: 3, ...FOES },
850
+ { type: 'damage', amount: [60, 100, 140], scaling: { attackDamage: 0.8 }, damageType: 'physical', radius: 3, ...FOES_NOT_HEROES }
851
+ ]
852
+ },
853
+ {
854
+ id: 'gorge',
855
+ name: 'Gorge',
856
+ description: 'Tears into a creep or another unit that is not a hero for 250, 400 or 550 true damage, and heals 60, 100 or 140',
857
+ hotkey: 'W',
858
+ target: 'unit',
859
+ targets: { enemies: true, heroes: false },
860
+ range: 2,
861
+ manaCost: 90,
862
+ cooldown: [16, 14, 12],
863
+ castTime: 0.25,
864
+ effects: [
865
+ { type: 'damage', amount: [250, 400, 550], damageType: 'true' },
866
+ { type: 'heal', amount: [60, 100, 140], on: 'caster' }
867
+ ]
868
+ },
869
+ {
870
+ id: 'carrionHunger',
871
+ name: 'Carrion Hunger',
872
+ description: 'His blows strike the enemies around his target too, for 30, 45 or 60% of their damage',
873
+ target: 'passive',
874
+ effects: [{ type: 'buff', buff: 'carrionHunger' }]
875
+ },
876
+ {
877
+ id: 'bonePit',
878
+ name: 'Bone Pit',
879
+ description: 'A ring of bones for 4 seconds: whoever crosses it is stunned for 1 second, and allies inside attack 30% faster and heal by their attacks',
880
+ hotkey: 'R',
881
+ target: 'point',
882
+ range: 7,
883
+ manaCost: 150,
884
+ cooldown: 100,
885
+ maxLevel: 1,
886
+ ultimate: true,
887
+ effects: [
888
+ {
889
+ type: 'zone',
890
+ duration: 4,
891
+ radius: 4.5,
892
+ effects: [{ type: 'buff', buff: 'bonePitFrenzy', duration: 0.5, radius: 4.5, affects: 'allies' }],
893
+ edge: [{ type: 'buff', buff: 'stunned', duration: 1, ...FOES }]
894
+ }
895
+ ]
896
+ }
897
+ ]
898
+
899
+ // ---- in code: what the fields above do not say
900
+
901
+ /** What Bone Reaver reaped, kept on him through deaths, saved with him: the count of Harvest */
902
+ export const harvested = defineComponent<{ count: number }>({ id: 'melee.harvested' })
903
+
904
+ /** Gale Hunter, every half second: where it stood the last time, a stack more of Steadied; moved, none */
905
+ function steadyTick(world: World, carrier: Entity, instance: BuffInstance): void {
906
+ const was = instance.data as { x: number; z: number } | undefined
907
+ instance.data = { x: carrier.pos.x, z: carrier.pos.z }
908
+ const [dx, dz] = [carrier.pos.x - (was?.x ?? 0), carrier.pos.z - (was?.z ?? 0)]
909
+ if (!was || dx * dx + dz * dz > STILL * STILL) {
910
+ if (hasBuff(carrier, 'steadied')) removeBuff(world, carrier, 'steadied')
911
+ return
912
+ }
913
+ applyBuff(world, carrier, 'steadied', undefined, carrier)
914
+ const steadied = carrier.buffs.find((each) => each.id === 'steadied')
915
+ if (steadied) steadied.stacks = Math.min((steadied.stacks ?? 0) + 1, world.registry.buff('steadied').maxStacks ?? 1)
916
+ invalidateStats(world, carrier)
917
+ }
918
+
919
+ /** How far a unit may be nudged in half a second and still stand still, to Steady Aim */
920
+ const STILL = 0.2
921
+
922
+ /** Seconds out of a fight before Duskblade is in the shadows */
923
+ const QUIET = 3
924
+
925
+ /** Duskblade, every quarter second: three seconds since she last hurt anyone or was hurt, in the shadows */
926
+ function shadowTick(world: World, carrier: Entity, instance: BuffInstance): void {
927
+ const struck = (instance.data?.struckAt as number | undefined) ?? -Infinity
928
+ if (hasBuff(carrier, 'shadowed') || world.time - Math.max(carrier.hurtAt, struck) < QUIET) return
929
+ applyBuff(world, carrier, 'shadowed', undefined, carrier, instance.level)
930
+ }
931
+
932
+ /** Bone Reaver's Harvest put to a new count: the stacks shown and the stats */
933
+ function setHarvest(world: World, hero: Entity, count: number): void {
934
+ hero.set(harvested, { count })
935
+ const buff = hero.buffs.find((instance) => instance.id === 'harvest')
936
+ if (buff) buff.stacks = count
937
+ invalidateStats(world, hero)
938
+ }
939
+
940
+ /** The ally of a hero's who stands guard over it (Bulwark): the Oathbreaker whose aura it carries, alive and not itself */
941
+ function guardOf(world: World, hero: Entity): Entity | undefined {
942
+ const ward = hero.buffs.find((instance) => instance.id === 'bulwarkWard')
943
+ const guard = ward && world.get(ward.source)
944
+ return guard?.alive && guard !== hero && !guard.hidden ? guard : undefined
945
+ }
946
+
947
+ /** The share of a blow Bulwark takes */
948
+ const BULWARK = 0.2
949
+ /** How much more a shattering blow deals, by the level of Shatter */
950
+ const SHATTER = [0.25, 0.3, 0.35]
951
+
952
+ export const heroHooks: HookDef[] = [
953
+ defineHook({
954
+ hook: 'beforeDamage',
955
+ id: 'melee.heroes.damage',
956
+ run(world, event) {
957
+ const { target, source } = event
958
+ if (!target.buffs.length) return
959
+ // Shatter: a hold of a Rime Witch's, struck by an ally of hers, deals more and ends
960
+ if (source && !event.tags?.includes('shatter'))
961
+ for (const instance of target.buffs) {
962
+ const def = world.registry.buff(instance.id)
963
+ if (!def.harmful || !holdsUp(def)) continue
964
+ const witch = world.get(instance.source)
965
+ const sense = witch?.buffs.find((each) => each.id === 'shatterSense')
966
+ if (!witch || !sense || witch === source || world.areEnemies(witch.owner, source.owner)) continue
967
+ event.amount *= 1 + SHATTER[Math.min(sense.level, SHATTER.length) - 1]
968
+ for (const hold of target.buffs.filter((each) => each.source === witch.id && holdsUp(world.registry.buff(each.id))))
969
+ removeBuff(world, target, hold.id, witch)
970
+ break
971
+ }
972
+ // Bulwark: an Oathbreaker near takes a fifth of what an allied hero takes, before its armor
973
+ const guard = event.tags?.includes('bulwark') ? undefined : guardOf(world, target)
974
+ if (guard && event.amount > 0) {
975
+ const share = event.amount * BULWARK
976
+ event.amount -= share
977
+ world.damage(guard, share, source, { damageType: event.type, tags: ['bulwark'] })
978
+ }
979
+ }
980
+ }),
981
+ defineHook({
982
+ hook: 'afterDamage',
983
+ id: 'melee.heroes.shadows',
984
+ run(_world, event) {
985
+ // Duskblade out of the shadows as she strikes; and the time of it, for Shadow Step
986
+ const watch = event.source?.buffs.find((instance) => instance.id === 'shadowWatch')
987
+ if (watch) watch.data = { ...watch.data, struckAt: _world.time }
988
+ if (event.dealt > 0 && hasBuff(event.target, 'shadowed')) removeBuff(_world, event.target, 'shadowed')
989
+ }
990
+ }),
991
+ defineHook({
992
+ hook: 'beforeCast',
993
+ id: 'melee.heroes.staticCharge',
994
+ run(world, event) {
995
+ // Static Charge full: the spell costs nothing
996
+ const charge = event.ability.charge
997
+ if (charge !== 'staticCharge') return
998
+ const stacks = event.caster.buffs.find((instance) => instance.id === charge)?.stacks ?? 0
999
+ if (stacks >= (world.registry.buff(charge).maxStacks ?? 1)) event.manaCost = 0
1000
+ }
1001
+ }),
1002
+ defineHook({
1003
+ hook: 'death',
1004
+ id: 'melee.heroes.harvest',
1005
+ run(world, { entity, killer }) {
1006
+ // Harvest: a kill adds to it, a hero's five times; dying, Bone Reaver loses half
1007
+ if (entity.has(harvested)) setHarvest(world, entity, Math.floor(entity.get(harvested)!.count / 2))
1008
+ if (!killer || killer === entity || !killer.alive || killer.def.hero?.innate !== 'harvest' || !entity.isUnit) return
1009
+ if (!world.areEnemies(killer.owner, entity.owner)) return
1010
+ const gain = heroOf(entity) ? HERO_HARVEST : 1
1011
+ setHarvest(world, killer, Math.min((killer.get(harvested)?.count ?? 0) + gain, MOST_HARVEST))
1012
+ }
1013
+ })
1014
+ ]
1015
+
1016
+ export const heroEffects: EffectType[] = [
1017
+ /** Lightning leaping from where a spell went off to the nearest other enemy: Static Charge */
1018
+ defineEffect({ type: 'staticArc', run: ({ world, caster, target }, effect) => {
1019
+ const from = target ?? caster
1020
+ const struck = world.query({ near: from, radius: effect.radius, is: 'enemy', of: caster.owner, kinds: ['unit'], except: [caster, ...(target ? [target] : [])], sort: 'nearest', limit: 1 })[0]
1021
+ if (!struck) return
1022
+ const level = heroOf(caster)?.level ?? 1
1023
+ world.damage(struck, effect.amount + effect.perLevel * (level - 1), caster, { damageType: 'magic', ability: 'staticCharge' })
1024
+ } })
1025
+ ]