@v12-engine/create 0.0.0-stage → 0.1.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -2
  3. package/base/_gitignore +10 -0
  4. package/base/index.html +20 -0
  5. package/base/package.json +23 -0
  6. package/base/src/ground.worker.ts +8 -0
  7. package/base/src/main.ts +61 -0
  8. package/base/tsconfig.json +14 -0
  9. package/base/vite.config.ts +25 -0
  10. package/bin.mjs +10 -0
  11. package/cli.mjs +381 -0
  12. package/index.mjs +286 -0
  13. package/layers/assets/package.json +5 -0
  14. package/layers/editor/package.json +8 -0
  15. package/layers/editor/src/editor.ts +13 -0
  16. package/layers/rooms/server.ts +69 -0
  17. package/layers/server/package.json +8 -0
  18. package/layers/server/server.ts +34 -0
  19. package/layers/source/package.json +12 -0
  20. package/layers/source/tsconfig.json +5 -0
  21. package/layers/source/vite.config.ts +45 -0
  22. package/layers/tests/package.json +8 -0
  23. package/package.json +45 -5
  24. package/starters/campaign/README.md +61 -0
  25. package/starters/campaign/files.txt +42 -0
  26. package/starters/campaign/layers/editor/src/editor.ts +18 -0
  27. package/starters/campaign/layers/rooms/server.ts +39 -0
  28. package/starters/campaign/layers/server/server.ts +33 -0
  29. package/starters/campaign/layers/tests/tests/rules.test.ts +320 -0
  30. package/starters/campaign/public/games/campaign/assets.json +1 -0
  31. package/starters/campaign/src/campaign/campaign.json +52 -0
  32. package/starters/campaign/src/campaign/carry.ts +47 -0
  33. package/starters/campaign/src/campaign/cinematics/intro.json +148 -0
  34. package/starters/campaign/src/campaign/cinematics/outro.json +92 -0
  35. package/starters/campaign/src/campaign/data.json +1 -0
  36. package/starters/campaign/src/campaign/fx.json +1 -0
  37. package/starters/campaign/src/campaign/game.ts +6 -0
  38. package/starters/campaign/src/campaign/ids.ts +77 -0
  39. package/starters/campaign/src/campaign/index.ts +121 -0
  40. package/starters/campaign/src/campaign/kills.ts +16 -0
  41. package/starters/campaign/src/campaign/looks.json +1 -0
  42. package/starters/campaign/src/campaign/maps.ts +34 -0
  43. package/starters/campaign/src/campaign/missions/homecoming.ts +160 -0
  44. package/starters/campaign/src/campaign/missions/siege.ts +91 -0
  45. package/starters/campaign/src/campaign/missions/warlord.ts +177 -0
  46. package/starters/campaign/src/campaign/prefabs/watchpost.prefab.json +18 -0
  47. package/starters/campaign/src/campaign/scenes.ts +46 -0
  48. package/starters/campaign/src/campaign/tiles.ts +8 -0
  49. package/starters/campaign/src/campaign/units.ts +390 -0
  50. package/starters/campaign/src/campaign/view/cinematics.ts +59 -0
  51. package/starters/campaign/src/campaign/view/fire.ts +115 -0
  52. package/starters/campaign/src/campaign/view/index.ts +334 -0
  53. package/starters/campaign/src/campaign/view/labels.ts +105 -0
  54. package/starters/campaign/src/campaign/view/rubble.ts +40 -0
  55. package/starters/campaign/src/campaign/view/waveLane.ts +88 -0
  56. package/starters/campaign/src/campaign/waves.json +36 -0
  57. package/starters/campaign/src/campaign/waves.ts +186 -0
  58. package/starters/campaign/src/campaign-editor.ts +43 -0
  59. package/starters/campaign/src/game.ts +2 -0
  60. package/starters/campaign/src/main.ts +70 -0
  61. package/starters/campaign/src/maps/fort.names.ts +31 -0
  62. package/starters/campaign/src/maps/fort.vgmap.json +1162 -0
  63. package/starters/campaign/src/maps/homestead.names.ts +36 -0
  64. package/starters/campaign/src/maps/homestead.vgmap.json +916 -0
  65. package/starters/campaign/src/maps/pass.names.ts +40 -0
  66. package/starters/campaign/src/maps/pass.vgmap.json +522 -0
  67. package/starters/campaign/src/maps.ts +2 -0
  68. package/starters/campaign/src/rules.ts +20 -0
  69. package/starters/campaign/src/waves-panel.ts +355 -0
  70. package/starters/empty/README.md +71 -0
  71. package/starters/empty/layers/tests/tests/rules.test.ts +31 -0
  72. package/starters/empty/src/game.ts +79 -0
  73. package/starters/empty/src/generate.worker.ts +9 -0
  74. package/starters/empty/src/maps.ts +44 -0
  75. package/starters/empty/src/rules.ts +67 -0
  76. package/starters/empty/src/tiles.ts +12 -0
  77. package/starters/melee/README.md +61 -0
  78. package/starters/melee/files.txt +60 -0
  79. package/starters/melee/layers/editor/src/editor.ts +13 -0
  80. package/starters/melee/layers/tests/tests/rules.test.ts +58 -0
  81. package/starters/melee/src/game.ts +2 -0
  82. package/starters/melee/src/maps/bay.names.ts +28 -0
  83. package/starters/melee/src/maps/bay.vgmap.json +3123 -0
  84. package/starters/melee/src/maps.ts +20 -0
  85. package/starters/melee/src/melee/abilities.ts +376 -0
  86. package/starters/melee/src/melee/bay.ts +319 -0
  87. package/starters/melee/src/melee/blight.ts +102 -0
  88. package/starters/melee/src/melee/cellLayers.json +11 -0
  89. package/starters/melee/src/melee/data.json +18 -0
  90. package/starters/melee/src/melee/floats.json +199 -0
  91. package/starters/melee/src/melee/formations.ts +46 -0
  92. package/starters/melee/src/melee/fx.json +1 -0
  93. package/starters/melee/src/melee/game.ts +6 -0
  94. package/starters/melee/src/melee/generator.ts +116 -0
  95. package/starters/melee/src/melee/heroSpells.ts +38 -0
  96. package/starters/melee/src/melee/hud.json +269 -0
  97. package/starters/melee/src/melee/index.ts +182 -0
  98. package/starters/melee/src/melee/items.ts +362 -0
  99. package/starters/melee/src/melee/languages/he.json +96 -0
  100. package/starters/melee/src/melee/looks.json +1 -0
  101. package/starters/melee/src/melee/maps.ts +53 -0
  102. package/starters/melee/src/melee/pings.json +16 -0
  103. package/starters/melee/src/melee/pings.ts +34 -0
  104. package/starters/melee/src/melee/quality.json +1 -0
  105. package/starters/melee/src/melee/saves.ts +12 -0
  106. package/starters/melee/src/melee/sounds.json +1 -0
  107. package/starters/melee/src/melee/undead.ts +683 -0
  108. package/starters/melee/src/melee/units.ts +785 -0
  109. package/starters/melee/src/melee/view/buildings.ts +33 -0
  110. package/starters/melee/src/melee/view/cheerLane.ts +42 -0
  111. package/starters/melee/src/melee/view/heroSpellLooks.ts +21 -0
  112. package/starters/melee/src/melee/view/index.ts +268 -0
  113. package/starters/melee/src/melee/view/languages.ts +10 -0
  114. package/starters/melee/src/melee/view/looks.ts +659 -0
  115. package/starters/melee/src/melee/view/undeadLooks.ts +220 -0
  116. package/starters/melee/src/melee/view/walls.ts +59 -0
  117. package/starters/melee/src/rules.ts +24 -0
  118. package/starters.mjs +35 -0
  119. package/v12.mjs +111 -0
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,6 @@
1
+ import { defineGame } from '@v12-engine/client'
2
+ import { melee } from './index'
3
+ import { meleeView } from './view'
4
+
5
+ /** Melee, whole: its rules and how it looks */
6
+ export const meleeGame = defineGame({ rules: melee, view: meleeView })
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Melee's maps made by a generator (world plan G4): the engine's passes for the ground, its paint and the starts
3
+ * (each race puts its own hall and workers there, `defineRace`'s `start`), and the game's own for what it is played
4
+ * with: a gold mine by each start and more about the map, each guarded by a camp, more camps scattered away from the
5
+ * starts, and woods of trees to cut (the `tree` unit, not scenery). Everything mirrored as the map is
6
+ */
7
+ import {
8
+ exactMath,
9
+ type GeneratorContext,
10
+ MAP_NEUTRAL,
11
+ type MapGenerator,
12
+ presetGenerator,
13
+ resourcesPass,
14
+ scatterPass,
15
+ startsPass,
16
+ terrainPass,
17
+ tilesPass,
18
+ type Vec2
19
+ } from '@v12-engine/engine'
20
+
21
+ /** The engine's preset's parameters for the ground (cliff levels, water, roughness, hills): the game's own come after */
22
+ const GROUND = presetGenerator().params.filter((param) => ['levels', 'water', 'roughness', 'hills'].includes(param.id))
23
+
24
+ /** Who guards a camp, the weaker more often */
25
+ const CAMPS = [['gnoll', 'gnoll'], ['gnoll', 'gnoll', 'gnoll'], ['ogre'], ['ogre', 'gnoll']]
26
+
27
+ /** A camp of creeps at a place, of a kind drawn from the seed once for the place and its mirrors: the map is fair */
28
+ const camp = (ctx: GeneratorContext, at: Vec2) =>
29
+ ctx.addCamp(
30
+ at.x,
31
+ at.z,
32
+ ctx.once(at.x, at.z, () => CAMPS[ctx.rng.int(0, CAMPS.length - 1)])
33
+ )
34
+
35
+ export const meleeGenerator: MapGenerator = {
36
+ params: [
37
+ ...GROUND,
38
+ {
39
+ id: 'mines',
40
+ name: 'Gold mines',
41
+ description: 'How many more gold mines there are besides one at each start, each guarded: each mirrored as the map is',
42
+ default: 2,
43
+ min: 0,
44
+ max: 8,
45
+ step: 1
46
+ },
47
+ {
48
+ id: 'camps',
49
+ name: 'Creep camps',
50
+ description: 'How often a camp of creeps stands about the map, away from the starts',
51
+ default: 0.002,
52
+ min: 0,
53
+ max: 0.01,
54
+ step: 0.001
55
+ },
56
+ { id: 'trees', name: 'Woods', description: 'How much of the map is woods, to cut for lumber', default: 0.18, min: 0, max: 0.5, step: 0.02 }
57
+ ],
58
+ passes: [
59
+ terrainPass(),
60
+ tilesPass(),
61
+ // room for a hall and its workers: the race puts them there as the game starts
62
+ startsPass({ clearing: 8 }),
63
+ {
64
+ id: 'mines',
65
+ name: 'The gold mines',
66
+ run(ctx) {
67
+ resourcesPass({
68
+ main: 9,
69
+ extra: ctx.param('mines', 2),
70
+ from: (ctx, at, start) => {
71
+ // a mine stands on whole cells (4 by 4), its middle on a corner of them
72
+ ctx.addObject('goldmine', MAP_NEUTRAL, Math.round(at.x), Math.round(at.z))
73
+ if (start) return
74
+ // one not at a start is guarded, by a camp between it and the map's middle: the same side of every mirrored one
75
+ const dx = ctx.map.width / 2 - at.x
76
+ const dz = ctx.map.depth / 2 - at.z
77
+ const far = exactMath.hypot(dx, dz) || 1
78
+ const guard = { x: at.x + (dx / far) * 5, z: at.z + (dz / far) * 5 }
79
+ ctx.mark('camps', guard.x, guard.z, 3)
80
+ camp(ctx, guard)
81
+ }
82
+ }).run(ctx)
83
+ }
84
+ },
85
+ // a tavern in the middle, for anyone who goes there (its heroes hired once each), where the middle is land
86
+ {
87
+ id: 'tavern',
88
+ name: 'The tavern',
89
+ run(ctx) {
90
+ const at = { x: Math.round(ctx.map.width / 2) + 0.5, z: Math.round(ctx.map.depth / 2) + 0.5 }
91
+ const { map } = ctx
92
+ if (!ctx.free(at.x, at.z, 3) || map.water[Math.floor(at.z) * map.width + Math.floor(at.x)] !== 0) return
93
+ ctx.mark('camps', at.x, at.z, 4)
94
+ ctx.addObject('tavern', MAP_NEUTRAL, at.x, at.z, { name: 'the tavern' })
95
+ }
96
+ },
97
+ // camps about the map: far from the starts, off the mines and each other
98
+ scatterPass({
99
+ kind: 'camps',
100
+ spacing: 24,
101
+ avoid: 24,
102
+ patches: 0,
103
+ avoidKinds: { resource: 8, camps: 20 },
104
+ where: (cell) => !cell.shore,
105
+ what: camp
106
+ }),
107
+ // woods: trees to cut, on whole cells, off the mines and the camps
108
+ scatterPass({
109
+ kind: 'trees',
110
+ spacing: 1.5,
111
+ avoid: 12,
112
+ avoidKinds: { resource: 5, camps: 4 },
113
+ what: (ctx, at) => void ctx.addObject('tree', MAP_NEUTRAL, Math.floor(at.x) + 0.5, Math.floor(at.z) + 0.5)
114
+ })
115
+ ]
116
+ }
@@ -0,0 +1,38 @@
1
+ import type { AbilityDef, BuffDef } from '@v12-engine/engine'
2
+
3
+ /** The spells every hero brings along (`HeroDef.spells`): learned from the start, one point each, on long cooldowns */
4
+
5
+ export const heroSpellBuffs: BuffDef[] = [
6
+ { id: 'healHaste', name: 'Second Wind', description: 'Moves 30% faster', duration: 1, percent: { moveSpeed: 0.3 } },
7
+ { id: 'barrier', name: 'Barrier', description: 'A shield takes the damage', duration: 2.5, shield: 120, shieldScaling: { level: 21.2 } }
8
+ ]
9
+
10
+ export const heroSpells: AbilityDef[] = [
11
+ {
12
+ id: 'flash',
13
+ name: 'Flash',
14
+ description: 'At once a short way towards the pointer, through whatever is in between',
15
+ target: 'direction',
16
+ cooldown: 300,
17
+ effects: [{ type: 'blink', distance: 5.3 }]
18
+ },
19
+ {
20
+ id: 'secondWind',
21
+ name: 'Second Wind',
22
+ description: 'The hero and allied heroes nearby regain 80 to 318 health, more with every level, and run 30% faster for a second',
23
+ target: 'none',
24
+ cooldown: 240,
25
+ effects: [
26
+ { type: 'heal', amount: 80, scaling: { level: 14 }, radius: 10.9, only: 'heroes' },
27
+ { type: 'buff', buff: 'healHaste', radius: 10.9, only: 'heroes' }
28
+ ]
29
+ },
30
+ {
31
+ id: 'barrier',
32
+ name: 'Barrier',
33
+ description: 'A shield for 2.5 seconds that takes 120 to 480 damage, more with every level',
34
+ target: 'none',
35
+ cooldown: 180,
36
+ effects: [{ type: 'buff', buff: 'barrier', on: 'caster' }]
37
+ }
38
+ ]
@@ -0,0 +1,269 @@
1
+ {
2
+ "default": {
3
+ "plates": [
4
+ {
5
+ "when": {
6
+ "relation": [
7
+ "self",
8
+ "ally"
9
+ ]
10
+ },
11
+ "show": "hurt",
12
+ "gap": 2,
13
+ "items": [
14
+ {
15
+ "bar": "health",
16
+ "ghost": {
17
+ "seconds": 0.5
18
+ }
19
+ },
20
+ {
21
+ "buffs": "all",
22
+ "size": 10,
23
+ "gap": 1,
24
+ "most": 6,
25
+ "dots": true
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "show": "hurt",
31
+ "items": [
32
+ {
33
+ "bar": "health",
34
+ "ghost": {
35
+ "seconds": 0.5
36
+ }
37
+ }
38
+ ]
39
+ }
40
+ ]
41
+ },
42
+ "types": {
43
+ "necromancer": {
44
+ "plates": [
45
+ {
46
+ "when": {
47
+ "relation": [
48
+ "self",
49
+ "ally"
50
+ ]
51
+ },
52
+ "show": "hurt",
53
+ "gap": 2,
54
+ "items": [
55
+ {
56
+ "column": [
57
+ {
58
+ "bar": "health",
59
+ "ghost": {
60
+ "seconds": 0.5
61
+ }
62
+ },
63
+ {
64
+ "bar": "mana",
65
+ "height": 3
66
+ }
67
+ ],
68
+ "gap": 1
69
+ },
70
+ {
71
+ "buffs": "all",
72
+ "size": 10,
73
+ "gap": 1,
74
+ "most": 6,
75
+ "dots": true
76
+ }
77
+ ]
78
+ },
79
+ {
80
+ "show": "hurt",
81
+ "items": [
82
+ {
83
+ "bar": "health",
84
+ "ghost": {
85
+ "seconds": 0.5
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ },
92
+ "priest": {
93
+ "plates": [
94
+ {
95
+ "when": {
96
+ "relation": [
97
+ "self",
98
+ "ally"
99
+ ]
100
+ },
101
+ "show": "hurt",
102
+ "gap": 2,
103
+ "items": [
104
+ {
105
+ "column": [
106
+ {
107
+ "bar": "health",
108
+ "ghost": {
109
+ "seconds": 0.5
110
+ }
111
+ },
112
+ {
113
+ "bar": "mana",
114
+ "height": 3
115
+ }
116
+ ],
117
+ "gap": 1
118
+ },
119
+ {
120
+ "buffs": "all",
121
+ "size": 10,
122
+ "gap": 1,
123
+ "most": 6,
124
+ "dots": true
125
+ }
126
+ ]
127
+ },
128
+ {
129
+ "show": "hurt",
130
+ "items": [
131
+ {
132
+ "bar": "health",
133
+ "ghost": {
134
+ "seconds": 0.5
135
+ }
136
+ }
137
+ ]
138
+ }
139
+ ]
140
+ },
141
+ "sorceress": {
142
+ "plates": [
143
+ {
144
+ "when": {
145
+ "relation": [
146
+ "self",
147
+ "ally"
148
+ ]
149
+ },
150
+ "show": "hurt",
151
+ "gap": 2,
152
+ "items": [
153
+ {
154
+ "column": [
155
+ {
156
+ "bar": "health",
157
+ "ghost": {
158
+ "seconds": 0.5
159
+ }
160
+ },
161
+ {
162
+ "bar": "mana",
163
+ "height": 3
164
+ }
165
+ ],
166
+ "gap": 1
167
+ },
168
+ {
169
+ "buffs": "all",
170
+ "size": 10,
171
+ "gap": 1,
172
+ "most": 6,
173
+ "dots": true
174
+ }
175
+ ]
176
+ },
177
+ {
178
+ "show": "hurt",
179
+ "items": [
180
+ {
181
+ "bar": "health",
182
+ "ghost": {
183
+ "seconds": 0.5
184
+ }
185
+ }
186
+ ]
187
+ }
188
+ ]
189
+ },
190
+ "gnoll": {
191
+ "plates": [
192
+ {
193
+ "show": "hurt",
194
+ "items": [
195
+ {
196
+ "bar": "health",
197
+ "ghost": {
198
+ "seconds": 0.5
199
+ }
200
+ }
201
+ ]
202
+ }
203
+ ],
204
+ "within": 45,
205
+ "far": {
206
+ "items": [
207
+ {
208
+ "badge": "",
209
+ "size": 7,
210
+ "background": "#e8913a",
211
+ "border": "#2a1a08"
212
+ }
213
+ ]
214
+ }
215
+ },
216
+ "ogre": {
217
+ "plates": [
218
+ {
219
+ "show": "hurt",
220
+ "items": [
221
+ {
222
+ "bar": "health",
223
+ "ghost": {
224
+ "seconds": 0.5
225
+ }
226
+ }
227
+ ]
228
+ }
229
+ ],
230
+ "within": 45,
231
+ "far": {
232
+ "items": [
233
+ {
234
+ "badge": "",
235
+ "size": 7,
236
+ "background": "#e8913a",
237
+ "border": "#2a1a08"
238
+ }
239
+ ]
240
+ }
241
+ },
242
+ "troll": {
243
+ "plates": [
244
+ {
245
+ "show": "hurt",
246
+ "items": [
247
+ {
248
+ "bar": "health",
249
+ "ghost": {
250
+ "seconds": 0.5
251
+ }
252
+ }
253
+ ]
254
+ }
255
+ ],
256
+ "within": 45,
257
+ "far": {
258
+ "items": [
259
+ {
260
+ "badge": "",
261
+ "size": 7,
262
+ "background": "#e8913a",
263
+ "border": "#2a1a08"
264
+ }
265
+ ]
266
+ }
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,182 @@
1
+ import {
2
+ defineCellLayer,
3
+ defineRace,
4
+ withCapture,
5
+ withConcealment,
6
+ withStances,
7
+ withData,
8
+ type Entity,
9
+ type GameModule,
10
+ type StartData,
11
+ hotRules,
12
+ presetScore,
13
+ heroState,
14
+ withHeroes,
15
+ withMobaStats,
16
+ withVeterancy,
17
+ withWalls,
18
+ withRemains,
19
+ withFriendlyFire,
20
+ laneMinion
21
+ } from '@v12-engine/engine'
22
+ import { meleeBrain } from '@v12-engine/engine/ai'
23
+ import { abilities, buffs } from './abilities'
24
+ import { formations } from './formations'
25
+ import { blightLayer, blightSpread, blightSystem, undeadHooks } from './blight'
26
+ import { heroSpellBuffs, heroSpells } from './heroSpells'
27
+ import { itemAbilities, itemEffects, items } from './items'
28
+ import { meleeEditor, tiles } from './maps'
29
+ import { meleeGenerator } from './generator'
30
+ import { units, upgrades } from './units'
31
+ import { meleeSaves } from './saves'
32
+ import { undeadAbilities, undeadBuffs } from './undead'
33
+ import dataFile from './data.json'
34
+ import { maps } from '../maps'
35
+ import { PING_KINDS, pingsFile } from './pings'
36
+
37
+ /** The ruins a fallen building leaves (`ruins`): of its size, 2 to 4 cells; none for a wall's piece, what stands on something, what cannot be felled, or ruins (taken away, they leave nothing) */
38
+ function ruinsOf(dead: Entity): string | null {
39
+ const footprint = dead.def.footprint
40
+ if (!footprint || dead.isResource || dead.def.wall || dead.def.invulnerable || dead.on !== undefined) return null
41
+ const size = Math.min(Math.max(footprint.w, footprint.d), 4)
42
+ return size >= 2 ? `ruins${size}` : null
43
+ }
44
+
45
+ /** A town hall and five peasants on the side of the closest gold mine, as in a classic melee game: the humans' start, and the game's */
46
+ const HUMAN_START: StartData = { hall: 'townhall', workers: { type: 'peasant', count: 5 } }
47
+
48
+ /** Classic base building: gather gold, raise an army, destroy the other side */
49
+ export const melee: GameModule = withData(
50
+ {
51
+ id: 'my-game',
52
+ name: 'My game',
53
+ version: '1.0.0',
54
+ // a route in the dark says nothing of what is there: each side plans with the buildings it has seen
55
+ paths: 'seen',
56
+ // its computer player: the engine's melee brain, what a seat that names no brain gets
57
+ brains: [meleeBrain],
58
+ units,
59
+ upgrades,
60
+ // two sides, each its own roster, hall and workers (features-plan C4): chosen in the lobby, or fixed by a map
61
+ races: [
62
+ defineRace({
63
+ id: 'human',
64
+ name: 'Human',
65
+ description: 'The Kingdom: peasants, footmen, riflemen and gryphons',
66
+ // a town hall and five peasants on the side of the closest gold mine (`StartData`, the engine's `startingBase`)
67
+ start: HUMAN_START
68
+ }),
69
+ defineRace({
70
+ id: 'undead',
71
+ name: 'Undead',
72
+ description: 'The Restless: acolytes gather at a cursed mine, ghouls bring lumber, everything goes up on blight',
73
+ // a gravehold and a cursed mine on the closest gold mine, three acolytes and a ghoul
74
+ start: { hall: 'gravehold', workers: { type: 'acolyte', count: 3 }, units: [{ type: 'ghoul', count: 1 }], onMine: 'cursedMine' },
75
+ // gold at a cursed mine, buildings on blight (`blight.ts`)
76
+ hooks: undeadHooks
77
+ })
78
+ ],
79
+ abilities: [...abilities, ...heroSpells, ...itemAbilities, ...undeadAbilities],
80
+ // the classic battle line beside the engine's formations, on the button by the minimap (`formations.ts`)
81
+ formations,
82
+ effects: itemEffects,
83
+ items,
84
+ resources: [
85
+ // gold pays for most things, and is what upkeep cuts into
86
+ { id: 'gold', name: 'Gold', start: 500, upkeep: true },
87
+ { id: 'lumber', name: 'Lumber', start: 300 }
88
+ ],
89
+ currency: 'gold',
90
+ tiles,
91
+ // tall grass, painted on the map: what is in it is hidden from those outside (features-plan G)
92
+ cellLayers: [
93
+ defineCellLayer({
94
+ id: 'bush',
95
+ name: 'Bushes',
96
+ description: 'Tall grass: who stands in it is hidden from those outside it',
97
+ color: [0.2, 0.75, 0.25]
98
+ }),
99
+ // spoiled ground round the Restless' halls and cursed mines, that its buildings go up on (features-plan C5)
100
+ blightLayer
101
+ ],
102
+ systems: [blightSystem],
103
+ editor: meleeEditor,
104
+ // maps made by a generator: the ground, the starts, the mines and their camps, the woods (`generator.ts`)
105
+ generator: meleeGenerator,
106
+ // the maps it ships, the Bay: a save of a game on it keeps only what changed of its ground
107
+ maps: Object.values(maps),
108
+ // the file the editor's Pings fold writes, whose kinds the rules let go (`./pings`)
109
+ files: [pingsFile],
110
+ upkeep: [
111
+ { above: 50, name: 'Low upkeep', income: 0.7 },
112
+ { above: 80, name: 'High upkeep', income: 0.4 }
113
+ ],
114
+ buffs: [...buffs, ...heroSpellBuffs, ...undeadBuffs],
115
+ // as in classic RTS games: eight minutes from one morning to the next, starting at eight
116
+ day: { length: 480, start: 8, dawn: 6, dusk: 18 },
117
+ // heroes, creeps' camps that guard their ground and sleep there at night, shops and taverns, as in classic RTS games
118
+ // who stands in the bushes is seen only from the same bush, or from close by; points to capture; units' stances
119
+ // the heroes' stats: ability power, crits, penetration, life steal, tenacity, haste (a hero arena game's)
120
+ // pieces of wall laid in a line and joined (`stoneWall`); units that are not heroes ranking up by their kills, a
121
+ // lane's minions not: a wave is as strong as the one before it
122
+ packs: [
123
+ withHeroes(),
124
+ withConcealment({ layer: 'bush' }),
125
+ withCapture(),
126
+ withStances(),
127
+ withMobaStats(),
128
+ withWalls(),
129
+ withVeterancy({ earns: (_world, entity) => entity.alive && !entity.isStructure && !entity.has(heroState) && !entity.has(laneMinion) }),
130
+ // a fallen building leaves ruins of its size where it stood, to clear for their lumber (`ruins`); not a wall's
131
+ // piece, nor what stands on something (a cursed mine on its gold mine), nor what nothing can fell
132
+ withRemains({ rubble: (_world, dead) => ruinsOf(dead) }),
133
+ // as in classic RTS games: a unit told to attack (A, then a click) one of its own or an ally's does; a right click never
134
+ withFriendlyFire()
135
+ ],
136
+ // what a player of no race starts with: the humans' start
137
+ start: HUMAN_START,
138
+ // its computer players play by the engine's profiles: on a server, each seat the computer plays gets a difficulty
139
+ // and a personality to choose in the lobby (`seatSettingsOf`; `againstTheRest` reads them)
140
+ ai: {},
141
+ // the classic damage table: each attack type against each armour type (`units.ts`, `undead.ts` name them)
142
+ rules: {
143
+ // the pings the view draws (`./pings`), and those the editor's Pings fold added (`pings.json`)
144
+ talk: { pings: { kinds: PING_KINDS } },
145
+ // the time left of a hero's buffs to everyone who sees it, for the plate's sweeps (indicators-plan U3): a hero's
146
+ // alone, not every footman's under an aura
147
+ hud: { shareBuffTimes: (_world, entity) => entity.has(heroState) },
148
+ // the classic score screen: everything the engine counts, a point of the graph every 10 seconds; the side's numbers
149
+ // while it goes on, everyone's at the end
150
+ score: presetScore(),
151
+ // classic melee maps: the whole map explored from the start, the fog over it all the same: the ground dim,
152
+ // trees, mines and neutral buildings seen as they stand; units, and the other players' buildings, only once in
153
+ // sight. A map says otherwise (Map › Starts explored)
154
+ fog: { startExplored: true },
155
+ // allies trade as in a classic allies menu: what one sends arrives whole (`give`, the Allies panel)
156
+ economy: { tradeTax: 0 },
157
+ combat: {
158
+ damageTable: {
159
+ attacks: ['normal', 'pierce', 'siege', 'magic', 'chaos', 'hero'],
160
+ armours: ['light', 'medium', 'heavy', 'fortified', 'hero', 'unarmored'],
161
+ table: [
162
+ [1, 1.5, 1, 0.7, 1, 1],
163
+ [2, 0.75, 1, 0.35, 0.5, 1.5],
164
+ [1, 0.5, 1, 1.5, 0.5, 1.5],
165
+ [1.25, 0.75, 2, 0.35, 0.5, 1],
166
+ [1, 1, 1, 1, 1, 1],
167
+ [1, 1, 1, 0.5, 1, 1]
168
+ ]
169
+ }
170
+ }
171
+ },
172
+ components: [blightSpread],
173
+ // a save made before a patch is loaded after it (`saves.ts`)
174
+ saves: meleeSaves,
175
+ // every number and word of every definition can be changed from the editor's Data panel, and is kept in `data.json`
176
+ editable: 'all'
177
+ },
178
+ dataFile
179
+ )
180
+
181
+ // saved while it runs in the dev server: the games and editors of the page take the new version (hot reload)
182
+ if (import.meta.hot) import.meta.hot.accept((module) => hotRules(import.meta.hot, melee, module?.melee))