@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,659 @@
1
+ import type { AbilityLooks, EntityView, ModelDef, Nameplate, UnitHudDef, UnitLook, UnitLooks } from '@v12-engine/client'
2
+ import { units } from '../units'
3
+ import { building } from './buildings'
4
+ import { heroSpellEffects } from './heroSpellLooks'
5
+ import { undeadEffects, undeadLooks } from './undeadLooks'
6
+
7
+ export const effects: AbilityLooks = {
8
+ defend: { color: [0.75, 0.8, 0.9] },
9
+ heal: { color: [1, 0.95, 0.45] },
10
+ innerFire: { color: [1, 0.55, 0.15] },
11
+ dispelMagic: { color: [0.8, 0.9, 1], style: 'burst', options: { count: 70, ring: 3, shape: 'about' } },
12
+ raiseDead: { color: [0.55, 0.85, 0.45], style: 'burst', options: { count: 50, rise: 2.5, shape: 'rise' } },
13
+ mirrorImage: { color: [0.45, 0.65, 1], style: 'burst', options: { count: 40, ring: 1.6, shape: 'about' } },
14
+ slow: { color: [0.6, 0.4, 1] },
15
+ arcaneBlast: { color: [0.45, 0.8, 1] },
16
+ holyLight: { color: [1, 0.95, 0.45] },
17
+ stomp: { color: [0.85, 0.7, 0.45] },
18
+ stunned: { color: [1, 1, 0.3] },
19
+ avatar: { color: [1, 0.85, 0.3] },
20
+ blessedSprint: { color: [1, 0.95, 0.6] },
21
+ flameStrike: { color: [1, 0.45, 0.1] },
22
+ frostBolt: { color: [0.55, 0.85, 1] },
23
+ cataclysm: { color: [1, 0.3, 0.1] },
24
+ clap: { color: [0.85, 0.7, 0.45] },
25
+ tornado: { color: [0.78, 0.86, 0.92], style: 'tornado' },
26
+ groundSmash: { color: [0.8, 0.6, 0.35], style: 'smash' },
27
+ lasso: { color: [0.62, 0.47, 0.27], style: 'tether', until: 'lassoed' },
28
+ lassoed: { color: [0.62, 0.47, 0.27] },
29
+ knockedUp: { color: [0.78, 0.86, 0.92] },
30
+ revealArea: { color: [0.55, 0.85, 1] },
31
+ plantWard: { color: [0.95, 0.9, 0.55] },
32
+ sweep: { color: [0.55, 1, 0.75], style: 'sonar' },
33
+ ...heroSpellEffects,
34
+ ...undeadEffects
35
+ }
36
+
37
+ const SKIN: [number, number, number] = [0.87, 0.68, 0.53]
38
+ const STEEL: [number, number, number] = [0.62, 0.65, 0.7]
39
+ const WOOD: [number, number, number] = [0.45, 0.31, 0.18]
40
+ const STONE: [number, number, number] = [0.66, 0.63, 0.58]
41
+ const STRAW: [number, number, number] = [0.8, 0.66, 0.32]
42
+ const GOLD: [number, number, number] = [1, 0.8, 0.2]
43
+ const DARK: [number, number, number] = [0.2, 0.19, 0.2]
44
+
45
+ // The units, the buildings and the trees are of the shared art (@v12-engine/assets): KayKit's adventurers with their
46
+ // gear, Quaternius's buildings and trees, CC0. Each unit is drawn as its parts while its model loads
47
+
48
+ /** A character of the shared art, its clips KayKit's (the client's names for them by default) */
49
+ const character = (name: string, scale: number, own: Partial<ModelDef> = {}): ModelDef => ({ src: `characters/${name}.glb`, scale, ...own })
50
+ /** A piece of the adventurers' gear in a hand: the right one when not said */
51
+ const hold = (name: string, hand: 'l' | 'r' = 'r') => ({ src: `gear/${name}.glb`, at: `handslot.${hand}` })
52
+
53
+ /**
54
+ * The trees of the Stylized Trees pack a tree of the map is drawn as: its kinds by number (`001`…`200`) go around
55
+ * them. Of the shared art's trees, the ones of a few hundred triangles to a thousand: a map has a forest of them,
56
+ * drawn again for every shadow
57
+ */
58
+ const FOREST = ['oak_5', 'pine_5', 'columnar_1', 'columnar_2', 'columnar_5', 'pine_4']
59
+
60
+ /** The kinds of tree, by number */
61
+ const TREE_LOOKS: Record<string, ModelDef> = Object.fromEntries(
62
+ Array.from({ length: 200 }, (_, i) => {
63
+ const look = String(i + 1).padStart(3, '0')
64
+ const kind = FOREST[i % FOREST.length]
65
+ // about 4 tall: the oaks were made bigger than the rest
66
+ return [look, { src: `trees/${kind}.glb`, scale: kind.startsWith('oak') ? 0.6 : 1 }]
67
+ })
68
+ )
69
+ /** The trees a map gets that does not say which kind: one of each tree of the pack */
70
+ const TREE_MIX = FOREST.map((_, i) => String(i + 1).padStart(3, '0'))
71
+
72
+ /** What was just lost of a bar, shown white a while, draining to it */
73
+ const GHOST = { seconds: 0.6 }
74
+
75
+ /** A hero's plate: with its health's points on the bar where `points` */
76
+ const heroPlate = (points: boolean): Nameplate => ({
77
+ show: 'always',
78
+ gap: 1,
79
+ items: [
80
+ {
81
+ row: [
82
+ // its picture in its side's colour (the type's, `icons.units`; a letter while melee has no pictures)
83
+ { portrait: true, size: 26, border: 'owner' },
84
+ {
85
+ column: [
86
+ { text: (entity) => entity.def.name, size: 12, color: 'owner' },
87
+ {
88
+ row: [
89
+ { badge: (entity) => entity.level ?? 1, size: 16, border: 'owner' },
90
+ {
91
+ column: [
92
+ { bar: 'health', width: 84, height: points ? 10 : 8, ticks: 100, ghost: GHOST, ...(points && { label: 'value' as const }) },
93
+ { bar: 'mana', width: 84, height: 3 }
94
+ ],
95
+ gap: 1
96
+ }
97
+ ],
98
+ gap: 2
99
+ }
100
+ ],
101
+ gap: 1
102
+ }
103
+ ],
104
+ gap: 3
105
+ },
106
+ // what an enemy is not told: said so
107
+ { abilities: 'own', size: 14, gap: 2, for: 'allies' },
108
+ // its buffs, a letter each (melee has no pictures of them), with the time left swept off: the rules share it (`hud`)
109
+ { buffs: 'all', size: 12, gap: 2, dots: true }
110
+ ]
111
+ })
112
+
113
+ /**
114
+ * Over a hero, the way a hero arena game draws it (indicators-plan U2, D2): its picture in its side's colour; beside it the
115
+ * name on top, under it the level in a circle beside the health, with a mark every 100 points and what was just lost
116
+ * draining white, and a thin mana bar under that; under those its abilities, darkened by their cooldowns, for the
117
+ * player's own and its allies' alone, and its buffs with the time they have left, for everyone (the rules share it).
118
+ * Selected, the health's points on its bar. Always there, as a MOBA's are, faded in as it comes into sight
119
+ */
120
+ const heroHud: UnitHudDef = {
121
+ plates: [{ when: { selected: true }, ...heroPlate(true) }, heroPlate(false)]
122
+ }
123
+
124
+ /** Whether a building of the player's sends what it makes somewhere: its own side is told (`rally`) */
125
+ const rallies = (entity: EntityView) => entity.rally !== undefined
126
+
127
+ /**
128
+ * Over a building (D2): going up, how far it is and its health, for everyone who sees it; making something, how far
129
+ * that is, for its side; else selected, the player's own with a rally says so; else its health when hurt (or every bar is
130
+ * asked for), what was just lost draining white. The other units' plates are data, `hud.json`
131
+ */
132
+ const buildingHud: UnitHudDef = {
133
+ plates: [
134
+ {
135
+ when: { constructing: true },
136
+ show: 'always',
137
+ items: [
138
+ { bar: 'progress', height: 4 },
139
+ { bar: 'health', ghost: GHOST }
140
+ ]
141
+ },
142
+ {
143
+ when: { relation: ['self', 'ally'], of: (entity) => entity.queue.length > 0 },
144
+ show: 'always',
145
+ items: [
146
+ { bar: 'health', ghost: GHOST },
147
+ { bar: 'progress', height: 3 }
148
+ ]
149
+ },
150
+ {
151
+ when: { relation: ['self'], selected: true, of: rallies },
152
+ items: [
153
+ { bar: 'health', ghost: GHOST, label: 'value', height: 9 },
154
+ { text: '⚑ Rally', size: 11, color: '#ffd23c' }
155
+ ]
156
+ },
157
+ { show: 'hurt', items: [{ bar: 'health', ghost: GHOST }] }
158
+ ]
159
+ }
160
+
161
+ /** Over a tree or a gold mine: nothing, whatever is asked (every bar shown, `all`, leaves the forest alone) */
162
+ const noHud: UnitHudDef = { plates: [] }
163
+
164
+ export const looks: UnitLooks = {
165
+ peasant: {
166
+ height: 1.5,
167
+ model: character('barbarian', 0.58, { hold: [hold('axe_1handed')] }),
168
+ parts: [
169
+ { shape: 'cylinder', size: [0.5, 0.8, 0.5], offset: [0, 0.4, 0], color: 'team' },
170
+ { shape: 'sphere', size: [0.38, 0.38, 0.38], offset: [0, 0.98, 0], color: SKIN },
171
+ { shape: 'cone', size: [0.5, 0.22, 0.5], offset: [0, 1.22, 0], color: STRAW },
172
+ { shape: 'box', size: [0.08, 0.08, 0.6], offset: [0.3, 0.6, 0.15], color: WOOD, tilt: -0.6 },
173
+ { shape: 'sphere', size: [0.42, 0.36, 0.42], offset: [0, 0.75, -0.34], color: GOLD, carrying: 'gold' },
174
+ {
175
+ shape: 'cylinder',
176
+ size: [0.22, 0.9, 0.22],
177
+ offset: [0, 0.8, -0.34],
178
+ color: WOOD,
179
+ tilt: Math.PI / 2,
180
+ turn: Math.PI / 2,
181
+ carrying: 'lumber'
182
+ }
183
+ ]
184
+ },
185
+ footman: {
186
+ height: 1.8,
187
+ model: character('knight', 0.7, { hold: [hold('sword_1handed'), hold('shield_round', 'l')] }),
188
+ // armies of them: drawn as copies of one mesh, their animations baked (S41)
189
+ baked: true,
190
+ parts: [
191
+ { shape: 'box', size: [0.62, 1, 0.42], offset: [0, 0.5, 0], color: 'team' },
192
+ { shape: 'sphere', size: [0.42, 0.42, 0.42], offset: [0, 1.22, 0], color: STEEL },
193
+ { shape: 'box', size: [0.1, 0.6, 0.5], offset: [-0.4, 0.65, 0.12], color: STEEL },
194
+ { shape: 'box', size: [0.07, 0.07, 0.85], offset: [0.4, 0.75, 0.3], color: STEEL, tilt: -0.35 }
195
+ ]
196
+ },
197
+ rifleman: {
198
+ height: 1.8,
199
+ model: character('ranger', 0.7, { hold: [hold('crossbow_2handed')], animations: { attack: 'Ranged_2H_Shoot' }, hitAt: 0.3 }),
200
+ parts: [
201
+ { shape: 'cylinder', size: [0.56, 0.95, 0.56], offset: [0, 0.48, 0], color: 'team' },
202
+ { shape: 'sphere', size: [0.4, 0.4, 0.4], offset: [0, 1.17, 0], color: SKIN },
203
+ { shape: 'cylinder', size: [0.5, 0.12, 0.5], offset: [0, 1.36, 0], color: WOOD },
204
+ { shape: 'box', size: [0.08, 0.08, 1.2], offset: [0.28, 0.85, 0.45], color: DARK }
205
+ ]
206
+ },
207
+ catapult: {
208
+ // a machine: no blood
209
+ blood: false,
210
+ height: 2.4,
211
+ // its shots are rocks, thrown high
212
+ shot: { color: STONE, size: 0.5, arc: 0.4 },
213
+ parts: [
214
+ { shape: 'box', size: [1.1, 0.3, 1.7], offset: [0, 0.5, 0], color: WOOD },
215
+ { shape: 'box', size: [1.2, 0.12, 0.5], offset: [0, 0.72, -0.4], color: 'team' },
216
+ { shape: 'cylinder', size: [0.6, 0.16, 0.6], offset: [0.64, 0.3, 0.55], color: DARK, tilt: Math.PI / 2, turn: Math.PI / 2 },
217
+ { shape: 'cylinder', size: [0.6, 0.16, 0.6], offset: [-0.64, 0.3, 0.55], color: DARK, tilt: Math.PI / 2, turn: Math.PI / 2 },
218
+ { shape: 'cylinder', size: [0.6, 0.16, 0.6], offset: [0.64, 0.3, -0.55], color: DARK, tilt: Math.PI / 2, turn: Math.PI / 2 },
219
+ { shape: 'cylinder', size: [0.6, 0.16, 0.6], offset: [-0.64, 0.3, -0.55], color: DARK, tilt: Math.PI / 2, turn: Math.PI / 2 },
220
+ { shape: 'box', size: [0.14, 1.1, 0.14], offset: [0.42, 1.15, 0.35], color: WOOD },
221
+ { shape: 'box', size: [0.14, 1.1, 0.14], offset: [-0.42, 1.15, 0.35], color: WOOD },
222
+ { shape: 'box', size: [1, 0.14, 0.14], offset: [0, 1.7, 0.35], color: WOOD },
223
+ { shape: 'box', size: [0.14, 0.14, 1.9], offset: [0, 1.35, -0.2], color: WOOD, tilt: 0.55 },
224
+ { shape: 'sphere', size: [0.5, 0.4, 0.5], offset: [0, 1.95, -1.05], color: STONE }
225
+ ]
226
+ },
227
+ gryphon: {
228
+ height: 1.9,
229
+ altitude: 3.2,
230
+ shot: { color: [0.55, 0.8, 1], size: 0.3 },
231
+ parts: [
232
+ { shape: 'sphere', size: [0.8, 0.7, 1.5], offset: [0, 0.7, 0], color: [0.72, 0.58, 0.36] },
233
+ { shape: 'sphere', size: [0.5, 0.5, 0.6], offset: [0, 1.05, 0.85], color: [0.93, 0.91, 0.84] },
234
+ { shape: 'cone', size: [0.2, 0.4, 0.2], offset: [0, 0.98, 1.25], color: GOLD, tilt: Math.PI / 2 },
235
+ { shape: 'box', size: [1.7, 0.08, 0.8], offset: [1.1, 0.95, 0], color: [0.6, 0.47, 0.28], turn: 0.25 },
236
+ { shape: 'box', size: [1.7, 0.08, 0.8], offset: [-1.1, 0.95, 0], color: [0.6, 0.47, 0.28], turn: -0.25 },
237
+ { shape: 'cone', size: [0.5, 0.9, 0.5], offset: [0, 0.65, -1], color: [0.6, 0.47, 0.28], tilt: -Math.PI / 2 },
238
+ { shape: 'box', size: [0.45, 0.7, 0.35], offset: [0, 1.35, -0.1], color: 'team' },
239
+ { shape: 'sphere', size: [0.34, 0.34, 0.34], offset: [0, 1.85, -0.1], color: STEEL }
240
+ ]
241
+ },
242
+ goblinShipyard: {
243
+ height: 3,
244
+ model: building('rts/dock.glb', 3),
245
+ parts: [
246
+ { shape: 'box', size: [3, 0.4, 3], offset: [0, 0.2, 0], color: WOOD },
247
+ { shape: 'box', size: [1.6, 1.4, 1.6], offset: [-0.6, 1.1, -0.6], color: WOOD },
248
+ { shape: 'pyramid', size: [1.9, 0.8, 1.9], offset: [-0.6, 2.2, -0.6], color: 'team' },
249
+ { shape: 'cylinder', size: [0.15, 2.4, 0.15], offset: [1, 1.4, 1], color: DARK }
250
+ ]
251
+ },
252
+ // a hull, a deck house and a sail in the side's colour: drawn of parts, there is no boat among the shared art
253
+ transportShip: {
254
+ height: 2.6,
255
+ parts: [
256
+ { shape: 'box', size: [1.4, 0.6, 2.8], offset: [0, 0.3, 0], color: WOOD },
257
+ { shape: 'cone', size: [1.4, 0.8, 0.6], offset: [0, 0.3, 1.6], color: WOOD, tilt: Math.PI / 2 },
258
+ { shape: 'box', size: [0.9, 0.6, 0.9], offset: [0, 0.9, -0.6], color: STRAW },
259
+ { shape: 'cylinder', size: [0.12, 2, 0.12], offset: [0, 1.6, 0.3], color: DARK },
260
+ { shape: 'box', size: [1.2, 1.1, 0.05], offset: [0, 1.8, 0.35], color: 'team' }
261
+ ]
262
+ },
263
+ aviary: {
264
+ height: 4,
265
+ model: building('rts/stone_tower.glb', 2.6),
266
+ parts: [
267
+ { shape: 'cylinder', size: [2.6, 1.2, 2.6], offset: [0, 0.6, 0], color: STONE },
268
+ { shape: 'cylinder', size: [0.4, 2.6, 0.4], offset: [0.9, 2.2, 0.9], color: WOOD },
269
+ { shape: 'cylinder', size: [0.4, 2.6, 0.4], offset: [-0.9, 2.2, 0.9], color: WOOD },
270
+ { shape: 'cylinder', size: [0.4, 2.6, 0.4], offset: [0.9, 2.2, -0.9], color: WOOD },
271
+ { shape: 'cylinder', size: [0.4, 2.6, 0.4], offset: [-0.9, 2.2, -0.9], color: WOOD },
272
+ { shape: 'cone', size: [3.2, 1, 3.2], offset: [0, 3.9, 0], color: 'team' },
273
+ { shape: 'sphere', size: [0.9, 0.5, 0.9], offset: [0, 1.35, 0], color: STRAW }
274
+ ]
275
+ },
276
+ priest: {
277
+ height: 1.8,
278
+ model: character('mage', 0.7, { hold: [hold('staff')], animations: { attack: 'Ranged_Magic_Shoot' } }),
279
+ parts: [
280
+ { shape: 'cone', size: [0.8, 1.05, 0.8], offset: [0, 0.52, 0], color: [0.93, 0.91, 0.84] },
281
+ { shape: 'box', size: [0.3, 0.5, 0.3], offset: [0, 0.75, 0], color: 'team' },
282
+ { shape: 'sphere', size: [0.38, 0.38, 0.38], offset: [0, 1.2, 0], color: SKIN },
283
+ { shape: 'box', size: [0.06, 1.5, 0.06], offset: [0.38, 0.75, 0.1], color: WOOD },
284
+ { shape: 'sphere', size: [0.2, 0.2, 0.2], offset: [0.38, 1.55, 0.1], color: GOLD }
285
+ ]
286
+ },
287
+ sorceress: {
288
+ height: 1.9,
289
+ model: character('mage', 0.72, { hold: [hold('wand')], animations: { attack: 'Ranged_Magic_Shoot' } }),
290
+ parts: [
291
+ { shape: 'cone', size: [0.85, 1.05, 0.85], offset: [0, 0.52, 0], color: 'team' },
292
+ { shape: 'sphere', size: [0.38, 0.38, 0.38], offset: [0, 1.2, 0], color: SKIN },
293
+ { shape: 'cone', size: [0.5, 0.55, 0.5], offset: [0, 1.6, 0], color: [0.4, 0.25, 0.6] },
294
+ { shape: 'sphere', size: [0.22, 0.22, 0.22], offset: [0.4, 0.95, 0.2], color: [0.6, 0.85, 1] }
295
+ ]
296
+ },
297
+ paladin: {
298
+ height: 2.3,
299
+ model: character('knight', 0.9, {
300
+ hold: [hold('sword_2handed')],
301
+ animations: { attack: 'Melee_2H_Attack_Chop', work: 'Melee_2H_Attack_Chop' },
302
+ // the sprint goes off without a gesture: the Paladin keeps running
303
+ casts: { blessedSprint: '' }
304
+ }),
305
+ parts: [
306
+ { shape: 'box', size: [0.8, 1.25, 0.55], offset: [0, 0.62, 0], color: 'team' },
307
+ { shape: 'box', size: [1.1, 0.3, 0.6], offset: [0, 1.25, 0], color: GOLD },
308
+ { shape: 'sphere', size: [0.48, 0.48, 0.48], offset: [0, 1.62, 0], color: STEEL },
309
+ { shape: 'box', size: [0.14, 0.14, 1.1], offset: [0.55, 0.9, 0.35], color: WOOD, tilt: -0.5 },
310
+ { shape: 'box', size: [0.4, 0.4, 0.4], offset: [0.55, 1.18, 0.85], color: STEEL },
311
+ { shape: 'box', size: [0.1, 1.1, 0.75], offset: [0, 0.8, -0.34], color: GOLD }
312
+ ]
313
+ },
314
+ archmage: {
315
+ height: 2.4,
316
+ model: character('mage', 0.92, { hold: [hold('staff')], animations: { attack: 'Ranged_Magic_Shoot' } }),
317
+ parts: [
318
+ { shape: 'cone', size: [1, 1.35, 1], offset: [0, 0.67, 0], color: 'team' },
319
+ { shape: 'sphere', size: [0.44, 0.44, 0.44], offset: [0, 1.5, 0], color: SKIN },
320
+ { shape: 'cone', size: [0.7, 0.8, 0.7], offset: [0, 1.98, 0], color: GOLD },
321
+ { shape: 'box', size: [0.07, 1.9, 0.07], offset: [0.5, 0.95, 0.15], color: WOOD },
322
+ { shape: 'sphere', size: [0.3, 0.3, 0.3], offset: [0.5, 1.95, 0.15], color: [0.5, 0.85, 1] }
323
+ ]
324
+ },
325
+ testiro: {
326
+ height: 2.6,
327
+ model: character('mage', 1, { hold: [hold('spellbook_open', 'l')], animations: { attack: 'Ranged_Magic_Shoot' } }),
328
+ shot: { color: [0.75, 0.9, 1], size: 0.26 },
329
+ parts: [
330
+ { shape: 'cone', size: [0.95, 1.2, 0.95], offset: [0, 0.6, 0], color: 'team' },
331
+ { shape: 'cylinder', size: [0.5, 0.5, 0.5], offset: [0, 1.15, 0], color: [0.78, 0.86, 0.92] },
332
+ { shape: 'sphere', size: [0.42, 0.42, 0.42], offset: [0, 1.55, 0], color: SKIN },
333
+ { shape: 'cylinder', size: [0.9, 0.08, 0.9], offset: [0, 1.78, 0], color: WOOD },
334
+ { shape: 'cone', size: [0.5, 0.35, 0.5], offset: [0, 1.95, 0], color: WOOD },
335
+ { shape: 'cylinder', size: [0.5, 0.06, 0.5], offset: [0.42, 0.95, 0.1], color: [0.62, 0.47, 0.27], tilt: Math.PI / 2 },
336
+ { shape: 'box', size: [0.07, 1.7, 0.07], offset: [-0.45, 0.85, 0.15], color: WOOD },
337
+ { shape: 'cone', size: [0.3, 0.35, 0.3], offset: [-0.45, 1.85, 0.15], color: [0.78, 0.86, 0.92] }
338
+ ]
339
+ },
340
+ ward: {
341
+ // a flag on a stick: no blood
342
+ blood: false,
343
+ height: 1.4,
344
+ parts: [
345
+ { shape: 'cylinder', size: [0.06, 1.3, 0.06], offset: [0, 0.65, 0], color: WOOD },
346
+ { shape: 'box', size: [0.04, 0.35, 0.5], offset: [0, 1.1, 0.25], color: 'team' }
347
+ ]
348
+ },
349
+ skeleton: {
350
+ // bones: no blood
351
+ blood: false,
352
+ height: 1.6,
353
+ model: character('skeleton_minion', 0.62, { hold: [{ src: 'skeleton-gear/blade.glb', at: 'handslot.r' }] }),
354
+ parts: [
355
+ { shape: 'cylinder', size: [0.5, 1, 0.5], offset: [0, 0.5, 0], color: [0.86, 0.83, 0.74] },
356
+ { shape: 'sphere', size: [0.4, 0.4, 0.4], offset: [0, 1.2, 0], color: [0.86, 0.83, 0.74] }
357
+ ]
358
+ },
359
+ altar: {
360
+ height: 3.2,
361
+ model: building('rts/temple_3.glb', 2.9),
362
+ parts: [
363
+ { shape: 'box', size: [2.8, 0.5, 2.8], offset: [0, 0.25, 0], color: STONE },
364
+ { shape: 'box', size: [2, 0.5, 2], offset: [0, 0.75, 0], color: STONE },
365
+ { shape: 'cylinder', size: [0.35, 1.8, 0.35], offset: [1, 1.4, 1], color: STONE },
366
+ { shape: 'cylinder', size: [0.35, 1.8, 0.35], offset: [-1, 1.4, 1], color: STONE },
367
+ { shape: 'cylinder', size: [0.35, 1.8, 0.35], offset: [1, 1.4, -1], color: STONE },
368
+ { shape: 'cylinder', size: [0.35, 1.8, 0.35], offset: [-1, 1.4, -1], color: STONE },
369
+ { shape: 'sphere', size: [0.8, 0.8, 0.8], offset: [0, 1.6, 0], color: 'team' }
370
+ ]
371
+ },
372
+ // a sack of straw on a post, with a mark to aim at
373
+ dummy: {
374
+ // straw: no blood
375
+ blood: false,
376
+ height: 2,
377
+ parts: [
378
+ { shape: 'cylinder', size: [0.16, 1.1, 0.16], offset: [0, 0.55, 0], color: WOOD },
379
+ { shape: 'cylinder', size: [0.7, 0.85, 0.55], offset: [0, 1.2, 0], color: [0.78, 0.66, 0.42] },
380
+ { shape: 'box', size: [1.25, 0.12, 0.12], offset: [0, 1.4, 0], color: WOOD },
381
+ { shape: 'sphere', size: [0.46, 0.46, 0.46], offset: [0, 1.85, 0], color: [0.84, 0.74, 0.5] },
382
+ { shape: 'cylinder', size: [0.34, 0.04, 0.34], offset: [0, 1.22, 0.29], color: [0.8, 0.16, 0.12], tilt: Math.PI / 2 },
383
+ { shape: 'cylinder', size: [0.14, 0.05, 0.14], offset: [0, 1.22, 0.3], color: [0.95, 0.92, 0.85], tilt: Math.PI / 2 }
384
+ ]
385
+ },
386
+ gnoll: {
387
+ height: 1.6,
388
+ model: character('rogue_hooded', 0.62, { hold: [hold('axe_1handed')] }),
389
+ parts: [
390
+ { shape: 'box', size: [0.6, 0.85, 0.5], offset: [0, 0.5, 0], color: [0.5, 0.38, 0.22], tilt: 0.25 },
391
+ { shape: 'sphere', size: [0.42, 0.4, 0.5], offset: [0, 1.05, 0.15], color: [0.62, 0.48, 0.3] },
392
+ { shape: 'cone', size: [0.14, 0.3, 0.14], offset: [0.14, 1.32, 0.1], color: [0.62, 0.48, 0.3] },
393
+ { shape: 'cone', size: [0.14, 0.3, 0.14], offset: [-0.14, 1.32, 0.1], color: [0.62, 0.48, 0.3] },
394
+ { shape: 'box', size: [0.1, 0.1, 0.9], offset: [0.38, 0.7, 0.3], color: WOOD, tilt: -0.5 }
395
+ ]
396
+ },
397
+ troll: {
398
+ height: 1.9,
399
+ model: character('skeleton_rogue', 0.72, {
400
+ hold: [{ src: 'skeleton-gear/crossbow.glb', at: 'handslot.r' }],
401
+ animations: { attack: 'Ranged_2H_Shoot' },
402
+ hitAt: 0.3
403
+ }),
404
+ parts: [
405
+ { shape: 'cylinder', size: [0.55, 1.1, 0.55], offset: [0, 0.55, 0], color: [0.3, 0.5, 0.34] },
406
+ { shape: 'sphere', size: [0.4, 0.42, 0.4], offset: [0, 1.3, 0], color: [0.36, 0.58, 0.4] },
407
+ { shape: 'cone', size: [0.2, 0.5, 0.2], offset: [0, 1.7, -0.05], color: [0.8, 0.3, 0.2] },
408
+ { shape: 'box', size: [0.06, 0.06, 1.3], offset: [0.32, 1, 0.3], color: WOOD, tilt: -0.3 }
409
+ ]
410
+ },
411
+ ogre: {
412
+ height: 3,
413
+ model: character('skeleton_warrior', 1.2, {
414
+ hold: [
415
+ { src: 'skeleton-gear/axe.glb', at: 'handslot.r' },
416
+ { src: 'skeleton-gear/shield_large_a.glb', at: 'handslot.l' }
417
+ ]
418
+ }),
419
+ parts: [
420
+ { shape: 'sphere', size: [1.5, 1.6, 1.3], offset: [0, 1, 0], color: [0.62, 0.52, 0.4] },
421
+ { shape: 'sphere', size: [0.7, 0.7, 0.7], offset: [0, 2.1, 0.1], color: [0.68, 0.57, 0.44] },
422
+ { shape: 'box', size: [1.2, 0.35, 1], offset: [0, 0.75, 0], color: [0.35, 0.25, 0.18] },
423
+ { shape: 'cylinder', size: [0.3, 1.6, 0.3], offset: [0.95, 1.3, 0.5], color: WOOD, tilt: -0.9 },
424
+ { shape: 'sphere', size: [0.6, 0.6, 0.6], offset: [0.95, 1.85, 1.15], color: STONE }
425
+ ]
426
+ },
427
+ townhall: {
428
+ height: 4.6,
429
+ model: building('rts/castle.glb', 3.9),
430
+ parts: [
431
+ { shape: 'box', size: [3.6, 1.9, 3.6], offset: [0, 0.95, 0], color: STONE },
432
+ { shape: 'pyramid', size: [4.4, 1.7, 4.4], offset: [0, 2.75, 0], color: 'team' },
433
+ { shape: 'box', size: [1, 1.2, 0.2], offset: [0, 0.6, -1.76], color: WOOD },
434
+ { shape: 'box', size: [0.7, 1.2, 0.7], offset: [1.2, 3, 1.2], color: STONE }
435
+ ]
436
+ },
437
+ keep: {
438
+ height: 5.6,
439
+ model: building('rts/castle_2.glb', 4),
440
+ parts: [
441
+ { shape: 'box', size: [3.7, 2.4, 3.7], offset: [0, 1.2, 0], color: STONE },
442
+ { shape: 'pyramid', size: [4.4, 1.7, 4.4], offset: [0, 3.25, 0], color: 'team' },
443
+ { shape: 'box', size: [1, 1.2, 0.2], offset: [0, 0.6, -1.81], color: WOOD },
444
+ { shape: 'cylinder', size: [1, 3.6, 1], offset: [1.6, 1.8, 1.6], color: STONE },
445
+ { shape: 'cone', size: [1.3, 1, 1.3], offset: [1.6, 4.1, 1.6], color: 'team' }
446
+ ]
447
+ },
448
+ castle: {
449
+ height: 6.6,
450
+ model: building('rts/castle_fortress.glb', 4.2),
451
+ parts: [
452
+ { shape: 'box', size: [3.8, 2.8, 3.8], offset: [0, 1.4, 0], color: STONE },
453
+ { shape: 'pyramid', size: [4.4, 1.9, 4.4], offset: [0, 3.75, 0], color: 'team' },
454
+ { shape: 'box', size: [1.1, 1.4, 0.2], offset: [0, 0.7, -1.86], color: WOOD },
455
+ { shape: 'cylinder', size: [1, 4.4, 1], offset: [1.6, 2.2, 1.6], color: STONE },
456
+ { shape: 'cone', size: [1.3, 1.1, 1.3], offset: [1.6, 4.95, 1.6], color: 'team' },
457
+ { shape: 'cylinder', size: [1, 4.4, 1], offset: [-1.6, 2.2, 1.6], color: STONE },
458
+ { shape: 'cone', size: [1.3, 1.1, 1.3], offset: [-1.6, 4.95, 1.6], color: 'team' },
459
+ { shape: 'cylinder', size: [1, 4.4, 1], offset: [1.6, 2.2, -1.6], color: STONE },
460
+ { shape: 'cone', size: [1.3, 1.1, 1.3], offset: [1.6, 4.95, -1.6], color: 'team' },
461
+ { shape: 'cylinder', size: [1, 4.4, 1], offset: [-1.6, 2.2, -1.6], color: STONE },
462
+ { shape: 'cone', size: [1.3, 1.1, 1.3], offset: [-1.6, 4.95, -1.6], color: 'team' }
463
+ ]
464
+ },
465
+ farm: {
466
+ height: 2.3,
467
+ model: building('rts/small_farm.glb', 2.1),
468
+ parts: [
469
+ { shape: 'box', size: [1.7, 0.9, 1.7], offset: [0, 0.45, 0], color: WOOD },
470
+ { shape: 'pyramid', size: [2.1, 0.95, 2.1], offset: [0, 1.37, 0], color: STRAW },
471
+ { shape: 'box', size: [0.5, 0.12, 0.5], offset: [0, 1.9, 0], color: 'team' }
472
+ ]
473
+ },
474
+ barracks: {
475
+ height: 3.4,
476
+ model: building('rts/barracks.glb', 3),
477
+ parts: [
478
+ { shape: 'box', size: [2.7, 1.6, 2.7], offset: [0, 0.8, 0], color: STONE },
479
+ { shape: 'box', size: [2.9, 0.35, 2.9], offset: [0, 1.75, 0], color: 'team' },
480
+ { shape: 'box', size: [0.5, 0.7, 0.5], offset: [1.15, 2.2, 1.15], color: STONE },
481
+ { shape: 'box', size: [0.5, 0.7, 0.5], offset: [-1.15, 2.2, 1.15], color: STONE },
482
+ { shape: 'box', size: [0.5, 0.7, 0.5], offset: [1.15, 2.2, -1.15], color: STONE },
483
+ { shape: 'box', size: [0.5, 0.7, 0.5], offset: [-1.15, 2.2, -1.15], color: STONE },
484
+ { shape: 'box', size: [0.9, 1.1, 0.2], offset: [0, 0.55, -1.32], color: WOOD }
485
+ ]
486
+ },
487
+ tower: {
488
+ height: 4.4,
489
+ // its arrows leave from its top, not from its middle
490
+ model: building('rts/watch_tower.glb', 1.7, { points: { launch: { offset: [0, 3.6, 0] } } }),
491
+ parts: [
492
+ { shape: 'cylinder', size: [1.5, 2.8, 1.5], offset: [0, 1.4, 0], color: STONE },
493
+ { shape: 'cylinder', size: [1.9, 0.5, 1.9], offset: [0, 3.05, 0], color: STONE },
494
+ { shape: 'cone', size: [2, 1, 2], offset: [0, 3.8, 0], color: 'team' }
495
+ ]
496
+ },
497
+ // a piece of wall, by its shape among those beside it (the engine's walls turn it): the wall, and a tower where it meets
498
+ stoneWall: {
499
+ height: 2,
500
+ model: building('rts/stone_wall.glb', 1),
501
+ looks: {
502
+ single: building('rts/stone_wall_towers.glb', 1),
503
+ end: building('rts/stone_wall.glb', 1),
504
+ straight: building('rts/stone_wall.glb', 1),
505
+ corner: building('rts/stone_wall_towers.glb', 1),
506
+ tee: building('rts/stone_wall_towers.glb', 1),
507
+ cross: building('rts/stone_wall_towers.glb', 1)
508
+ },
509
+ parts: [{ shape: 'box', size: [1, 1.8, 0.8], offset: [0, 0.9, 0], color: STONE }]
510
+ },
511
+ // a gate of the wall, shut or open (the arch without its door: the way through is clear)
512
+ stoneGate: {
513
+ height: 3,
514
+ model: building('rts/castle_gate.glb', 3),
515
+ looks: {
516
+ shut: building('rts/castle_gate.glb', 3),
517
+ open: building('dungeon-kit/wall_gate.glb', 3)
518
+ },
519
+ parts: [{ shape: 'box', size: [3, 1.8, 0.6], offset: [0, 0.9, 0], color: STONE }]
520
+ },
521
+ blacksmith: {
522
+ height: 3.3,
523
+ model: building('village/blacksmith.glb', 2.9),
524
+ parts: [
525
+ { shape: 'box', size: [2.7, 1.4, 2.7], offset: [0, 0.7, 0], color: STONE },
526
+ { shape: 'pyramid', size: [3.1, 1, 3.1], offset: [0, 1.9, 0], color: DARK },
527
+ { shape: 'box', size: [0.6, 1.5, 0.6], offset: [-0.8, 2.2, 0.8], color: STONE },
528
+ { shape: 'box', size: [0.9, 0.35, 0.5], offset: [0.6, 0.18, -1.7], color: DARK },
529
+ { shape: 'box', size: [0.12, 0.9, 0.7], offset: [1.42, 1, 0], color: 'team' }
530
+ ]
531
+ },
532
+ // what is left of a fallen building, by its size: charred beams and stones
533
+ ...Object.fromEntries(
534
+ [2, 3, 4].map((size) => [
535
+ `ruins${size}`,
536
+ {
537
+ height: 0.8,
538
+ parts: [
539
+ { shape: 'box', size: [size * 0.9, 0.25, size * 0.9], offset: [0, 0.12, 0], color: [0.33, 0.3, 0.27] },
540
+ { shape: 'box', size: [size * 0.6, 0.3, 0.25], offset: [0, 0.35, size * 0.15], color: DARK, turn: 0.4 },
541
+ { shape: 'box', size: [0.25, 0.35, size * 0.5], offset: [size * 0.2, 0.4, 0], color: [0.28, 0.2, 0.12], turn: -0.3 },
542
+ { shape: 'box', size: [0.6, 0.45, 0.5], offset: [-size * 0.25, 0.3, -size * 0.2], color: STONE }
543
+ ]
544
+ } satisfies UnitLook
545
+ ])
546
+ ),
547
+ tavern: {
548
+ height: 3.4,
549
+ model: building('rts/houses_2.glb', 3),
550
+ parts: [
551
+ { shape: 'box', size: [2.8, 1.6, 2.6], offset: [0, 0.8, 0], color: WOOD },
552
+ { shape: 'pyramid', size: [3.2, 1.2, 3], offset: [0, 2.2, 0], color: STRAW },
553
+ { shape: 'box', size: [0.8, 1, 0.2], offset: [0, 0.5, -1.35], color: DARK },
554
+ { shape: 'box', size: [0.12, 0.6, 0.6], offset: [1.45, 1.3, 0], color: GOLD }
555
+ ]
556
+ },
557
+ alchemist: {
558
+ height: 3.6,
559
+ model: building('rts/tower_house.glb', 2.8),
560
+ parts: [
561
+ { shape: 'cylinder', size: [2.6, 1.5, 2.6], offset: [0, 0.75, 0], color: [0.78, 0.74, 0.66] },
562
+ { shape: 'cone', size: [3, 1.5, 3], offset: [0, 2.25, 0], color: 'team' },
563
+ { shape: 'sphere', size: [0.55, 0.55, 0.55], offset: [0, 3.15, 0], color: [0.45, 0.95, 0.5] },
564
+ { shape: 'box', size: [0.8, 1, 0.2], offset: [0, 0.5, -1.3], color: WOOD }
565
+ ]
566
+ },
567
+ armory: {
568
+ height: 3.4,
569
+ model: building('rts/archery_building.glb', 2.9),
570
+ parts: [
571
+ { shape: 'box', size: [2.7, 1.5, 2.7], offset: [0, 0.75, 0], color: STONE },
572
+ { shape: 'pyramid', size: [3.1, 1.1, 3.1], offset: [0, 2.05, 0], color: 'team' },
573
+ { shape: 'box', size: [0.9, 1.1, 0.2], offset: [0, 0.55, -1.32], color: WOOD },
574
+ { shape: 'box', size: [0.12, 1.3, 0.12], offset: [1, 0.85, -1.5], color: STEEL },
575
+ { shape: 'box', size: [0.5, 0.12, 0.12], offset: [1, 1.15, -1.5], color: STEEL },
576
+ { shape: 'cylinder', size: [0.7, 0.12, 0.7], offset: [-1, 0.9, -1.45], color: GOLD, tilt: Math.PI / 2 }
577
+ ]
578
+ },
579
+ tree: {
580
+ height: 3,
581
+ // 200 kinds by number, as the maps say them, drawn as the Stylized Trees' oaks, pines and a columnar tree
582
+ looks: TREE_LOOKS,
583
+ mix: TREE_MIX,
584
+ vary: { scale: 0.25, lean: 0.07 },
585
+ parts: [
586
+ { shape: 'cylinder', size: [0.3, 1, 0.3], offset: [0, 0.5, 0], color: [0.36, 0.25, 0.15] },
587
+ { shape: 'cone', size: [1.5, 1.5, 1.5], offset: [0, 1.5, 0], color: [0.14, 0.36, 0.17] },
588
+ { shape: 'cone', size: [1.1, 1.3, 1.1], offset: [0, 2.4, 0], color: [0.17, 0.42, 0.2] }
589
+ ]
590
+ },
591
+ lumbermill: {
592
+ height: 3,
593
+ model: building('village/fantasy_sawmill.glb', 2.9),
594
+ parts: [
595
+ { shape: 'box', size: [2.7, 1.3, 2.4], offset: [0, 0.65, 0], color: WOOD },
596
+ { shape: 'pyramid', size: [3.1, 1.2, 2.8], offset: [0, 1.9, 0], color: 'team' },
597
+ { shape: 'cylinder', size: [0.4, 2.2, 0.4], offset: [-1.1, 0.25, -1.45], color: [0.36, 0.25, 0.15], tilt: Math.PI / 2, turn: Math.PI / 2 },
598
+ { shape: 'cylinder', size: [0.4, 2.2, 0.4], offset: [-1.1, 0.25, -1.9], color: [0.36, 0.25, 0.15], tilt: Math.PI / 2, turn: Math.PI / 2 }
599
+ ]
600
+ },
601
+ goldmine: {
602
+ height: 3.2,
603
+ model: building('rts/mine.glb', 3.9),
604
+ // run out, it caves in: rocks and the last of its gold, flying where there is physics
605
+ debris: [
606
+ { shape: 'box', size: [0.9, 0.7, 0.8], color: [0.36, 0.33, 0.3], count: 9 },
607
+ { shape: 'box', size: [0.5, 0.4, 0.45], color: [0.3, 0.28, 0.26], count: 8 },
608
+ { shape: 'sphere', size: [0.3, 0.25, 0.3], color: GOLD, count: 6 }
609
+ ],
610
+ parts: [
611
+ { shape: 'pyramid', size: [3.9, 2.6, 3.9], offset: [0, 1.3, 0], color: [0.36, 0.33, 0.3] },
612
+ { shape: 'box', size: [1.1, 1.2, 0.4], offset: [0, 0.6, -1.85], color: DARK },
613
+ { shape: 'sphere', size: [0.6, 0.5, 0.6], offset: [1.4, 0.25, -1.9], color: GOLD },
614
+ { shape: 'sphere', size: [0.45, 0.4, 0.45], offset: [-1.5, 0.2, -1.85], color: GOLD },
615
+ { shape: 'sphere', size: [0.5, 0.45, 0.5], offset: [-0.2, 2.2, 0.3], color: GOLD }
616
+ ]
617
+ },
618
+ ...undeadLooks
619
+ }
620
+
621
+ // buildings smoke from the roof when badly hurt and burn when nearly gone (and stop as they are repaired); they come down
622
+ // crumbling into pieces of themselves in a cloud of dust, wherever physics has loaded, else sink into it; dark while
623
+ // they are going up, their lamp over the door lit once they are built
624
+ for (const visual of Object.values(looks)) {
625
+ if (!visual.model?.lights?.some((light) => light.name === 'door')) continue
626
+ visual.damaged = [
627
+ { below: 0.5, fx: 'smoke', at: 'roof', params: { size: 0.8, darkness: 0.6 } },
628
+ { below: 0.25, fx: 'fire', at: 'roof', params: { size: 0.9, smoke: false } }
629
+ ]
630
+ visual.death = { collapse: { look: 'crumble', pieces: 18, dust: true } }
631
+ // it goes up from the ground with a glowing edge, dust as it is laid out and as it is done, sparks off the hammers
632
+ visual.construction = {
633
+ motion: 'reveal',
634
+ stages: [{ at: 0, fx: 'dust' }],
635
+ working: { fx: 'spark-burst', at: 'handslot.r', params: { count: 8, size: 0.6 } },
636
+ done: { fx: 'dust' }
637
+ }
638
+ visual.lights = (entity, declared) => (entity.construction === undefined ? declared : [])
639
+ }
640
+
641
+ // the casters carry a soft light at night, at the hand that holds the staff: it moves with their clips
642
+ for (const caster of ['priest', 'sorceress', 'archmage']) {
643
+ const visual = looks[caster]
644
+ if (visual?.model)
645
+ visual.model = {
646
+ ...visual.model,
647
+ lights: [{ name: 'staff', at: 'handslot.r', offset: [0, 0.35, 0], color: [0.62, 0.76, 1], intensity: 0.9, range: 4, flicker: 0.04 }]
648
+ }
649
+ }
650
+
651
+ // the HUD over units (indicators-plan D2): the heroes' and the buildings' in code (they ask the unit), none over what is
652
+ // gathered; every other unit's is data, `hud.json` (its default, the casters' and the creeps')
653
+ for (const def of units) {
654
+ const visual = looks[def.id]
655
+ if (!visual) continue
656
+ if (def.hero) visual.unitHud = heroHud
657
+ else if (def.kind === 'resource') visual.unitHud = noHud
658
+ else if (def.kind === 'building') visual.unitHud = buildingHud
659
+ }