@v12-engine/create 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +4 -1
  2. package/base/_gitignore +5 -0
  3. package/base/package.json +3 -3
  4. package/base/src/main.ts +36 -10
  5. package/base/src/navigation.worker.ts +7 -0
  6. package/base/vite.config.ts +24 -2
  7. package/cli.mjs +21 -6
  8. package/index.mjs +27 -7
  9. package/layers/desktop/electron/app.mjs +237 -0
  10. package/layers/desktop/electron/config.mjs +16 -0
  11. package/layers/desktop/electron/dev.mjs +63 -0
  12. package/layers/desktop/electron/files.mjs +86 -0
  13. package/layers/desktop/electron/icons/icon.icns +0 -0
  14. package/layers/desktop/electron/icons/icon.png +0 -0
  15. package/layers/desktop/electron/main.mjs +12 -0
  16. package/layers/desktop/electron/package.mjs +75 -0
  17. package/layers/desktop/electron/preload.cjs +41 -0
  18. package/layers/desktop/electron/presentation.m +87 -0
  19. package/layers/desktop/electron/serve.mjs +76 -0
  20. package/layers/desktop/electron/tools.mjs +63 -0
  21. package/layers/desktop/electron/updates.mjs +71 -0
  22. package/layers/desktop/package.json +13 -0
  23. package/layers/desktop/src/desktop.ts +175 -0
  24. package/layers/desktop/src/shell.ts +279 -0
  25. package/layers/desktop/tests/main.test.ts +189 -0
  26. package/layers/desktop/tests/shell.test.ts +152 -0
  27. package/layers/rooms/server.ts +13 -10
  28. package/layers/server/server.ts +4 -5
  29. package/layers/source/vite.config.ts +21 -1
  30. package/package.json +2 -1
  31. package/starters/campaign/README.md +9 -2
  32. package/starters/campaign/layers/rooms/server.ts +3 -3
  33. package/starters/campaign/layers/server/server.ts +3 -5
  34. package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
  35. package/starters/campaign/src/campaign/game.ts +26 -5
  36. package/starters/campaign/src/campaign/index.ts +104 -68
  37. package/starters/campaign/src/campaign/kills.ts +1 -1
  38. package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
  39. package/starters/campaign/src/campaign/missions/siege.ts +6 -6
  40. package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
  41. package/starters/campaign/src/campaign/scenes.ts +8 -7
  42. package/starters/campaign/src/campaign/units.ts +59 -20
  43. package/starters/campaign/src/campaign/view/attacks.ts +19 -0
  44. package/starters/campaign/src/campaign/view/fire.ts +2 -2
  45. package/starters/campaign/src/campaign/view/index.ts +54 -43
  46. package/starters/campaign/src/campaign/waves.ts +18 -17
  47. package/starters/campaign/src/game.ts +8 -1
  48. package/starters/campaign/src/main.ts +38 -19
  49. package/starters/campaign/src/maps/fort.names.ts +2 -2
  50. package/starters/campaign/src/maps/fort.vgmap.json +102 -2
  51. package/starters/campaign/src/maps/homestead.names.ts +2 -2
  52. package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
  53. package/starters/campaign/src/maps/pass.names.ts +2 -2
  54. package/starters/campaign/src/maps/pass.vgmap.json +69 -1
  55. package/starters/campaign/src/waves-panel.ts +2 -1
  56. package/starters/empty/README.md +18 -10
  57. package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
  58. package/starters/empty/src/game.ts +11 -75
  59. package/starters/empty/src/generate.worker.ts +5 -5
  60. package/starters/empty/src/rules.ts +83 -25
  61. package/starters/empty/src/view.ts +82 -0
  62. package/starters/examples/README.md +83 -0
  63. package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
  64. package/starters/examples/package.json +5 -0
  65. package/starters/examples/src/examples/abilities/areas.ts +165 -0
  66. package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
  67. package/starters/examples/src/examples/abilities/buffs.ts +287 -0
  68. package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
  69. package/starters/examples/src/examples/abilities/changing.ts +228 -0
  70. package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
  71. package/starters/examples/src/examples/abilities/control.ts +158 -0
  72. package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
  73. package/starters/examples/src/examples/abilities/damage.ts +156 -0
  74. package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
  75. package/starters/examples/src/examples/abilities/family.ts +29 -0
  76. package/starters/examples/src/examples/abilities/index.ts +32 -0
  77. package/starters/examples/src/examples/abilities/kit.ts +179 -0
  78. package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
  79. package/starters/examples/src/examples/abilities/looks.ts +47 -0
  80. package/starters/examples/src/examples/abilities/missiles.ts +203 -0
  81. package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
  82. package/starters/examples/src/examples/abilities/moves.ts +206 -0
  83. package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
  84. package/starters/examples/src/examples/abilities/recasts.ts +181 -0
  85. package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
  86. package/starters/examples/src/examples/abilities/resources.ts +166 -0
  87. package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
  88. package/starters/examples/src/examples/abilities/summons.ts +296 -0
  89. package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
  90. package/starters/examples/src/examples/abilities/triggers.ts +204 -0
  91. package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
  92. package/starters/examples/src/examples/abilities/vision.ts +137 -0
  93. package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
  94. package/starters/examples/src/examples/animation/index.ts +235 -0
  95. package/starters/examples/src/examples/animation/looks.ts +444 -0
  96. package/starters/examples/src/examples/catalog.ts +52 -0
  97. package/starters/examples/src/examples/cinematics/index.ts +217 -0
  98. package/starters/examples/src/examples/cinematics/looks.ts +476 -0
  99. package/starters/examples/src/examples/cinematics/tour.json +43 -0
  100. package/starters/examples/src/examples/cinematics/village.ts +88 -0
  101. package/starters/examples/src/examples/common/looks.ts +65 -0
  102. package/starters/examples/src/examples/common/units.ts +70 -0
  103. package/starters/examples/src/examples/construction/index.ts +329 -0
  104. package/starters/examples/src/examples/construction/looks.ts +275 -0
  105. package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
  106. package/starters/examples/src/examples/custom-animation/index.ts +190 -0
  107. package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
  108. package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
  109. package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
  110. package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
  111. package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
  112. package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
  113. package/starters/examples/src/examples/custom-fx/index.ts +25 -0
  114. package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
  115. package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
  116. package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
  117. package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
  118. package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
  119. package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
  120. package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
  121. package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
  122. package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
  123. package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
  124. package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
  125. package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
  126. package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
  127. package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
  128. package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
  129. package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
  130. package/starters/examples/src/examples/define.ts +86 -0
  131. package/starters/examples/src/examples/destruction/index.ts +570 -0
  132. package/starters/examples/src/examples/destruction/looks.ts +447 -0
  133. package/starters/examples/src/examples/dialogs/index.ts +383 -0
  134. package/starters/examples/src/examples/dialogs/looks.ts +546 -0
  135. package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
  136. package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
  137. package/starters/examples/src/examples/fx/blood.look.ts +62 -0
  138. package/starters/examples/src/examples/fx/blood.ts +59 -0
  139. package/starters/examples/src/examples/fx/feel.look.ts +86 -0
  140. package/starters/examples/src/examples/fx/feel.ts +57 -0
  141. package/starters/examples/src/examples/fx/fight.ts +28 -0
  142. package/starters/examples/src/examples/fx/floats.look.ts +108 -0
  143. package/starters/examples/src/examples/fx/floats.ts +24 -0
  144. package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
  145. package/starters/examples/src/examples/fx/gallery.ts +23 -0
  146. package/starters/examples/src/examples/fx/index.ts +30 -0
  147. package/starters/examples/src/examples/fx/links.look.ts +83 -0
  148. package/starters/examples/src/examples/fx/links.ts +49 -0
  149. package/starters/examples/src/examples/fx/looks.ts +55 -0
  150. package/starters/examples/src/examples/fx/map.look.ts +93 -0
  151. package/starters/examples/src/examples/fx/map.ts +33 -0
  152. package/starters/examples/src/examples/fx/marks.look.ts +100 -0
  153. package/starters/examples/src/examples/fx/marks.ts +36 -0
  154. package/starters/examples/src/examples/fx/shots.look.ts +92 -0
  155. package/starters/examples/src/examples/fx/shots.ts +71 -0
  156. package/starters/examples/src/examples/fx/spells.look.ts +87 -0
  157. package/starters/examples/src/examples/fx/spells.ts +122 -0
  158. package/starters/examples/src/examples/fx/stage.ts +31 -0
  159. package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
  160. package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
  161. package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
  162. package/starters/examples/src/examples/gui/index.ts +143 -0
  163. package/starters/examples/src/examples/gui/keys.ts +91 -0
  164. package/starters/examples/src/examples/gui/layout.ts +42 -0
  165. package/starters/examples/src/examples/gui/looks.ts +270 -0
  166. package/starters/examples/src/examples/gui/minimap.ts +127 -0
  167. package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
  168. package/starters/examples/src/examples/gui/plates.ts +87 -0
  169. package/starters/examples/src/examples/gui/radial.ts +104 -0
  170. package/starters/examples/src/examples/gui/solidPart.ts +107 -0
  171. package/starters/examples/src/examples/looks.ts +45 -0
  172. package/starters/examples/src/examples/mechanics/battle.ts +51 -0
  173. package/starters/examples/src/examples/mechanics/damage.ts +80 -0
  174. package/starters/examples/src/examples/mechanics/flying.ts +93 -0
  175. package/starters/examples/src/examples/mechanics/formations.ts +58 -0
  176. package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
  177. package/starters/examples/src/examples/mechanics/index.ts +44 -0
  178. package/starters/examples/src/examples/mechanics/looks.ts +289 -0
  179. package/starters/examples/src/examples/mechanics/maze.ts +119 -0
  180. package/starters/examples/src/examples/mechanics/orders.ts +68 -0
  181. package/starters/examples/src/examples/mechanics/selection.ts +30 -0
  182. package/starters/examples/src/examples/moba/index.ts +377 -0
  183. package/starters/examples/src/examples/moba/looks.ts +349 -0
  184. package/starters/examples/src/examples/moba/maps.ts +69 -0
  185. package/starters/examples/src/examples/moba/spells.ts +34 -0
  186. package/starters/examples/src/examples/moba/units.ts +196 -0
  187. package/starters/examples/src/examples/nature/camera.ts +15 -0
  188. package/starters/examples/src/examples/nature/grass.ts +64 -0
  189. package/starters/examples/src/examples/nature/index.ts +327 -0
  190. package/starters/examples/src/examples/nature/land.ts +173 -0
  191. package/starters/examples/src/examples/nature/looks.ts +577 -0
  192. package/starters/examples/src/examples/nature/maps.ts +370 -0
  193. package/starters/examples/src/examples/nature/skies.ts +324 -0
  194. package/starters/examples/src/examples/nature/weather.ts +80 -0
  195. package/starters/examples/src/examples/performance/index.ts +263 -0
  196. package/starters/examples/src/examples/performance/looks.ts +372 -0
  197. package/starters/examples/src/examples/performance/meter.ts +158 -0
  198. package/starters/examples/src/examples/post/grades.ts +86 -0
  199. package/starters/examples/src/examples/post/index.ts +202 -0
  200. package/starters/examples/src/examples/post/looks.ts +884 -0
  201. package/starters/examples/src/examples/post/passes.ts +213 -0
  202. package/starters/examples/src/examples/rts/ai.ts +49 -0
  203. package/starters/examples/src/examples/rts/airlift.ts +43 -0
  204. package/starters/examples/src/examples/rts/base.ts +129 -0
  205. package/starters/examples/src/examples/rts/build.ts +65 -0
  206. package/starters/examples/src/examples/rts/camps.ts +36 -0
  207. package/starters/examples/src/examples/rts/carry.ts +103 -0
  208. package/starters/examples/src/examples/rts/creeps.ts +61 -0
  209. package/starters/examples/src/examples/rts/data.ts +406 -0
  210. package/starters/examples/src/examples/rts/ferry.ts +41 -0
  211. package/starters/examples/src/examples/rts/garrison.ts +50 -0
  212. package/starters/examples/src/examples/rts/harvest.ts +29 -0
  213. package/starters/examples/src/examples/rts/hero.ts +67 -0
  214. package/starters/examples/src/examples/rts/index.ts +77 -0
  215. package/starters/examples/src/examples/rts/looks.ts +721 -0
  216. package/starters/examples/src/examples/rts/shop.ts +46 -0
  217. package/starters/examples/src/examples/rts/tech.ts +39 -0
  218. package/starters/examples/src/examples/rts/train.ts +48 -0
  219. package/starters/examples/src/examples/rts/waterside.ts +185 -0
  220. package/starters/examples/src/examples/showcase/index.ts +714 -0
  221. package/starters/examples/src/examples/showcase/looks.ts +238 -0
  222. package/starters/examples/src/game.ts +17 -0
  223. package/starters/examples/src/main.ts +82 -0
  224. package/starters/examples/src/rules.ts +31 -0
  225. package/starters/examples/src/shell/look.ts +79 -0
  226. package/starters/examples/src/shell/registry.ts +7 -0
  227. package/starters/examples/src/shell/scenes.ts +178 -0
  228. package/starters/examples/src/shell/shell.ts +290 -0
  229. package/starters/examples/src/shell/style.ts +202 -0
  230. package/starters/examples/src/view.ts +50 -0
  231. package/starters/melee/README.md +12 -3
  232. package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
  233. package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
  234. package/starters/melee/src/game.ts +32 -2
  235. package/starters/melee/src/maps/bay.names.ts +2 -2
  236. package/starters/melee/src/maps/bay.vgmap.json +198 -1
  237. package/starters/melee/src/maps.ts +11 -9
  238. package/starters/melee/src/melee/abilities.ts +1 -194
  239. package/starters/melee/src/melee/blight.ts +7 -7
  240. package/starters/melee/src/melee/cellLayers.json +3 -3
  241. package/starters/melee/src/melee/data.json +1 -18
  242. package/starters/melee/src/melee/game.ts +6 -4
  243. package/starters/melee/src/melee/heroes.ts +1025 -0
  244. package/starters/melee/src/melee/index.ts +64 -61
  245. package/starters/melee/src/melee/items.ts +15 -362
  246. package/starters/melee/src/melee/languages/he.json +14 -2
  247. package/starters/melee/src/melee/pings.ts +5 -6
  248. package/starters/melee/src/melee/saves.ts +1 -1
  249. package/starters/melee/src/melee/undead.ts +15 -230
  250. package/starters/melee/src/melee/units.ts +37 -171
  251. package/starters/melee/src/melee/view/attacks.ts +19 -0
  252. package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
  253. package/starters/melee/src/melee/view/index.ts +56 -52
  254. package/starters/melee/src/melee/view/looks.ts +86 -63
  255. package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
  256. package/starters/melee/src/melee/view/walls.ts +1 -1
  257. package/starters.mjs +6 -0
  258. package/v12.mjs +14 -15
  259. package/starters/campaign/src/rules.ts +0 -20
  260. package/starters/empty/src/maps.ts +0 -44
  261. package/starters/empty/src/tiles.ts +0 -12
  262. package/starters/melee/src/melee/heroSpells.ts +0 -38
  263. package/starters/melee/src/rules.ts +0 -24
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A meteor, on the rules' side: footmen standing about a field for it to fall among. The meteor is the view's effect
3
+ * (`./meteor.look.ts`); the rules could call one down for every player with `world.fx('customFx.meteor', { at })`
4
+ */
5
+ import { exactMath } from '@v12-engine/engine'
6
+ import { PLAYER, UNIT } from '../common/units'
7
+ import type { ExampleDef } from '../define'
8
+
9
+ export const METEOR_AT = { x: 24, z: 25 }
10
+
11
+ export const meteor: ExampleDef = {
12
+ id: 'custom-fx-meteor',
13
+ category: 'custom-fx',
14
+ title: 'A meteor',
15
+ summary:
16
+ 'An effect with a way of its own (`path`): a burning rock falls from the sky trailing fire and light, then the blast, a ring thrown out, rocks flung that trail smoke as they fly (particles that emit particles), the ground cracked and burning. Call one down, or a shower.',
17
+ tags: ['path', 'meteor', 'sub-emitters', 'trail', 'shake', 'decal', 'explosion', 'FxDef'],
18
+ environment: 'dusk',
19
+ start: (world) => {
20
+ for (let i = 0; i < 8; i++) {
21
+ const angle = (i / 8) * Math.PI * 2
22
+ world.spawn(UNIT.footman, { at: { x: METEOR_AT.x + exactMath.cos(angle) * 6, z: METEOR_AT.z + exactMath.sin(angle) * 6 }, owner: PLAYER })
23
+ }
24
+ },
25
+ source: ['examples/custom-fx/meteor.ts', 'examples/custom-fx/meteor.look.ts']
26
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * An effect from parts, on the client's side: `customFx.blast`, a timeline of parts (`FxPart`) each starting `at`
3
+ * seconds in and lasting `for`, every field a value or a function of the knobs (`params`). Particles burst and stream;
4
+ * a sub-emitter throws sparks off each spark as it dies; a light and a shake grow with `size`; a scorch is laid; another
5
+ * effect (smoke) plays as a part. A ready-made effect is changed by its knobs alone with `fx.extend`
6
+ */
7
+ import { fx, type FxDef, type FxHandle, type GameClientApi } from '@v12-engine/client'
8
+ import { button, type ExampleLook, select, slider, toggle } from '../../shell/look'
9
+ import { aim } from '../fx/stage'
10
+ import { BLAST_AT } from './parts'
11
+
12
+ /** The blast: everything in it reads `size`, `color` (its heart) and `tint` (its edge) */
13
+ const blast: FxDef = {
14
+ params: { size: 1, color: '#fff0b0', tint: '#ff5a14' },
15
+ knobs: {
16
+ size: { min: 0.3, max: 3, step: 0.1, about: 'How big the blast is' },
17
+ color: { about: 'Its heart' },
18
+ tint: { about: 'Its edge, its sparks' }
19
+ },
20
+ parts: [
21
+ // a white flash: a few big soft dots, gone in a blink
22
+ {
23
+ particles: {
24
+ bursts: [{ at: 0, count: 6 }],
25
+ lifetime: 0.15,
26
+ size: (p) => [4 * Number(p.size), 6 * Number(p.size)],
27
+ color: '#ffffff',
28
+ texture: 'flare'
29
+ }
30
+ },
31
+ // the fireball: puffs flung out and slowing, from the heart's colour to the edge's to smoke
32
+ {
33
+ particles: {
34
+ bursts: [{ at: 0, count: 40 }],
35
+ shape: { type: 'sphere', radius: 0.3 },
36
+ speed: (p) => [2 * Number(p.size), 5 * Number(p.size)],
37
+ drag: 0.9,
38
+ lifetime: [0.5, 0.9],
39
+ size: (p) => [
40
+ [0, 1.2 * Number(p.size)],
41
+ [1, 2.8 * Number(p.size)]
42
+ ],
43
+ color: (p) => [
44
+ [0, String(p.color)],
45
+ [0.35, String(p.tint)],
46
+ [1, '#2a201a']
47
+ ],
48
+ texture: 'puff',
49
+ rotation: [-1, 1]
50
+ }
51
+ },
52
+ // sparks thrown high, falling, each throwing a few small ones as it dies (`sub`)
53
+ {
54
+ particles: {
55
+ bursts: [{ at: 0, count: 50 }],
56
+ shape: 'hemisphere',
57
+ spread: 1,
58
+ speed: (p) => [6 * Number(p.size), 12 * Number(p.size)],
59
+ gravity: 9,
60
+ lifetime: [0.6, 1.2],
61
+ size: [0.25, 0.05],
62
+ color: (p) => [String(p.color), String(p.tint)],
63
+ texture: 'spark',
64
+ billboard: 'stretched',
65
+ stretch: 3,
66
+ sub: [
67
+ {
68
+ on: 'death',
69
+ particles: {
70
+ bursts: [{ at: 0, count: 4 }],
71
+ shape: 'sphere',
72
+ speed: [1, 3],
73
+ lifetime: 0.3,
74
+ size: [0.12, 0],
75
+ color: (p) => String(p.tint),
76
+ texture: 'dot'
77
+ }
78
+ }
79
+ ]
80
+ }
81
+ },
82
+ // a ring of dust thrown out along the ground
83
+ {
84
+ at: 0.05,
85
+ particles: {
86
+ bursts: [{ at: 0, count: 30 }],
87
+ shape: { type: 'ring', radius: 0.4 },
88
+ speed: (p) => [4 * Number(p.size), 6 * Number(p.size)],
89
+ drag: 0.95,
90
+ lifetime: [0.8, 1.4],
91
+ size: [0.8, 2],
92
+ color: '#9a8466',
93
+ texture: 'puff',
94
+ blend: 'alpha',
95
+ lit: true
96
+ }
97
+ },
98
+ // the light, as bright as the blast is big, fading; the view shaken; the ground scorched for half a minute
99
+ { light: { color: (p) => String(p.tint), intensity: (p) => 5 * Number(p.size), range: (p) => 9 * Number(p.size), fade: 0.7 } },
100
+ { shake: (p) => ({ strength: 0.35 * Number(p.size), time: 0.35 }) },
101
+ { at: 0.1, decal: { size: (p) => 3.2 * Number(p.size), time: 30, color: '#140d08' } },
102
+ // smoke rising after, another effect of its own played as a part for 5 seconds
103
+ { at: 0.25, for: 5, fx: 'customFx.smoke', params: (p) => ({ size: p.size }) }
104
+ ]
105
+ }
106
+
107
+ /** The smoke after: one emitter, made with `fx.particles` */
108
+ const smoke: FxDef = fx.particles(
109
+ {
110
+ rate: (p) => 18 * Number(p.size),
111
+ over: { rate: [1, 0] },
112
+ shape: { type: 'sphere', radius: 0.6 },
113
+ speed: [0.6, 1.4],
114
+ direction: [0, 1, 0],
115
+ lifetime: [2.5, 4],
116
+ size: (p) => [
117
+ [0, 0.8 * Number(p.size)],
118
+ [1, 3 * Number(p.size)]
119
+ ],
120
+ color: ['#4a4440', '#2a2826'],
121
+ alpha: [0.6, 0],
122
+ texture: 'puff',
123
+ blend: 'alpha',
124
+ wind: 1,
125
+ lit: true
126
+ },
127
+ { params: { size: 1 } }
128
+ )
129
+
130
+ /** The effects of this example, taken into the game's view: the blast, its smoke, and a fire made frost */
131
+ export const partsFx: Record<string, FxDef> = {
132
+ 'customFx.blast': blast,
133
+ 'customFx.smoke': smoke,
134
+ // the ready-made fire with knobs of its own: blue flames, a cold light, no smoke
135
+ 'customFx.frostFire': fx.extend('fire', { color: '#c8f4ff', tint: '#2a7fff', smoke: false, light: 2 })
136
+ }
137
+
138
+ /** The colours the card offers, as the blast's heart and edge */
139
+ const COLORS: Record<string, { color: string; tint: string }> = {
140
+ Fire: { color: '#fff0b0', tint: '#ff5a14' },
141
+ Frost: { color: '#f0ffff', tint: '#38a8ff' },
142
+ Venom: { color: '#f4ffb0', tint: '#58d020' },
143
+ Arcane: { color: '#ffe0ff', tint: '#a040ff' }
144
+ }
145
+
146
+ const chosen = { size: 1, colors: 'Fire' }
147
+ let frostFire: FxHandle | undefined
148
+
149
+ /** Plays the blast where it goes off, with the card's knobs */
150
+ const play = (client: GameClientApi) => client.fx.play('customFx.blast', { at: BLAST_AT, params: { size: chosen.size, ...COLORS[chosen.colors] } })
151
+
152
+ export const partsLook: ExampleLook = {
153
+ controls: [
154
+ button('Blast', (client) => play(client), 'Plays the blast again, at the size and colour chosen'),
155
+ slider(
156
+ 'Size',
157
+ { min: 0.3, max: 3, step: 0.1, value: chosen.size },
158
+ (_client, size) => (chosen.size = size),
159
+ 'How big the next blast is, its light and shake too (the knob `size`, read by every part): press Blast to see it'
160
+ ),
161
+ select(
162
+ 'Colour',
163
+ Object.keys(COLORS),
164
+ chosen.colors,
165
+ (client, name) => {
166
+ chosen.colors = name
167
+ play(client)
168
+ },
169
+ 'The blast’s colours, its heart and its edge: choosing plays it'
170
+ ),
171
+ toggle(
172
+ 'Frost fire',
173
+ true,
174
+ (client, on) => {
175
+ frostFire?.stop({ mode: 'fade', fade: 0.6 })
176
+ frostFire = on ? client.fx.play('customFx.frostFire', { at: { x: BLAST_AT.x - 6, z: BLAST_AT.z + 2 } }) : undefined
177
+ },
178
+ 'The blue fire to the left: the ready-made fire made frost by its knobs alone (`fx.extend`)'
179
+ )
180
+ ],
181
+ ready: (client) => {
182
+ chosen.size = 1
183
+ chosen.colors = 'Fire'
184
+ aim(client, { x: BLAST_AT.x - 2, z: BLAST_AT.z + 0.5 }, 18, 0.6)
185
+ frostFire = client.fx.play('customFx.frostFire', { at: { x: BLAST_AT.x - 6, z: BLAST_AT.z + 2 } })
186
+ const first = window.setTimeout(() => play(client), 400)
187
+ return () => {
188
+ window.clearTimeout(first)
189
+ frostFire?.stop({ mode: 'at-once' })
190
+ frostFire = undefined
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * An effect made of parts, on the rules' side: a knight to stand by it. The effect is the view's (`./parts.look.ts`);
3
+ * the rules could play it too, for every player, with `world.fx('customFx.blast', { at, params })`
4
+ */
5
+ import { PLAYER, UNIT } from '../common/units'
6
+ import type { ExampleDef } from '../define'
7
+
8
+ export const BLAST_AT = { x: 25.5, z: 25 }
9
+
10
+ export const parts: ExampleDef = {
11
+ id: 'custom-fx-parts',
12
+ category: 'custom-fx',
13
+ title: 'An effect from parts',
14
+ summary:
15
+ 'A blast of your own, written as a timeline of parts: a flash, sparks that throw sparks of their own as they die, a fireball, a light, a shake, a scorch, smoke after. Its knobs (size, colour) are sliders here; a ready-made fire is made frost with `fx.extend`.',
16
+ tags: ['FxDef', 'parts', 'knobs', 'params', 'particles', 'sub-emitters', 'fx.extend', 'timeline', 'shake', 'decal'],
17
+ start: (world) => {
18
+ world.spawn(UNIT.knight, { at: { x: BLAST_AT.x - 4, z: BLAST_AT.z - 1 }, owner: PLAYER })
19
+ },
20
+ source: ['examples/custom-fx/parts.ts', 'examples/custom-fx/parts.look.ts']
21
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Two portals, on the client's side. `customFx.gate` is parts: one made in code (`make`: a frame, and a disc drawn by a
3
+ * shader of its own, a swirl turning inward), motes drawn in toward it, and its light. `customFx.warp` is the flash the
4
+ * rules play where a footman leaves and comes: a burst, a ring, a light. The gates are put on the map as the example
5
+ * opens (`MapEnvironment.fx`), the warps played by the rules
6
+ */
7
+ import { Constants } from '@babylonjs/core/Engines/constants.js'
8
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
9
+ import { Color3 } from '@babylonjs/core/Maths/math.color.js'
10
+ import { CreateDisc } from '@babylonjs/core/Meshes/Builders/discBuilder.js'
11
+ import { CreateTorus } from '@babylonjs/core/Meshes/Builders/torusBuilder.js'
12
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
13
+ import type { FxDef, FxInstance, FxContext } from '@v12-engine/client'
14
+ import { button, type ExampleLook } from '../../shell/look'
15
+ import { aim } from '../fx/stage'
16
+ import { PORTALS, stepThrough } from './portal'
17
+ import { type TwoShaders, shaderMaterial } from './shader'
18
+
19
+ /**
20
+ * The swirl: in polar terms about the disc's middle, arms that wind tighter toward it and turn with time, between two
21
+ * colours, bright in the middle, fading out at the rim
22
+ */
23
+ const SWIRL: TwoShaders = {
24
+ glsl: {
25
+ vertex: `precision highp float;
26
+ attribute vec3 position;
27
+ attribute vec2 uv;
28
+ uniform mat4 worldViewProjection;
29
+ varying vec2 vUV;
30
+ void main() {
31
+ gl_Position = worldViewProjection * vec4(position, 1.0);
32
+ vUV = uv;
33
+ }`,
34
+ fragment: `precision highp float;
35
+ uniform float time;
36
+ uniform vec3 inner;
37
+ uniform vec3 outer;
38
+ varying vec2 vUV;
39
+ void main() {
40
+ vec2 p = vUV * 2.0 - 1.0;
41
+ float r = length(p);
42
+ float a = atan(p.y, p.x);
43
+ float arms = sin(a * 5.0 + 6.0 / (r + 0.15) - time * 4.0) * 0.5 + 0.5;
44
+ float fine = sin(a * 11.0 - 9.0 / (r + 0.2) + time * 2.5) * 0.5 + 0.5;
45
+ vec3 color = mix(outer, inner, arms * (1.0 - r) + 0.25) + vec3(1.0) * pow(max(1.0 - r * 1.6, 0.0), 3.0);
46
+ float alpha = (0.35 + arms * 0.5 + fine * 0.25) * smoothstep(1.0, 0.82, r);
47
+ gl_FragColor = vec4(color * alpha, alpha);
48
+ }`
49
+ },
50
+ wgsl: {
51
+ vertex: `attribute position: vec3f;
52
+ attribute uv: vec2f;
53
+ uniform worldViewProjection: mat4x4f;
54
+ varying vUV: vec2f;
55
+ @vertex
56
+ fn main(input: VertexInputs) -> FragmentInputs {
57
+ vertexOutputs.position = uniforms.worldViewProjection * vec4f(vertexInputs.position, 1.0);
58
+ vertexOutputs.vUV = vertexInputs.uv;
59
+ }`,
60
+ fragment: `uniform time: f32;
61
+ uniform inner: vec3f;
62
+ uniform outer: vec3f;
63
+ varying vUV: vec2f;
64
+ @fragment
65
+ fn main(input: FragmentInputs) -> FragmentOutputs {
66
+ let p = fragmentInputs.vUV * 2.0 - 1.0;
67
+ let r = length(p);
68
+ let a = atan2(p.y, p.x);
69
+ let arms = sin(a * 5.0 + 6.0 / (r + 0.15) - uniforms.time * 4.0) * 0.5 + 0.5;
70
+ let fine = sin(a * 11.0 - 9.0 / (r + 0.2) + uniforms.time * 2.5) * 0.5 + 0.5;
71
+ let color = mix(uniforms.outer, uniforms.inner, arms * (1.0 - r) + 0.25) + vec3f(1.0) * pow(max(1.0 - r * 1.6, 0.0), 3.0);
72
+ let alpha = (0.35 + arms * 0.5 + fine * 0.25) * smoothstep(1.0, 0.82, r);
73
+ fragmentOutputs.color = vec4f(color * alpha, alpha);
74
+ }`
75
+ }
76
+ }
77
+
78
+ /** A colour of the knobs as Babylon's */
79
+ const color3 = (hex: unknown) => Color3.FromHexString(String(hex))
80
+
81
+ /** The gate made in code: a stone-and-light frame standing up, the swirl inside it, turning */
82
+ function gate({ scene, anchor, params }: FxContext): FxInstance {
83
+ const root = new TransformNode('customFx.gate', scene)
84
+ root.parent = anchor
85
+ root.position.y = 1.7
86
+ const frame = CreateTorus('customFx.gateFrame', { diameter: 3.2, thickness: 0.28, tessellation: 48 }, scene)
87
+ frame.rotation.x = Math.PI / 2
88
+ frame.parent = root
89
+ const stone = new StandardMaterial('customFx.gateStone', scene)
90
+ stone.diffuseColor = new Color3(0.25, 0.22, 0.28)
91
+ stone.emissiveColor = color3(params.outer).scale(0.6)
92
+ frame.material = stone
93
+ const disc = CreateDisc('customFx.gateSwirl', { radius: 1.5, tessellation: 64 }, scene)
94
+ disc.parent = root
95
+ const swirl = shaderMaterial('customFxSwirl', scene, SWIRL, ['time', 'inner', 'outer'], ['position', 'uv'])
96
+ swirl.alphaMode = Constants.ALPHA_ADD
97
+ swirl.backFaceCulling = false
98
+ disc.material = swirl
99
+ for (const mesh of [frame, disc]) mesh.isPickable = false
100
+ let time = 0
101
+ let inner = color3(params.inner)
102
+ let outer = color3(params.outer)
103
+ return {
104
+ update: (dt) => {
105
+ time += dt
106
+ swirl.setFloat('time', time)
107
+ swirl.setColor3('inner', inner)
108
+ swirl.setColor3('outer', outer)
109
+ },
110
+ set: (next) => {
111
+ inner = color3(next.inner)
112
+ outer = color3(next.outer)
113
+ stone.emissiveColor = outer.scale(0.6)
114
+ },
115
+ dispose: () => {
116
+ root.dispose(false, true)
117
+ swirl.dispose()
118
+ }
119
+ }
120
+ }
121
+
122
+ /** The effects of this example, taken into the game's view */
123
+ export const portalFx: Record<string, FxDef> = {
124
+ 'customFx.gate': {
125
+ params: { inner: '#e6d4ff', outer: '#7a3cff' },
126
+ parts: [
127
+ { make: gate },
128
+ // motes drawn in from all round toward the middle of the swirl
129
+ {
130
+ offset: [0, 1.7, 0],
131
+ particles: {
132
+ rate: 50,
133
+ shape: { type: 'sphere', radius: 2.6, rim: true },
134
+ speed: [-2.6, -1.8],
135
+ lifetime: 1,
136
+ size: [0.05, 0.2],
137
+ color: (p) => [String(p.outer), String(p.inner)],
138
+ alpha: [0, 1],
139
+ texture: 'flare'
140
+ }
141
+ },
142
+ { offset: [0, 1.7, 0.6], light: { color: (p) => String(p.outer), intensity: 2.2, range: 9, flicker: 0.1 } }
143
+ ]
144
+ },
145
+ 'customFx.warp': {
146
+ parts: [
147
+ {
148
+ particles: {
149
+ bursts: [{ at: 0, count: 40 }],
150
+ shape: { type: 'cylinder', radius: 0.5, height: 2 },
151
+ direction: [0, 1, 0],
152
+ speed: [2, 6],
153
+ lifetime: [0.4, 0.8],
154
+ size: [0.3, 0],
155
+ color: ['#ffffff', '#b07aff'],
156
+ texture: 'spark',
157
+ billboard: 'stretched',
158
+ stretch: 3
159
+ }
160
+ },
161
+ { particles: { bursts: [{ at: 0, count: 3 }], lifetime: 0.25, size: [3, 5], color: '#d8c0ff', texture: 'flare', order: 1 }, offset: [0, 1, 0] },
162
+ { fx: 'shockwave', params: { size: 0.5, color: '#c8a8ff', dust: false } },
163
+ { light: { color: '#b07aff', intensity: 4, range: 8, fade: 0.5 } }
164
+ ]
165
+ }
166
+ }
167
+
168
+ export const portalLook: ExampleLook = {
169
+ controls: [
170
+ button(
171
+ 'Send one through',
172
+ (client) => client.session.send(stepThrough.make({})),
173
+ 'A footman steps into one gate and out of the other: a flash at both ends'
174
+ ),
175
+ button(
176
+ 'Send them all',
177
+ (client) => {
178
+ for (let i = 0; i < 4; i++) window.setTimeout(() => client.session.send(stepThrough.make({})), i * 400)
179
+ },
180
+ 'Four footmen through, one after another'
181
+ )
182
+ ],
183
+ ready: (client) => {
184
+ // the gates laid on the map as its own effects, the second a warmer one
185
+ client.setEnvironment({
186
+ fx: [
187
+ { id: 1, name: 'gate 1', fx: 'customFx.gate', ...PORTALS[0] },
188
+ { id: 2, name: 'gate 2', fx: 'customFx.gate', ...PORTALS[1], params: { inner: '#fff0c8', outer: '#ff7a1a' } }
189
+ ]
190
+ })
191
+ aim(client, { x: (PORTALS[0].x + PORTALS[1].x) / 2, z: PORTALS[0].z - 1 }, 25, 0.45)
192
+ }
193
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Two portals, on the rules' side: footmen by the first, and a command that sends one through, from whichever portal
3
+ * he is nearer to the other, with a flash at both ends that every player sees (`world.fx`). The portals themselves are
4
+ * the screen's: an effect made in code with a shader of its own (`./portal.look.ts`)
5
+ */
6
+ import { defineCommand, type Game, exactMath } from '@v12-engine/engine'
7
+ import { PLAYER, UNIT } from '../common/units'
8
+ import type { ExampleDef } from '../define'
9
+
10
+ export const PORTALS = [
11
+ { x: 18, z: 25 },
12
+ { x: 30, z: 25 }
13
+ ] as const
14
+
15
+ /** A footman steps through: put down at the other portal, walking on out of it, a flash where he left and where he came */
16
+ export const stepThrough = defineCommand('customFx.step', {}, (world) => {
17
+ const footmen = [...world.units.values()].filter((unit) => unit.def.id === UNIT.footman && unit.alive)
18
+ const away = (unit: { pos: { x: number; z: number } }, at: { x: number; z: number }) => exactMath.hypot(unit.pos.x - at.x, unit.pos.z - at.z)
19
+ // the one nearest either portal
20
+ const one = footmen.sort((a, b) => Math.min(away(a, PORTALS[0]), away(a, PORTALS[1])) - Math.min(away(b, PORTALS[0]), away(b, PORTALS[1])))[0]
21
+ if (!one) return
22
+ const [from, to] = away(one, PORTALS[0]) < away(one, PORTALS[1]) ? PORTALS : [PORTALS[1], PORTALS[0]]
23
+ world.fx('customFx.warp', { at: { x: one.pos.x, z: one.pos.z } })
24
+ world.place(one, { x: to.x, z: to.z - 0.8 })
25
+ world.fx('customFx.warp', { at: { x: to.x, z: to.z - 0.8 } })
26
+ // on out, a few steps the way he was going
27
+ one.order('move', { x: to.x + (to.x - from.x) * 0.25 + (world.rng.next() - 0.5) * 3, z: to.z - 3 - world.rng.next() * 2 })
28
+ })
29
+
30
+ export const portal: ExampleDef = {
31
+ id: 'custom-fx-portal',
32
+ category: 'custom-fx',
33
+ title: 'Portals (a shader)',
34
+ summary:
35
+ 'Two gates, each an effect made in code: a frame, a swirl drawn by a shader of your own, motes drawn in, a light. Send footmen through: the rules move them and play a flash at both ends for every player.',
36
+ tags: ['portal', 'shader', 'ShaderMaterial', 'make', 'teleport', 'world.fx', 'swirl'],
37
+ environment: 'night',
38
+ start: (world) => {
39
+ for (let i = 0; i < 4; i++)
40
+ world.spawn(UNIT.footman, { at: { x: PORTALS[0].x - 1.5 + (i % 2) * 3, z: PORTALS[0].z - 3 - Math.floor(i / 2) * 1.4 }, owner: PLAYER })
41
+ },
42
+ source: ['examples/custom-fx/portal.ts', 'examples/custom-fx/portal.look.ts', 'examples/custom-fx/shader.ts']
43
+ }
44
+
45
+ /** Registers the command */
46
+ export function portalRules(game: Game<any>): void {
47
+ game.command(stepThrough)
48
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A renderer of your own, on the client's side: a `RendererFactory` in the view's `renderers`, handed the render kit
3
+ * (`RenderKit`: the scene, the state the screen knows, where each unit is drawn). It gives back a `FeatureRenderer`:
4
+ * `sync` hears what each snapshot brought (units come and gone, events: an attack, a blow), `frame` draws every
5
+ * frame, `dispose` takes it all away. Here, a ring under every unit coloured by its health, flaring at a blow, and a
6
+ * line from each attacker to what it attacks
7
+ */
8
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
9
+ import { Color3 } from '@babylonjs/core/Maths/math.color.js'
10
+ import { Vector3 } from '@babylonjs/core/Maths/math.vector.js'
11
+ import { CreateLines } from '@babylonjs/core/Meshes/Builders/linesBuilder.js'
12
+ import { CreateTorus } from '@babylonjs/core/Meshes/Builders/torusBuilder.js'
13
+ import type { LinesMesh } from '@babylonjs/core/Meshes/linesMesh.js'
14
+ import type { Mesh } from '@babylonjs/core/Meshes/mesh.js'
15
+ import type { RendererFactory } from '@v12-engine/client'
16
+ import { button, type ExampleLook, toggle } from '../../shell/look'
17
+ import { ENEMY } from '../common/units'
18
+ import { clash, lineUpAgain } from '../fx/blood'
19
+ import { aim } from '../fx/stage'
20
+
21
+ /** Whether the card shows what the renderer draws */
22
+ const shown = { rings: true, lines: true }
23
+
24
+ const GREEN = new Color3(0.2, 1, 0.35)
25
+ const YELLOW = new Color3(1, 0.85, 0.2)
26
+ const RED = new Color3(1, 0.15, 0.1)
27
+
28
+ /** Green when whole, yellow at half, red near none */
29
+ const healthColor = (share: number, out: Color3) =>
30
+ share > 0.5 ? Color3.LerpToRef(YELLOW, GREEN, (share - 0.5) * 2, out) : Color3.LerpToRef(RED, YELLOW, share * 2, out)
31
+
32
+ const ringsAndLines: RendererFactory = (kit) => {
33
+ const rings = new Map<number, { mesh: Mesh; material: StandardMaterial; flare: number }>()
34
+ const lines: { mesh: LinesMesh; from: number; to: number; age: number }[] = []
35
+ const ringOf = (id: number) => {
36
+ let ring = rings.get(id)
37
+ if (ring) return ring
38
+ const mesh = CreateTorus('customFx.healthRing', { diameter: 1.25, thickness: 0.07, tessellation: 32 }, kit.scene)
39
+ const material = new StandardMaterial('customFx.healthRing', kit.scene)
40
+ material.disableLighting = true
41
+ mesh.material = material
42
+ mesh.isPickable = false
43
+ ring = { mesh, material, flare: 0 }
44
+ rings.set(id, ring)
45
+ return ring
46
+ }
47
+ /** Where a unit's line ends: over its head, so the line reads above the crowd */
48
+ const middle = (id: number) => {
49
+ const drawn = kit.drawn(id)
50
+ return drawn && drawn.visible ? drawn.position.add(new Vector3(0, 2.3, 0)) : undefined
51
+ }
52
+ return {
53
+ sync(changes) {
54
+ for (const event of changes.events) {
55
+ // a blow: the ring of the struck flares
56
+ if (event.type === 'damage') ringOf(event.id).flare = 1
57
+ // an attack begun: a line from the attacker to its target, its side's colour
58
+ if (event.type === 'attack' && event.target !== undefined) {
59
+ const a = middle(event.id)
60
+ const b = middle(event.target)
61
+ if (!a || !b) continue
62
+ const mesh = CreateLines('customFx.threat', { points: [a, b], updatable: true }, kit.scene)
63
+ mesh.color = kit.state.entities.get(event.id)?.owner === ENEMY ? new Color3(1, 0.35, 0.3) : new Color3(0.4, 0.8, 1)
64
+ mesh.isPickable = false
65
+ lines.push({ mesh, from: event.id, to: event.target, age: 0 })
66
+ }
67
+ }
68
+ for (const gone of changes.removed) {
69
+ const ring = rings.get(gone.id)
70
+ ring?.mesh.dispose()
71
+ ring?.material.dispose()
72
+ rings.delete(gone.id)
73
+ }
74
+ },
75
+ frame(_alpha, dt) {
76
+ for (const entity of kit.state.entities.values()) {
77
+ const drawn = kit.drawn(entity.id)
78
+ const ring = ringOf(entity.id)
79
+ const share = entity.health / Math.max(entity.maxHealth, 1)
80
+ ring.mesh.setEnabled(shown.rings && !!drawn?.visible && entity.health > 0)
81
+ if (!drawn) continue
82
+ ring.mesh.position.set(drawn.position.x, drawn.position.y + 0.05, drawn.position.z)
83
+ // a blow flares it white and wide, fading back in a third of a second
84
+ ring.flare = Math.max(ring.flare - dt * 3, 0)
85
+ healthColor(share, ring.material.emissiveColor)
86
+ Color3.LerpToRef(ring.material.emissiveColor, Color3.White(), ring.flare, ring.material.emissiveColor)
87
+ ring.mesh.scaling.setAll(1 + ring.flare * 0.35)
88
+ }
89
+ for (let i = lines.length - 1; i >= 0; i--) {
90
+ const line = lines[i]!
91
+ line.age += dt
92
+ const a = middle(line.from)
93
+ const b = middle(line.to)
94
+ if (line.age > 0.8 || !a || !b) {
95
+ line.mesh.dispose()
96
+ lines.splice(i, 1)
97
+ continue
98
+ }
99
+ CreateLines('customFx.threat', { points: [a, b], instance: line.mesh })
100
+ line.mesh.alpha = 1 - line.age / 0.8
101
+ line.mesh.setEnabled(shown.lines)
102
+ }
103
+ },
104
+ dispose() {
105
+ for (const ring of rings.values()) {
106
+ ring.mesh.dispose()
107
+ ring.material.dispose()
108
+ }
109
+ for (const line of lines) line.mesh.dispose()
110
+ rings.clear()
111
+ lines.length = 0
112
+ }
113
+ }
114
+ }
115
+
116
+ export const rendererLook: ExampleLook = {
117
+ view: (view) => ({ ...view, renderers: [...(view.renderers ?? []), ringsAndLines] }),
118
+ controls: [
119
+ button(
120
+ 'Fight',
121
+ (client) => client.session.send(clash.make({})),
122
+ 'Both sides go at each other: the rings redden as they are hurt, lines join each attacker to its target'
123
+ ),
124
+ button('Line them up again', (client) => client.session.send(lineUpAgain.make({})), 'Everyone back in line, whole, their rings green again'),
125
+ toggle(
126
+ 'Health rings',
127
+ true,
128
+ (_client, on) => (shown.rings = on),
129
+ 'The ring under each unit, green to red as it is hurt, flaring white at a blow'
130
+ ),
131
+ toggle('Attack lines', true, (_client, on) => (shown.lines = on), 'A fading line from each attacker to what it attacks, as it strikes')
132
+ ],
133
+ ready: (client) => {
134
+ shown.rings = shown.lines = true
135
+ aim(client, { x: 24, z: 24 }, 18, 0.8)
136
+ const go = window.setTimeout(() => client.session.send(clash.make({})), 1200)
137
+ return () => window.clearTimeout(go)
138
+ }
139
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A renderer of your own, on the rules' side: the footmen and skeletons of the blood example's fight, on a field of
3
+ * their own. What is drawn over them is the screen's (`./renderer.look.ts`)
4
+ */
5
+ import { type ExampleDef, exampleField } from '../define'
6
+ import { blood } from '../fx/blood'
7
+
8
+ export const renderer: ExampleDef = {
9
+ id: 'custom-fx-renderer',
10
+ category: 'custom-fx',
11
+ title: 'A renderer of your own',
12
+ summary:
13
+ "Something drawn for every unit by code of the game's own (`GameView.renderers`): a ring under each, from green to red as it is hurt, flaring white at each blow; a line from each attacker to what it attacks, fading. Fight, and watch the field read itself.",
14
+ tags: ['renderer', 'RendererFactory', 'FeatureRenderer', 'sync', 'events', 'health', 'lines', 'kit'],
15
+ map: () => exampleField({ name: 'Field', players: 2 }),
16
+ start: blood.start,
17
+ source: ['examples/custom-fx/renderer.ts', 'examples/custom-fx/renderer.look.ts', 'examples/fx/blood.ts']
18
+ }