@v12-engine/create 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +4 -1
  2. package/base/_gitignore +5 -0
  3. package/base/package.json +3 -3
  4. package/base/src/main.ts +36 -10
  5. package/base/src/navigation.worker.ts +7 -0
  6. package/base/vite.config.ts +24 -2
  7. package/cli.mjs +21 -6
  8. package/index.mjs +27 -7
  9. package/layers/desktop/electron/app.mjs +237 -0
  10. package/layers/desktop/electron/config.mjs +16 -0
  11. package/layers/desktop/electron/dev.mjs +63 -0
  12. package/layers/desktop/electron/files.mjs +86 -0
  13. package/layers/desktop/electron/icons/icon.icns +0 -0
  14. package/layers/desktop/electron/icons/icon.png +0 -0
  15. package/layers/desktop/electron/main.mjs +12 -0
  16. package/layers/desktop/electron/package.mjs +75 -0
  17. package/layers/desktop/electron/preload.cjs +41 -0
  18. package/layers/desktop/electron/presentation.m +87 -0
  19. package/layers/desktop/electron/serve.mjs +76 -0
  20. package/layers/desktop/electron/tools.mjs +63 -0
  21. package/layers/desktop/electron/updates.mjs +71 -0
  22. package/layers/desktop/package.json +13 -0
  23. package/layers/desktop/src/desktop.ts +175 -0
  24. package/layers/desktop/src/shell.ts +279 -0
  25. package/layers/desktop/tests/main.test.ts +189 -0
  26. package/layers/desktop/tests/shell.test.ts +152 -0
  27. package/layers/rooms/server.ts +13 -10
  28. package/layers/server/server.ts +4 -5
  29. package/layers/source/vite.config.ts +21 -1
  30. package/package.json +1 -1
  31. package/starters/campaign/README.md +9 -2
  32. package/starters/campaign/layers/rooms/server.ts +3 -3
  33. package/starters/campaign/layers/server/server.ts +3 -5
  34. package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
  35. package/starters/campaign/src/campaign/game.ts +26 -5
  36. package/starters/campaign/src/campaign/index.ts +104 -68
  37. package/starters/campaign/src/campaign/kills.ts +1 -1
  38. package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
  39. package/starters/campaign/src/campaign/missions/siege.ts +6 -6
  40. package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
  41. package/starters/campaign/src/campaign/scenes.ts +8 -7
  42. package/starters/campaign/src/campaign/units.ts +59 -20
  43. package/starters/campaign/src/campaign/view/attacks.ts +19 -0
  44. package/starters/campaign/src/campaign/view/fire.ts +2 -2
  45. package/starters/campaign/src/campaign/view/index.ts +54 -43
  46. package/starters/campaign/src/campaign/waves.ts +18 -17
  47. package/starters/campaign/src/game.ts +8 -1
  48. package/starters/campaign/src/main.ts +38 -19
  49. package/starters/campaign/src/maps/fort.names.ts +2 -2
  50. package/starters/campaign/src/maps/fort.vgmap.json +102 -2
  51. package/starters/campaign/src/maps/homestead.names.ts +2 -2
  52. package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
  53. package/starters/campaign/src/maps/pass.names.ts +2 -2
  54. package/starters/campaign/src/maps/pass.vgmap.json +69 -1
  55. package/starters/campaign/src/waves-panel.ts +2 -1
  56. package/starters/empty/README.md +18 -10
  57. package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
  58. package/starters/empty/src/game.ts +11 -75
  59. package/starters/empty/src/generate.worker.ts +5 -5
  60. package/starters/empty/src/rules.ts +83 -25
  61. package/starters/empty/src/view.ts +82 -0
  62. package/starters/examples/README.md +83 -0
  63. package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
  64. package/starters/examples/package.json +5 -0
  65. package/starters/examples/src/examples/abilities/areas.ts +165 -0
  66. package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
  67. package/starters/examples/src/examples/abilities/buffs.ts +287 -0
  68. package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
  69. package/starters/examples/src/examples/abilities/changing.ts +228 -0
  70. package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
  71. package/starters/examples/src/examples/abilities/control.ts +158 -0
  72. package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
  73. package/starters/examples/src/examples/abilities/damage.ts +156 -0
  74. package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
  75. package/starters/examples/src/examples/abilities/family.ts +29 -0
  76. package/starters/examples/src/examples/abilities/index.ts +32 -0
  77. package/starters/examples/src/examples/abilities/kit.ts +179 -0
  78. package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
  79. package/starters/examples/src/examples/abilities/looks.ts +47 -0
  80. package/starters/examples/src/examples/abilities/missiles.ts +203 -0
  81. package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
  82. package/starters/examples/src/examples/abilities/moves.ts +206 -0
  83. package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
  84. package/starters/examples/src/examples/abilities/recasts.ts +181 -0
  85. package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
  86. package/starters/examples/src/examples/abilities/resources.ts +166 -0
  87. package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
  88. package/starters/examples/src/examples/abilities/summons.ts +296 -0
  89. package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
  90. package/starters/examples/src/examples/abilities/triggers.ts +204 -0
  91. package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
  92. package/starters/examples/src/examples/abilities/vision.ts +137 -0
  93. package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
  94. package/starters/examples/src/examples/animation/index.ts +235 -0
  95. package/starters/examples/src/examples/animation/looks.ts +444 -0
  96. package/starters/examples/src/examples/catalog.ts +52 -0
  97. package/starters/examples/src/examples/cinematics/index.ts +217 -0
  98. package/starters/examples/src/examples/cinematics/looks.ts +476 -0
  99. package/starters/examples/src/examples/cinematics/tour.json +43 -0
  100. package/starters/examples/src/examples/cinematics/village.ts +88 -0
  101. package/starters/examples/src/examples/common/looks.ts +65 -0
  102. package/starters/examples/src/examples/common/units.ts +70 -0
  103. package/starters/examples/src/examples/construction/index.ts +329 -0
  104. package/starters/examples/src/examples/construction/looks.ts +275 -0
  105. package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
  106. package/starters/examples/src/examples/custom-animation/index.ts +190 -0
  107. package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
  108. package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
  109. package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
  110. package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
  111. package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
  112. package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
  113. package/starters/examples/src/examples/custom-fx/index.ts +25 -0
  114. package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
  115. package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
  116. package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
  117. package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
  118. package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
  119. package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
  120. package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
  121. package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
  122. package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
  123. package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
  124. package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
  125. package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
  126. package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
  127. package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
  128. package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
  129. package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
  130. package/starters/examples/src/examples/define.ts +86 -0
  131. package/starters/examples/src/examples/destruction/index.ts +570 -0
  132. package/starters/examples/src/examples/destruction/looks.ts +447 -0
  133. package/starters/examples/src/examples/dialogs/index.ts +383 -0
  134. package/starters/examples/src/examples/dialogs/looks.ts +546 -0
  135. package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
  136. package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
  137. package/starters/examples/src/examples/fx/blood.look.ts +62 -0
  138. package/starters/examples/src/examples/fx/blood.ts +59 -0
  139. package/starters/examples/src/examples/fx/feel.look.ts +86 -0
  140. package/starters/examples/src/examples/fx/feel.ts +57 -0
  141. package/starters/examples/src/examples/fx/fight.ts +28 -0
  142. package/starters/examples/src/examples/fx/floats.look.ts +108 -0
  143. package/starters/examples/src/examples/fx/floats.ts +24 -0
  144. package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
  145. package/starters/examples/src/examples/fx/gallery.ts +23 -0
  146. package/starters/examples/src/examples/fx/index.ts +30 -0
  147. package/starters/examples/src/examples/fx/links.look.ts +83 -0
  148. package/starters/examples/src/examples/fx/links.ts +49 -0
  149. package/starters/examples/src/examples/fx/looks.ts +55 -0
  150. package/starters/examples/src/examples/fx/map.look.ts +93 -0
  151. package/starters/examples/src/examples/fx/map.ts +33 -0
  152. package/starters/examples/src/examples/fx/marks.look.ts +100 -0
  153. package/starters/examples/src/examples/fx/marks.ts +36 -0
  154. package/starters/examples/src/examples/fx/shots.look.ts +92 -0
  155. package/starters/examples/src/examples/fx/shots.ts +71 -0
  156. package/starters/examples/src/examples/fx/spells.look.ts +87 -0
  157. package/starters/examples/src/examples/fx/spells.ts +122 -0
  158. package/starters/examples/src/examples/fx/stage.ts +31 -0
  159. package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
  160. package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
  161. package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
  162. package/starters/examples/src/examples/gui/index.ts +143 -0
  163. package/starters/examples/src/examples/gui/keys.ts +91 -0
  164. package/starters/examples/src/examples/gui/layout.ts +42 -0
  165. package/starters/examples/src/examples/gui/looks.ts +270 -0
  166. package/starters/examples/src/examples/gui/minimap.ts +127 -0
  167. package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
  168. package/starters/examples/src/examples/gui/plates.ts +87 -0
  169. package/starters/examples/src/examples/gui/radial.ts +104 -0
  170. package/starters/examples/src/examples/gui/solidPart.ts +107 -0
  171. package/starters/examples/src/examples/looks.ts +45 -0
  172. package/starters/examples/src/examples/mechanics/battle.ts +51 -0
  173. package/starters/examples/src/examples/mechanics/damage.ts +80 -0
  174. package/starters/examples/src/examples/mechanics/flying.ts +93 -0
  175. package/starters/examples/src/examples/mechanics/formations.ts +58 -0
  176. package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
  177. package/starters/examples/src/examples/mechanics/index.ts +44 -0
  178. package/starters/examples/src/examples/mechanics/looks.ts +289 -0
  179. package/starters/examples/src/examples/mechanics/maze.ts +119 -0
  180. package/starters/examples/src/examples/mechanics/orders.ts +68 -0
  181. package/starters/examples/src/examples/mechanics/selection.ts +30 -0
  182. package/starters/examples/src/examples/moba/index.ts +377 -0
  183. package/starters/examples/src/examples/moba/looks.ts +349 -0
  184. package/starters/examples/src/examples/moba/maps.ts +69 -0
  185. package/starters/examples/src/examples/moba/spells.ts +34 -0
  186. package/starters/examples/src/examples/moba/units.ts +196 -0
  187. package/starters/examples/src/examples/nature/camera.ts +15 -0
  188. package/starters/examples/src/examples/nature/grass.ts +64 -0
  189. package/starters/examples/src/examples/nature/index.ts +327 -0
  190. package/starters/examples/src/examples/nature/land.ts +173 -0
  191. package/starters/examples/src/examples/nature/looks.ts +577 -0
  192. package/starters/examples/src/examples/nature/maps.ts +370 -0
  193. package/starters/examples/src/examples/nature/skies.ts +324 -0
  194. package/starters/examples/src/examples/nature/weather.ts +80 -0
  195. package/starters/examples/src/examples/performance/index.ts +263 -0
  196. package/starters/examples/src/examples/performance/looks.ts +372 -0
  197. package/starters/examples/src/examples/performance/meter.ts +158 -0
  198. package/starters/examples/src/examples/post/grades.ts +86 -0
  199. package/starters/examples/src/examples/post/index.ts +202 -0
  200. package/starters/examples/src/examples/post/looks.ts +884 -0
  201. package/starters/examples/src/examples/post/passes.ts +213 -0
  202. package/starters/examples/src/examples/rts/ai.ts +49 -0
  203. package/starters/examples/src/examples/rts/airlift.ts +43 -0
  204. package/starters/examples/src/examples/rts/base.ts +129 -0
  205. package/starters/examples/src/examples/rts/build.ts +65 -0
  206. package/starters/examples/src/examples/rts/camps.ts +36 -0
  207. package/starters/examples/src/examples/rts/carry.ts +103 -0
  208. package/starters/examples/src/examples/rts/creeps.ts +61 -0
  209. package/starters/examples/src/examples/rts/data.ts +406 -0
  210. package/starters/examples/src/examples/rts/ferry.ts +41 -0
  211. package/starters/examples/src/examples/rts/garrison.ts +50 -0
  212. package/starters/examples/src/examples/rts/harvest.ts +29 -0
  213. package/starters/examples/src/examples/rts/hero.ts +67 -0
  214. package/starters/examples/src/examples/rts/index.ts +77 -0
  215. package/starters/examples/src/examples/rts/looks.ts +721 -0
  216. package/starters/examples/src/examples/rts/shop.ts +46 -0
  217. package/starters/examples/src/examples/rts/tech.ts +39 -0
  218. package/starters/examples/src/examples/rts/train.ts +48 -0
  219. package/starters/examples/src/examples/rts/waterside.ts +185 -0
  220. package/starters/examples/src/examples/showcase/index.ts +714 -0
  221. package/starters/examples/src/examples/showcase/looks.ts +238 -0
  222. package/starters/examples/src/game.ts +17 -0
  223. package/starters/examples/src/main.ts +82 -0
  224. package/starters/examples/src/rules.ts +31 -0
  225. package/starters/examples/src/shell/look.ts +79 -0
  226. package/starters/examples/src/shell/registry.ts +7 -0
  227. package/starters/examples/src/shell/scenes.ts +178 -0
  228. package/starters/examples/src/shell/shell.ts +290 -0
  229. package/starters/examples/src/shell/style.ts +202 -0
  230. package/starters/examples/src/view.ts +50 -0
  231. package/starters/melee/README.md +12 -3
  232. package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
  233. package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
  234. package/starters/melee/src/game.ts +32 -2
  235. package/starters/melee/src/maps/bay.names.ts +2 -2
  236. package/starters/melee/src/maps/bay.vgmap.json +198 -1
  237. package/starters/melee/src/maps.ts +11 -9
  238. package/starters/melee/src/melee/abilities.ts +1 -194
  239. package/starters/melee/src/melee/blight.ts +7 -7
  240. package/starters/melee/src/melee/cellLayers.json +3 -3
  241. package/starters/melee/src/melee/data.json +1 -18
  242. package/starters/melee/src/melee/game.ts +6 -4
  243. package/starters/melee/src/melee/heroes.ts +1025 -0
  244. package/starters/melee/src/melee/index.ts +64 -61
  245. package/starters/melee/src/melee/items.ts +15 -362
  246. package/starters/melee/src/melee/languages/he.json +14 -2
  247. package/starters/melee/src/melee/pings.ts +5 -6
  248. package/starters/melee/src/melee/saves.ts +1 -1
  249. package/starters/melee/src/melee/undead.ts +15 -230
  250. package/starters/melee/src/melee/units.ts +37 -171
  251. package/starters/melee/src/melee/view/attacks.ts +19 -0
  252. package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
  253. package/starters/melee/src/melee/view/index.ts +56 -52
  254. package/starters/melee/src/melee/view/looks.ts +86 -63
  255. package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
  256. package/starters/melee/src/melee/view/walls.ts +1 -1
  257. package/starters.mjs +6 -0
  258. package/v12.mjs +14 -15
  259. package/starters/campaign/src/rules.ts +0 -20
  260. package/starters/empty/src/maps.ts +0 -44
  261. package/starters/empty/src/tiles.ts +0 -12
  262. package/starters/melee/src/melee/heroSpells.ts +0 -38
  263. package/starters/melee/src/rules.ts +0 -24
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The dynamic-map examples on the client's side: a click on the ground sends where it was to the rules (the brush,
3
+ * the paint), the tree and the grass the examples grow, and the buttons. Nothing of the ground is drawn here: the
4
+ * client draws it again by itself as the rules reshape it (a `ground` event, `world.reshapeGround`)
5
+ */
6
+ import type { GameClientApi, UnitLook } from '@v12-engine/client'
7
+ import { foliage, presetFoliage, presetFx } from '@v12-engine/client'
8
+ import { button, type CategoryLooks, type ExampleLook, readout, select, slider, toggle } from '../../shell/look'
9
+ import {
10
+ BRUSHES,
11
+ clearRoad,
12
+ cutRamp,
13
+ GRASS,
14
+ growGrass,
15
+ lower,
16
+ paint,
17
+ PAINTS,
18
+ plantWood,
19
+ raiseLand,
20
+ raiseWall,
21
+ relay,
22
+ road,
23
+ sculpt,
24
+ setFlood,
25
+ sink,
26
+ spreadSnow,
27
+ TREE
28
+ } from './index'
29
+
30
+ const TREES = ['common_tree_1', 'common_tree_2', 'common_tree_3', 'common_tree_5', 'pine_1', 'pine_2', 'pine_3', 'pine_5']
31
+
32
+ const tree: UnitLook = {
33
+ height: 3,
34
+ looks: Object.fromEntries(TREES.map((name) => [name, { src: `forest/${name}.glb`, scale: 0.8 }])),
35
+ mix: TREES,
36
+ vary: { scale: 0.25, lean: 0.06 },
37
+ shapes: [
38
+ { shape: 'cylinder', size: [0.3, 1, 0.3], offset: [0, 0.5, 0], color: [0.45, 0.31, 0.18] },
39
+ { shape: 'cone', size: [1.5, 1.8, 1.5], offset: [0, 1.7, 0], color: [0.22, 0.45, 0.18] }
40
+ ],
41
+ // planted: it grows up out of the ground, leaves shaken off it as it is whole; felled, it falls
42
+ construction: { motion: 'grow', done: { fx: 'leaves' } },
43
+ death: { collapse: { look: 'topple', time: 2.5 }, fx: 'leaves' }
44
+ }
45
+
46
+ /**
47
+ * The camera over a point: how far back and how steeply, turned as it is. Farther than the player may zoom out (48
48
+ * cells), the limit is let out for this example: what it gives back puts it back
49
+ */
50
+ function look(client: GameClientApi, x: number, z: number, distance: number, pitch = 50): () => void {
51
+ if (distance > client.camera.farthest) client.camera.setLimits({ max: distance })
52
+ client.camera.setPose({ x, z, distance, yaw: client.camera.pose().yaw, pitch: (pitch * Math.PI) / 180 })
53
+ return () => client.camera.setLimits({ max: 48 })
54
+ }
55
+
56
+ /** Where on the ground the pointer is, or nothing over the sky */
57
+ function groundUnder(client: GameClientApi, event: PointerEvent): { x: number; z: number } | null {
58
+ const canvas = client.scene.getEngine().getRenderingCanvas()
59
+ if (!canvas) return null
60
+ const rect = canvas.getBoundingClientRect()
61
+ // the client's own (`GameClient.groundAt`): what a click picks; not in `GameClientApi`, so asked for by name
62
+ const picker = client as GameClientApi & { groundAt?: (x: number, y: number) => { x: number; z: number } | null }
63
+ return picker.groundAt?.(event.clientX - rect.left, event.clientY - rect.top) ?? null
64
+ }
65
+
66
+ /**
67
+ * Left clicks on the ground, and drags, while `on()` says so, are `stroke`'s, not the selection's: one stroke where it
68
+ * went down and one every so often as it is dragged. What it gives back stops listening
69
+ */
70
+ function brushClicks(client: GameClientApi, on: () => boolean, stroke: (at: { x: number; z: number }) => void, every = 0.12): () => void {
71
+ const canvas = client.scene.getEngine().getRenderingCanvas()
72
+ let down = false
73
+ let last = 0
74
+ const at = (event: PointerEvent) => {
75
+ const ground = groundUnder(client, event)
76
+ if (!ground) return
77
+ const now = performance.now() / 1000
78
+ if (now - last < every) return
79
+ last = now
80
+ stroke(ground)
81
+ }
82
+ const press = (event: PointerEvent) => {
83
+ if (!on() || event.button !== 0) return
84
+ event.stopImmediatePropagation()
85
+ down = true
86
+ last = 0
87
+ at(event)
88
+ }
89
+ const move = (event: PointerEvent) => {
90
+ if (!down || !on()) return
91
+ event.stopImmediatePropagation()
92
+ at(event)
93
+ }
94
+ const up = (event: PointerEvent) => {
95
+ if (!down) return
96
+ down = false
97
+ event.stopImmediatePropagation()
98
+ }
99
+ canvas?.addEventListener('pointerdown', press, { capture: true })
100
+ canvas?.addEventListener('pointermove', move, { capture: true })
101
+ window.addEventListener('pointerup', up, { capture: true })
102
+ return () => {
103
+ canvas?.removeEventListener('pointerdown', press, { capture: true })
104
+ canvas?.removeEventListener('pointermove', move, { capture: true })
105
+ window.removeEventListener('pointerup', up, { capture: true })
106
+ }
107
+ }
108
+
109
+ /** Where the camera looks: what the buttons act on when nothing is clicked */
110
+ const focus = (client: GameClientApi) => {
111
+ const pose = client.camera.pose()
112
+ return { x: pose.x, z: pose.z }
113
+ }
114
+
115
+ const again = (id: string) =>
116
+ button(
117
+ 'Start again',
118
+ (client) => client.session.send(relay.make({ example: id })),
119
+ 'Puts the ground, its water and what stands on it back as the example started.'
120
+ )
121
+
122
+ /** The sculpting brush, as its controls set it: one for the example, as the page has one example open at a time */
123
+ const brush = { on: true, kind: 'Raise' as (typeof BRUSHES)[number], radius: 4 }
124
+ const painter = { on: true, tile: 'road' as (typeof PAINTS)[number], radius: 2 }
125
+ let seed = 1
126
+
127
+ const trees = (client: GameClientApi) => client.session.snapshot().entities.filter((each) => each.type === TREE && each.health > 0).length
128
+
129
+ const examples: Record<string, ExampleLook> = {
130
+ 'dynamic-map-sculpt': {
131
+ controls: [
132
+ toggle(
133
+ 'Sculpt with the mouse',
134
+ true,
135
+ (_client, on) => (brush.on = on),
136
+ 'While on, a click or a drag on the ground sculpts it with the brush below; off, the mouse selects and orders as in a game.'
137
+ ),
138
+ select(
139
+ 'Brush',
140
+ BRUSHES,
141
+ 'Raise',
142
+ (_client, kind) => (brush.kind = kind as (typeof BRUSHES)[number]),
143
+ 'What the next stroke does: raise a hill or dig a hollow a little more each time, flatten, lift a plateau with cliffs, or sink a pool of water. Click the ground or the button below to use it.'
144
+ ),
145
+ slider(
146
+ 'Size',
147
+ { min: 1, max: 10, step: 0.5, value: 4 },
148
+ (_client, radius) => (brush.radius = radius),
149
+ 'How wide the brush is, in cells: takes effect on the next stroke.'
150
+ ),
151
+ button(
152
+ 'Where the camera looks',
153
+ (client) => client.session.send(sculpt.make({ ...focus(client), radius: brush.radius, brush: brush.kind })),
154
+ 'One stroke of the brush in the middle of the screen, without clicking the ground.'
155
+ ),
156
+ again('dynamic-map-sculpt')
157
+ ],
158
+ ready: (client) => {
159
+ brush.on = true
160
+ const camera = look(client, 24, 23, 44, 48)
161
+ const stop = brushClicks(
162
+ client,
163
+ () => brush.on,
164
+ (at) => client.session.send(sculpt.make({ ...at, radius: brush.radius, brush: brush.kind }))
165
+ )
166
+ return () => {
167
+ stop()
168
+ camera()
169
+ }
170
+ }
171
+ },
172
+ 'dynamic-map-paint': {
173
+ controls: [
174
+ toggle(
175
+ 'Paint with the mouse',
176
+ true,
177
+ (_client, on) => (painter.on = on),
178
+ 'While on, a click or a drag on the ground paints it with the tile below; off, the mouse selects and orders as in a game.'
179
+ ),
180
+ select(
181
+ 'Paint',
182
+ PAINTS,
183
+ 'road',
184
+ (_client, tile) => (painter.tile = tile as (typeof PAINTS)[number]),
185
+ 'Which tile the brush paints: drag on the ground to lay it, its edges blending into the tiles round it.'
186
+ ),
187
+ slider(
188
+ 'Size',
189
+ { min: 0, max: 8, step: 1, value: 2 },
190
+ (_client, radius) => (painter.radius = radius),
191
+ 'How wide the paint brush is, in cells (0 is a single cell): takes effect on the next stroke.'
192
+ ),
193
+ button('A road across', (client) => client.session.send(road.make({})), 'Paints a winding road from the west edge of the field to the east.'),
194
+ button(
195
+ 'Snow spreads',
196
+ (client) => client.session.send(spreadSnow.make(focus(client))),
197
+ 'Snow spreads out from the middle of the screen, a ring of tiles at a time, until it covers the field.'
198
+ ),
199
+ again('dynamic-map-paint')
200
+ ],
201
+ ready: (client) => {
202
+ painter.on = true
203
+ const camera = look(client, 24, 23, 44, 50)
204
+ const stop = brushClicks(
205
+ client,
206
+ () => painter.on,
207
+ (at) => client.session.send(paint.make({ ...at, radius: painter.radius, tile: painter.tile })),
208
+ 0.05
209
+ )
210
+ return () => {
211
+ stop()
212
+ camera()
213
+ }
214
+ }
215
+ },
216
+ 'dynamic-map-water': {
217
+ controls: [
218
+ button(
219
+ 'Flood',
220
+ (client) => client.session.send(setFlood.make({ level: -0.3 })),
221
+ 'Fills the valley up to just under its banks: deep in the middle, a shallow ford in the east that units can wade.'
222
+ ),
223
+ button(
224
+ 'Half full',
225
+ (client) => client.session.send(setFlood.make({ level: -1.3 })),
226
+ 'Sets the water halfway up: the river runs only in the deepest middle, the ford dry.'
227
+ ),
228
+ button('Drain', (client) => client.session.send(setFlood.make({ level: -3 })), 'Drains the valley: the water goes and its bare bed shows.'),
229
+ again('dynamic-map-water')
230
+ ],
231
+ ready: (client) => look(client, 24, 24, 46, 52)
232
+ },
233
+ 'dynamic-map-cliffs': {
234
+ controls: [
235
+ button(
236
+ 'Raise a cliff wall',
237
+ (client) => client.session.send(raiseWall.make({})),
238
+ 'Lifts a band of the field a level up, across it from west to east: cliffs on both sides, only its ends to go round.'
239
+ ),
240
+ button(
241
+ 'Cut a ramp',
242
+ (client) => client.session.send(cutRamp.make({})),
243
+ 'Cuts a ramp through the middle of the wall, up one side, over and down the other: the way across is open.'
244
+ ),
245
+ button('Lower it all', (client) => client.session.send(lower.make({})), 'Takes the wall and the ramp away: the field flat again.'),
246
+ again('dynamic-map-cliffs')
247
+ ],
248
+ ready: (client) => look(client, 24, 24, 48, 50)
249
+ },
250
+ 'dynamic-map-generate': {
251
+ controls: [
252
+ button(
253
+ 'Raise new land',
254
+ (client) => client.session.send(raiseLand.make({ seed })),
255
+ 'Generates land with the seed below and lays it into the sea on the east side: hills, plateaus, ponds and trees.'
256
+ ),
257
+ button(
258
+ 'Another seed',
259
+ (client) => client.session.send(raiseLand.make({ seed: ++seed })),
260
+ 'The next seed: different land in its place, the trees of the old one gone with it.'
261
+ ),
262
+ button('Back to the sea', (client) => client.session.send(sink.make({})), 'Sinks the land again: the sea back where it was.'),
263
+ readout('Seed', () => String(seed), 'The seed the land was made from: the same seed always makes the same land.'),
264
+ readout('Trees', (client) => String(trees(client)))
265
+ ],
266
+ ready: (client) => look(client, 30, 24, 54, 55)
267
+ },
268
+ 'dynamic-map-forest': {
269
+ controls: [
270
+ button(
271
+ 'Plant a wood',
272
+ (client) => client.session.send(plantWood.make({})),
273
+ 'Plants a round wood in the middle of the field: each tree grows up out of the ground.'
274
+ ),
275
+ button('Grow grass', (client) => client.session.send(growGrass.make({})), 'Grass grows over the meadow, eased in over five seconds.'),
276
+ button(
277
+ 'Clear a road',
278
+ (client) => client.session.send(clearRoad.make({})),
279
+ 'Clears a winding road through the wood: its trees fall, its grass goes, the ground is paved, and the footmen walk along it.'
280
+ ),
281
+ again('dynamic-map-forest'),
282
+ readout('Trees', (client) => String(trees(client)))
283
+ ],
284
+ ready: (client) => look(client, 25, 24, 46, 52)
285
+ }
286
+ }
287
+
288
+ export const category: CategoryLooks = {
289
+ looks: examples,
290
+ view: (view) => ({
291
+ ...view,
292
+ units: {
293
+ ...view.units,
294
+ looks: { ...view.units?.looks, [TREE]: tree },
295
+ // no bars over a tree, growing or standing
296
+ hud: { ...view.units?.hud, bars: (entity) => (entity.def.id === TREE ? { hide: true } : view.units?.hud?.bars?.(entity)) }
297
+ },
298
+ ground: { ...view.ground, cellLayers: { ...view.ground?.cellLayers, [GRASS]: foliage(presetFoliage.meadow) } },
299
+ fx: { ...view.fx, defs: { ...presetFx(['leaves', 'dust']), ...view.fx?.defs } }
300
+ })
301
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Blood and what it leaves, on the client's side: the view's `units.blood` (`BloodLook`: its colour, how many drops,
3
+ * the pools and how long they stay) and `bloodFor`, which decides it hit by hit: a death bursts (`blood-burst`), and a
4
+ * skeleton sheds bone dust rather than blood. The marks dry away over their `seconds`; the player's `decals` setting
5
+ * (`client.decalsAmount`) says how much lies on the ground and how long
6
+ */
7
+ import type { BloodLook, UnitLook } from '@v12-engine/client'
8
+ import { button, type ExampleLook, slider, toggle } from '../../shell/look'
9
+ import { character, hold, ONE_HANDED, person } from '../common/looks'
10
+ import { clash, lineUpAgain, SKELETON } from './blood'
11
+ import { aim } from './stage'
12
+
13
+ /** The skeleton: the shared art's warrior, an axe in hand */
14
+ export const skeletonLook: UnitLook = {
15
+ height: 1.8,
16
+ model: character('skeleton_warrior', 0.7, { hold: [hold('axe_1handed')] }),
17
+ attacks: [ONE_HANDED],
18
+ shapes: person(1.8, [0.85, 0.82, 0.72])
19
+ }
20
+
21
+ /** Every blow's blood: dark red drops, a pool where one falls, staying 40 seconds */
22
+ const BLOOD: BloodLook = { color: [0.5, 0.02, 0.03], amount: 20, pool: 1.3, seconds: 40 }
23
+
24
+ export const bloodLook: ExampleLook = {
25
+ view: (view) => ({
26
+ ...view,
27
+ units: {
28
+ ...view.units,
29
+ blood: BLOOD,
30
+ bloodFor: (entity, event) => {
31
+ // bones bleed dust; the rest bleed, and burst as they die
32
+ if (entity.def.id === SKELETON) return { color: [0.82, 0.78, 0.66], amount: 10, pool: 0, spray: 'dust' }
33
+ return event.kind === 'death' ? { ...BLOOD, amount: 70, pool: 1.8, spray: 'blood-burst' } : undefined
34
+ }
35
+ }
36
+ }),
37
+ controls: [
38
+ button(
39
+ 'Fight',
40
+ (client) => client.session.send(clash.make({})),
41
+ 'Both sides go at each other: footmen bleed and leave pools, the skeletons shed bone dust'
42
+ ),
43
+ button(
44
+ 'Line them up again',
45
+ (client) => client.session.send(lineUpAgain.make({})),
46
+ 'Takes everyone away and lines them up again: the stains on the ground stay, drying'
47
+ ),
48
+ toggle('Blood', true, (client, on) => (client.blood = on), 'Off: the same blows land with no drops, no pools and no bursts'),
49
+ slider(
50
+ 'Marks on the ground',
51
+ { min: 0, max: 1, step: 0.05, value: 1 },
52
+ (client, value) => (client.decalsAmount = value),
53
+ "The player's setting for marks on the ground: at 0 no pools are left, higher keeps more and longer"
54
+ )
55
+ ],
56
+ ready: (client) => {
57
+ aim(client, { x: 24, z: 24 }, 18, 0.72)
58
+ client.blood = true
59
+ client.decalsAmount = 1
60
+ client.session.send(clash.make({}))
61
+ }
62
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Blood and the marks it leaves, on the rules' side: footmen against skeletons, a fight to start and to lay out again.
3
+ * The blood is the screen's (`./blood.look.ts`, the view's `units.blood`): the rules only deal the blows
4
+ */
5
+ import { defineCommand, type Game, type World } from '@v12-engine/engine'
6
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
7
+ import { type ExampleDef, exampleField } from '../define'
8
+
9
+ export const SKELETON = 'fxSkeleton'
10
+
11
+ /** Two ranks facing each other across the middle */
12
+ function lineUp(world: World): void {
13
+ for (let i = 0; i < 6; i++) {
14
+ world.spawn(UNIT.footman, { at: { x: 20.5 + i * 1.4, z: 19 }, owner: PLAYER })
15
+ world.spawn(SKELETON, { at: { x: 20.5 + i * 1.4, z: 29 }, owner: ENEMY })
16
+ }
17
+ }
18
+
19
+ /** Both sides go at each other, fighting what they meet */
20
+ export const clash = defineCommand('fx.clash', {}, (world) => {
21
+ for (const unit of world.units.values()) {
22
+ if (!unit.alive) continue
23
+ if (unit.owner === PLAYER) unit.order('attackMove', { x: unit.pos.x, z: 30 })
24
+ if (unit.owner === ENEMY) unit.order('attackMove', { x: unit.pos.x, z: 18 })
25
+ }
26
+ })
27
+
28
+ /** Takes everyone away and lines them up again: the blood on the ground stays, drying */
29
+ export const lineUpAgain = defineCommand('fx.lineUp', {}, (world) => {
30
+ for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
31
+ lineUp(world)
32
+ })
33
+
34
+ export const blood: ExampleDef = {
35
+ id: 'fx-blood',
36
+ category: 'fx',
37
+ title: 'Blood and what it leaves',
38
+ summary:
39
+ 'Footmen against skeletons with blood on: a spray where each blow lands, a burst where one dies, pools and splatters left on the ground to dry. Fight, then line them up again and fight on the stains; turn blood off to see the same fight without.',
40
+ tags: ['blood', 'gore', 'decals', 'splatter', 'pools', 'BloodLook', 'death'],
41
+ map: () => exampleField({ name: 'Field', players: 2, tile: 'dirt' }),
42
+ start: lineUp,
43
+ source: ['examples/fx/blood.ts', 'examples/fx/blood.look.ts']
44
+ }
45
+
46
+ /** Registers the skeletons and the commands */
47
+ export function bloodRules(game: Game<any>): void {
48
+ game.unit({
49
+ id: SKELETON,
50
+ name: 'Skeleton',
51
+ kind: 'unit',
52
+ radius: 0.45,
53
+ stats: { health: 380, attackDamage: 16, attackSpeed: 1, attackRange: 0.5, armor: 8, moveSpeed: 3.6 },
54
+ sight: 10,
55
+ attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }]
56
+ })
57
+ game.command(clash)
58
+ game.command(lineUpAgain)
59
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The feel of a hit, on the client's side: the view's `feel` (`FeelLook`), written out whole so each piece can be
3
+ * read and changed: the flash of the struck, its flinch, the hit-stop, the swing's streak, and the shake and words of a
4
+ * critical strike and a kill. Each piece `false` turns it off; `feel: false` all of it. The player's own settings turn
5
+ * the hit-stop (`client.hitStop`) and the shake down. The card's Flash and Shake play the same by hand (`kit.flash`,
6
+ * `kit.shake`)
7
+ */
8
+ import { type FeelLook, presetFloats, type UnitLook } from '@v12-engine/client'
9
+ import { button, type ExampleLook, slider, toggle } from '../../shell/look'
10
+ import { character, hold, person, TWO_HANDED } from '../common/looks'
11
+ import { UNIT } from '../common/units'
12
+ import { BRUTE, BRUTE_AT, strike } from './feel'
13
+ import { fight } from './fight'
14
+ import { aim, idsOf, where } from './stage'
15
+
16
+ /** The brute: the knight, bigger, a two-handed sword leaving a warm streak */
17
+ export const bruteLook: UnitLook = {
18
+ height: 2.3,
19
+ model: character('knight', 0.95, { hold: [hold('sword_2handed')], animations: { attack: 'Melee_2H_Attack_Chop' } }),
20
+ attacks: [{ ...TWO_HANDED, trail: { color: [1, 0.78, 0.4], width: 0.3, seconds: 0.24, alpha: 0.85 } }],
21
+ shapes: person(2.3, [0.6, 0.6, 0.65])
22
+ }
23
+
24
+ /** The feel, every piece of it: a little more than the client's own, so it reads */
25
+ const FEEL: FeelLook = {
26
+ // the struck goes white a moment, warm and longer on a critical strike
27
+ flash: { color: [1, 1, 1], time: 0.12, strength: 0.45, crit: { color: [1, 0.75, 0.4], time: 0.2, strength: 0.85 } },
28
+ // a flinch clip where it stands, a nudge away where it is busy; a heavy hit is 8% of its health or more
29
+ reaction: { clip: 'Hit_A', nudge: 0.18, heavy: 0.08, crits: true },
30
+ // seconds both pause: a critical strike, a kill, a heavy hit
31
+ hitStop: { crit: 0.09, kill: 0.13, heavy: 0.03 },
32
+ // the streak behind every melee swing, where an attack's look says none of its own
33
+ trails: { color: [1, 1, 1], width: 0.14, seconds: 0.18, melee: true },
34
+ // the view shaken, and words over the struck
35
+ crit: { shake: 0.25, float: 'Crit!', who: 'all' },
36
+ kill: { shake: 0.4, who: 'all' }
37
+ }
38
+
39
+ export const feelLook: ExampleLook = {
40
+ view: (view) => ({ ...view, feel: FEEL, hud: { ...view.hud, floats: { ...presetFloats(['damage', 'crit']), ...view.hud?.floats } } }),
41
+ controls: [
42
+ button('Strike', (client) => client.session.send(strike.make({ kind: 'hit' })), 'The brute strikes the dummy once: a flash and a flinch'),
43
+ button(
44
+ 'Critical strike',
45
+ (client) => client.session.send(strike.make({ kind: 'crit' })),
46
+ 'A critical strike: a warmer flash, a short pause, a shake and words'
47
+ ),
48
+ button(
49
+ 'Killing blow',
50
+ (client) => client.session.send(strike.make({ kind: 'kill' })),
51
+ 'A footman brought and cut down: the longest pause, the hardest shake'
52
+ ),
53
+ toggle(
54
+ 'Keep swinging',
55
+ false,
56
+ (client, on) => client.session.send(fight.make({ on })),
57
+ 'The brute attacks the dummy over and over: watch the streak behind each swing'
58
+ ),
59
+ slider(
60
+ 'Hit-stop',
61
+ { min: 0, max: 1, step: 0.05, value: 1 },
62
+ (client, value) => (client.hitStop = value),
63
+ "The player's setting for the pause on heavy blows: at 0 there is none, at 1 the whole of what the game asks"
64
+ ),
65
+ button(
66
+ 'Flash',
67
+ (client) => {
68
+ for (const id of idsOf(client, UNIT.dummy)) client.renderer.kit.flash(id, [0.4, 0.8, 1], 0.3)
69
+ },
70
+ 'Flashes the dummies blue by hand (kit.flash)'
71
+ ),
72
+ button(
73
+ 'Shake',
74
+ (client) => {
75
+ const [brute] = idsOf(client, BRUTE)
76
+ const at = (brute !== undefined && where(client, brute)) || BRUTE_AT
77
+ client.renderer.kit.shake(at.x, at.z, 0.8)
78
+ },
79
+ 'Shakes the view by hand (kit.shake): less the farther it is'
80
+ )
81
+ ],
82
+ ready: (client) => {
83
+ aim(client, { x: BRUTE_AT.x, z: BRUTE_AT.z + 1 }, 15, 0.7)
84
+ client.hitStop = 1
85
+ }
86
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The feel of a hit, on the rules' side: a brute and dummies to strike. A blow is damage dealt with `world.damage`
3
+ * (an attack's, a critical one, one that kills); the flash, the flinch, the pause, the shake and the streak of the swing
4
+ * are all the screen's (`./feel.look.ts`, the view's `feel`)
5
+ */
6
+ import { defineCommand, type Game, oneOf } from '@v12-engine/engine'
7
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
8
+ import { type ExampleDef, exampleField } from '../define'
9
+ import { nearestEnemy } from './fight'
10
+
11
+ export const BRUTE = 'fxBrute'
12
+ export const BRUTE_AT = { x: 24, z: 22 }
13
+
14
+ /** The brute strikes the nearest dummy: a plain blow, a critical one, or one that kills a footman brought for it */
15
+ export const strike = defineCommand('fx.strike', { kind: oneOf('hit', 'crit', 'kill') }, (world, _player, { kind }) => {
16
+ const brute = [...world.units.values()].find((unit) => unit.def.id === BRUTE && unit.alive)
17
+ if (!brute) return
18
+ if (kind === 'kill') {
19
+ // a footman to fall, in front of the brute: the blow that kills it is a critical one, the most the screen shows
20
+ const victim = world.spawn(UNIT.footman, { at: { x: brute.pos.x + 1.4, z: brute.pos.z + 0.4 }, owner: ENEMY })
21
+ world.damage(victim, 9999, brute, { attack: true, crit: true })
22
+ return
23
+ }
24
+ const dummy = nearestEnemy(world, brute)
25
+ if (dummy) world.damage(dummy, kind === 'crit' ? 180 : 60, brute, { attack: true, crit: kind === 'crit' })
26
+ })
27
+
28
+ export const feel: ExampleDef = {
29
+ id: 'fx-hit-feel',
30
+ category: 'fx',
31
+ title: 'The feel of a hit',
32
+ summary:
33
+ 'What makes a blow land: the struck flashes and flinches, both pause a few frames on a critical strike or a kill (hit-stop), the view shakes, the swing leaves a streak. Strike, crit or kill, or let the brute swing on his own.',
34
+ tags: ['feel', 'hit-stop', 'flash', 'flinch', 'shake', 'trail', 'crit', 'juice', 'FeelLook'],
35
+ map: () => exampleField({ name: 'Yard', players: 2 }),
36
+ start: (world) => {
37
+ world.spawn(BRUTE, { at: BRUTE_AT, owner: PLAYER })
38
+ world.spawn(UNIT.dummy, { at: { x: BRUTE_AT.x + 1.5, z: BRUTE_AT.z + 1 }, owner: ENEMY })
39
+ world.spawn(UNIT.dummy, { at: { x: BRUTE_AT.x - 2.5, z: BRUTE_AT.z + 2 }, owner: ENEMY })
40
+ },
41
+ source: ['examples/fx/feel.ts', 'examples/fx/feel.look.ts', 'examples/fx/fight.ts']
42
+ }
43
+
44
+ /** Registers the brute and the command */
45
+ export function feelRules(game: Game<any>): void {
46
+ game.unit({
47
+ id: BRUTE,
48
+ name: 'Brute',
49
+ kind: 'unit',
50
+ radius: 0.55,
51
+ stats: { health: 2000, attackDamage: 70, attackSpeed: 0.9, attackRange: 0.8, armor: 20, moveSpeed: 3.5 },
52
+ sight: 10,
53
+ // he strikes when told (`fx.fight`, `fx.strike`), not of himself
54
+ attacks: [{ damagePoint: 0.45, acquisitionRange: 0 }]
55
+ })
56
+ game.command(strike)
57
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * What several of the effects' examples share on the rules' side: a command that sets the player's units on the
3
+ * nearest enemy, or stops them, so that blows land for the screen to show
4
+ */
5
+ import { bool, defineCommand, type Entity, type World, exactMath } from '@v12-engine/engine'
6
+ import { ENEMY, PLAYER } from '../common/units'
7
+
8
+ /** The living enemy nearest a unit */
9
+ export function nearestEnemy(world: World, unit: Entity): Entity | undefined {
10
+ let best: Entity | undefined
11
+ let bestAway = Infinity
12
+ for (const other of world.units.values()) {
13
+ if (!other.alive || other.owner !== ENEMY) continue
14
+ const away = exactMath.hypot(other.pos.x - unit.pos.x, other.pos.z - unit.pos.z)
15
+ if (away < bestAway) [best, bestAway] = [other, away]
16
+ }
17
+ return best
18
+ }
19
+
20
+ /** Every unit of the player's goes for the enemy nearest it (`on`), or stops where it is */
21
+ export const fight = defineCommand('fx.fight', { on: bool }, (world, _player, { on }) => {
22
+ for (const unit of world.units.values()) {
23
+ if (!unit.alive || unit.owner !== PLAYER || !unit.def.attacks?.length) continue
24
+ const enemy = on ? nearestEnemy(world, unit) : undefined
25
+ if (enemy) unit.order('attack', enemy)
26
+ else unit.order('stop')
27
+ }
28
+ })