@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,110 @@
1
+ /**
2
+ * A shot drawn your own way, on the client's side: the thrower's `shot.draw`, called as each glaive leaves with what
3
+ * the shot is (`ShotContext`: the shot, its shooter, where it left from, the render kit). It makes the glaive (three
4
+ * blades of meshes and a ribbon behind them, `TrailMesh`), is told where it is every frame (`move`, with the way it
5
+ * goes) and spins it, and as it lands (`dispose`) throws sparks there and lets the ribbon fade before it goes
6
+ */
7
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
8
+ import { Color3 } from '@babylonjs/core/Maths/math.color.js'
9
+ import { CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder.js'
10
+ import { CreateCylinder } from '@babylonjs/core/Meshes/Builders/cylinderBuilder.js'
11
+ import { TrailMesh } from '@babylonjs/core/Meshes/trailMesh.js'
12
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
13
+ import type { ShotContext, ShotDrawing, UnitLook } from '@v12-engine/client'
14
+ import { type ExampleLook, select } from '../../shell/look'
15
+ import { character, person } from '../common/looks'
16
+ import { aim } from '../fx/stage'
17
+ import { GLAIVE_AT } from './glaive'
18
+
19
+ /** The blades' glow, as the card sets it */
20
+ const glow = new Color3(0.3, 1, 0.75)
21
+
22
+ /** One glaive on its way */
23
+ function drawGlaive({ kit, from }: ShotContext): ShotDrawing {
24
+ const { scene } = kit
25
+ const root = new TransformNode('customFx.glaive', scene)
26
+ root.position.set(from.x, from.y, from.z)
27
+ const spinner = new TransformNode('customFx.glaiveSpin', scene)
28
+ spinner.parent = root
29
+ const steel = new StandardMaterial('customFx.glaiveSteel', scene)
30
+ steel.diffuseColor = new Color3(0.2, 0.22, 0.25)
31
+ steel.emissiveColor = glow.clone()
32
+ steel.specularColor = new Color3(1, 1, 1)
33
+ // three curved-looking blades round a hub, lying flat
34
+ const hub = CreateCylinder('customFx.glaiveHub', { height: 0.08, diameter: 0.25, tessellation: 12 }, scene)
35
+ hub.parent = spinner
36
+ hub.material = steel
37
+ for (let i = 0; i < 3; i++) {
38
+ const blade = CreateBox('customFx.glaiveBlade', { width: 0.7, height: 0.04, depth: 0.12 }, scene)
39
+ blade.parent = spinner
40
+ blade.material = steel
41
+ blade.rotation.y = (i / 3) * Math.PI * 2
42
+ blade.position.set(Math.cos(-blade.rotation.y) * 0.32, 0, Math.sin(-blade.rotation.y) * 0.32)
43
+ blade.rotation.y += 0.5
44
+ }
45
+ // the ribbon: a point at the tip of one blade, sweeping as it spins
46
+ const tip = new TransformNode('customFx.glaiveTip', scene)
47
+ tip.parent = spinner
48
+ tip.position.x = 0.62
49
+ const trail = new TrailMesh('customFx.glaiveTrail', tip, scene, 0.12, 40, true)
50
+ const ribbon = new StandardMaterial('customFx.glaiveRibbon', scene)
51
+ ribbon.emissiveColor = glow.clone()
52
+ ribbon.disableLighting = true
53
+ ribbon.alpha = 0.55
54
+ trail.material = ribbon
55
+ for (const mesh of [hub, trail, ...spinner.getChildMeshes()]) mesh.isPickable = false
56
+ let spin = 0
57
+ return {
58
+ move(at, heading) {
59
+ root.position.set(at.x, at.y, at.z)
60
+ root.rotation.set(0, heading.yaw, 0)
61
+ // tipped a little, spinning fast
62
+ spin += 0.45
63
+ spinner.rotation.set(0.25, spin, 0)
64
+ },
65
+ dispose() {
66
+ kit.fx.play('spark-burst', {
67
+ at: { x: root.position.x, y: root.position.y, z: root.position.z },
68
+ params: { count: 20, color: glow.toHexString(), size: 0.7 }
69
+ })
70
+ root.dispose(false, true)
71
+ steel.dispose()
72
+ // the ribbon fades where it was, then goes
73
+ let left = 0.35
74
+ const fade = scene.onBeforeRenderObservable.add(() => {
75
+ left -= scene.getEngine().getDeltaTime() / 1000
76
+ ribbon.alpha = Math.max(left / 0.35, 0) * 0.55
77
+ if (left > 0) return
78
+ scene.onBeforeRenderObservable.remove(fade)
79
+ trail.dispose()
80
+ ribbon.dispose()
81
+ })
82
+ }
83
+ }
84
+ }
85
+
86
+ /** The thrower: the rogue, a glaive drawn the game's own way */
87
+ export const glaiverLook: UnitLook = {
88
+ height: 1.8,
89
+ model: character('rogue_hooded', 0.72, { animations: { attack: 'Ranged_1H_Shoot' } }),
90
+ shot: { draw: drawGlaive },
91
+ shapes: person(1.8, [0.2, 0.5, 0.4])
92
+ }
93
+
94
+ const COLORS: Record<string, [number, number, number]> = { Jade: [0.3, 1, 0.75], Ember: [1, 0.5, 0.15], Frost: [0.5, 0.8, 1], Blood: [1, 0.15, 0.2] }
95
+
96
+ export const glaiveLook: ExampleLook = {
97
+ controls: [
98
+ select(
99
+ 'Glow',
100
+ Object.keys(COLORS),
101
+ 'Jade',
102
+ (_client, name) => glow.set(...(COLORS[name] ?? COLORS.Jade!)),
103
+ 'The colour the blades and the ribbon behind them glow, on every glaive in the air'
104
+ )
105
+ ],
106
+ ready: (client) => {
107
+ glow.set(...COLORS.Jade!)
108
+ aim(client, GLAIVE_AT, 15, 0.6)
109
+ }
110
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A shot drawn your own way, on the rules' side: two glaive throwers and the dummies they throw at. To the rules a
3
+ * glaive is a shot like any; how it flies on the screen is its look's `draw` (`./glaive.look.ts`)
4
+ */
5
+ import type { Game } from '@v12-engine/engine'
6
+ import { ENEMY, PLAYER, UNIT } from '../common/units'
7
+ import { type ExampleDef, exampleField } from '../define'
8
+
9
+ export const GLAIVER = 'customFxGlaiver'
10
+ export const GLAIVE_AT = { x: 24, z: 24 }
11
+
12
+ export const glaive: ExampleDef = {
13
+ id: 'custom-fx-glaive',
14
+ category: 'custom-fx',
15
+ title: 'A shot of your own',
16
+ summary:
17
+ "A shot drawn in code (`ShotDef.draw`): a spinning three-bladed glaive made of meshes, a glowing ribbon behind it (Babylon's `TrailMesh`), sparks where it lands. The throwers keep throwing; turn the blades' colour.",
18
+ tags: ['ShotDef.draw', 'projectile', 'missile', 'TrailMesh', 'ribbon', 'meshes', 'custom shot'],
19
+ environment: 'dusk',
20
+ map: () => exampleField({ name: 'Range', players: 2 }),
21
+ start: (world) => {
22
+ world.spawn(GLAIVER, { at: { x: GLAIVE_AT.x - 5, z: GLAIVE_AT.z - 2 }, owner: PLAYER })
23
+ world.spawn(GLAIVER, { at: { x: GLAIVE_AT.x - 5, z: GLAIVE_AT.z + 2 }, owner: PLAYER })
24
+ for (const dz of [-3, 0, 3]) world.spawn(UNIT.dummy, { at: { x: GLAIVE_AT.x + 5, z: GLAIVE_AT.z + dz }, owner: ENEMY })
25
+ },
26
+ source: ['examples/custom-fx/glaive.ts', 'examples/custom-fx/glaive.look.ts']
27
+ }
28
+
29
+ /** Registers the thrower: slow glaives, from 11 cells away */
30
+ export function glaiveRules(game: Game<any>): void {
31
+ game.unit({
32
+ id: GLAIVER,
33
+ name: 'Glaive thrower',
34
+ kind: 'unit',
35
+ radius: 0.42,
36
+ stats: { health: 400, attackDamage: 25, attackSpeed: 0.8, attackRange: 11, moveSpeed: 4 },
37
+ sight: 13,
38
+ attacks: [{ damagePoint: 0.4, acquisitionRange: 12, projectileSpeed: 9 }]
39
+ })
40
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Particles on the graphics card, on the client's side: `customFx.fountain` is made in code (`FxDef.make`), with
3
+ * Babylon's `GPUParticleSystem` where the browser can move particles on the graphics card, its `ParticleSystem`
4
+ * otherwise. `make` is handed the scene, a node that moves with the effect (`anchor`) and the knobs; it gives back what
5
+ * runs every frame, what changes with the knobs, and what takes it all away. A light and the glow of the scene make
6
+ * it shine on what is round it
7
+ */
8
+ import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js'
9
+ import { NoiseProceduralTexture } from '@babylonjs/core/Materials/Textures/Procedurals/noiseProceduralTexture.js'
10
+ import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js'
11
+ import { Vector3 } from '@babylonjs/core/Maths/math.vector.js'
12
+ import { PointLight } from '@babylonjs/core/Lights/pointLight.js'
13
+ import { GPUParticleSystem } from '@babylonjs/core/Particles/gpuParticleSystem.js'
14
+ import { ParticleSystem } from '@babylonjs/core/Particles/particleSystem.js'
15
+ import type { Scene } from '@babylonjs/core/scene.js'
16
+ import type { FxDef, FxHandle, GameClientApi } from '@v12-engine/client'
17
+ import { type ExampleLook, readout, slider, toggle } from '../../shell/look'
18
+ import { aim } from '../fx/stage'
19
+ import { FOUNTAIN_AT } from './gpu'
20
+
21
+ /** A soft round spark, drawn once in a canvas: white, so the system's colours colour it */
22
+ function sparkTexture(scene: Scene): DynamicTexture {
23
+ const texture = new DynamicTexture('customFx.spark', 64, scene, false)
24
+ const context = texture.getContext()
25
+ const glow = context.createRadialGradient(32, 32, 0, 32, 32, 32)
26
+ glow.addColorStop(0, 'rgba(255,255,255,1)')
27
+ glow.addColorStop(0.25, 'rgba(255,255,255,0.8)')
28
+ glow.addColorStop(1, 'rgba(255,255,255,0)')
29
+ context.fillStyle = glow
30
+ context.fillRect(0, 0, 64, 64)
31
+ texture.update()
32
+ texture.hasAlpha = true
33
+ return texture
34
+ }
35
+
36
+ /** The fountain: sparks thrown up from a ring, turning from white to cyan to violet as they fall, thrown about by noise */
37
+ const fountain: FxDef = {
38
+ params: { count: 100_000, gpu: true },
39
+ make: ({ scene, anchor, params }) => {
40
+ const count = Number(params.count)
41
+ const onCard = params.gpu !== false && GPUParticleSystem.IsSupported
42
+ const system = onCard
43
+ ? new GPUParticleSystem('customFx.fountain', { capacity: count }, scene)
44
+ : new ParticleSystem('customFx.fountain', count, scene)
45
+ system.particleTexture = sparkTexture(scene)
46
+ system.emitter = anchor as never
47
+ // a ring at the foot, each spark sent up and a little out
48
+ system.createDirectedCylinderEmitter(0.6, 0.1, 0.2, new Vector3(-0.12, 1, -0.12), new Vector3(0.12, 1, 0.12))
49
+ system.emitRate = count / 3.2
50
+ system.minLifeTime = 2.4
51
+ system.maxLifeTime = 3.2
52
+ system.minEmitPower = 9
53
+ system.maxEmitPower = 13
54
+ system.gravity = new Vector3(0, -7, 0)
55
+ system.minSize = 0.04
56
+ system.maxSize = 0.11
57
+ system.addColorGradient(0, new Color4(1, 1, 1, 0.45))
58
+ system.addColorGradient(0.25, new Color4(0.3, 0.85, 1, 0.4))
59
+ system.addColorGradient(0.7, new Color4(0.55, 0.25, 1, 0.3))
60
+ system.addColorGradient(1, new Color4(0.3, 0.1, 0.8, 0))
61
+ system.blendMode = ParticleSystem.BLENDMODE_ADD
62
+ // noise: each spark pushed about by where it is in a field that moves, so the jet frays as it falls
63
+ const noise = new NoiseProceduralTexture('customFx.noise', 256, scene)
64
+ noise.animationSpeedFactor = 4
65
+ noise.persistence = 1.6
66
+ noise.brightness = 0.5
67
+ system.noiseTexture = noise
68
+ system.noiseStrength = new Vector3(4, 1, 4)
69
+ system.start()
70
+ // its light, breathing
71
+ const light = new PointLight('customFx.fountainLight', new Vector3(0, 3, 0), scene)
72
+ light.parent = anchor
73
+ light.diffuse = new Color3(0.45, 0.6, 1)
74
+ light.range = 16
75
+ let time = 0
76
+ let stoppedAt: number | undefined
77
+ return {
78
+ update: (dt) => {
79
+ time += dt
80
+ light.intensity = (2.2 + Math.sin(time * 3) * 0.4) * (stoppedAt === undefined ? 1 : Math.max(1 - (time - stoppedAt) / 3, 0))
81
+ // over once the last sparks let go have fallen
82
+ return stoppedAt === undefined || time - stoppedAt < 3.3
83
+ },
84
+ stop: () => {
85
+ system.stop()
86
+ stoppedAt = time
87
+ },
88
+ dispose: () => {
89
+ system.dispose()
90
+ noise.dispose()
91
+ light.dispose()
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ /** The fountain of this example, taken into the game's view */
98
+ export const gpuFx: Record<string, FxDef> = { 'customFx.fountain': fountain }
99
+
100
+ const chosen = { count: 100_000, gpu: true }
101
+ let playing: FxHandle | undefined
102
+
103
+ /** Plays it again with the card's count and where it is moved */
104
+ function replay(client: GameClientApi): void {
105
+ playing?.stop({ mode: 'at-once' })
106
+ playing = client.fx.play('customFx.fountain', { at: FOUNTAIN_AT, params: { ...chosen } })
107
+ }
108
+
109
+ export const gpuLook: ExampleLook = {
110
+ controls: [
111
+ slider(
112
+ 'Sparks',
113
+ { min: 5000, max: 200_000, step: 5000, value: chosen.count },
114
+ (client, count) => {
115
+ chosen.count = count
116
+ replay(client)
117
+ },
118
+ 'How many sparks the fountain keeps in the air: watch the frames a second as it climbs'
119
+ ),
120
+ toggle(
121
+ 'On the graphics card',
122
+ true,
123
+ (client, on) => {
124
+ chosen.gpu = on
125
+ replay(client)
126
+ },
127
+ 'Off: the same fountain moved by the CPU frame by frame, which keeps far fewer sparks a second'
128
+ ),
129
+ readout(
130
+ 'Frames a second',
131
+ (client) => `${Math.round(client.scene.getEngine().getFps())}${GPUParticleSystem.IsSupported ? '' : ' (no GPU particles here)'}`,
132
+ 'How fast the page draws now: what the sparks cost'
133
+ )
134
+ ],
135
+ ready: (client) => {
136
+ chosen.count = 100_000
137
+ chosen.gpu = true
138
+ aim(client, { x: FOUNTAIN_AT.x, z: FOUNTAIN_AT.z }, 34, 0.3)
139
+ replay(client)
140
+ return () => {
141
+ playing?.stop({ mode: 'at-once' })
142
+ playing = undefined
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Particles on the graphics card, on the rules' side: nothing but a mage to stand by the fountain. The fountain is the
3
+ * view's effect, made of Babylon's own particle system (`./gpu.look.ts`)
4
+ */
5
+ import { PLAYER, UNIT } from '../common/units'
6
+ import type { ExampleDef } from '../define'
7
+
8
+ export const FOUNTAIN_AT = { x: 24, z: 24 }
9
+
10
+ export const gpu: ExampleDef = {
11
+ id: 'custom-fx-gpu-particles',
12
+ category: 'custom-fx',
13
+ title: "Babylon's particles, a hundred thousand",
14
+ summary:
15
+ 'An effect that makes its own Babylon `GPUParticleSystem` (`make`): a fountain of up to 200 000 sparks moved on the graphics card, thrown about by noise, falling back. Turn the count up; turn the graphics card off to see what the CPU manages.',
16
+ tags: ['GPUParticleSystem', 'ParticleSystem', 'Babylon', 'make', 'gpu', 'noise', 'particles', 'performance'],
17
+ environment: 'night',
18
+ start: (world) => {
19
+ world.spawn(UNIT.mage, { at: { x: FOUNTAIN_AT.x - 4, z: FOUNTAIN_AT.z - 2 }, owner: PLAYER })
20
+ },
21
+ source: ['examples/custom-fx/gpu.ts', 'examples/custom-fx/gpu.look.ts']
22
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The examples of effects of your own, on the rules' side (`ExampleCategory`): their list entries, maps and starts,
3
+ * and what they add to the game. Most effects of one's own are the screen's alone, so most examples here put a unit or
4
+ * two down and leave the rest to their look (`<name>.look.ts` beside each, gathered in `./looks.ts`)
5
+ */
6
+ import type { Game } from '@v12-engine/engine'
7
+ import type { ExampleCategory } from '../define'
8
+ import { glaive, glaiveRules } from './glaive'
9
+ import { gpu } from './gpu'
10
+ import { lights } from './lights'
11
+ import { meteor } from './meteor'
12
+ import { parts } from './parts'
13
+ import { portal, portalRules } from './portal'
14
+ import { renderer } from './renderer'
15
+ import { ribbons } from './ribbons'
16
+ import { shield, shieldRules } from './shield'
17
+
18
+ export const category: ExampleCategory = {
19
+ rules: (game: Game<any>) => {
20
+ shieldRules(game)
21
+ portalRules(game)
22
+ glaiveRules(game)
23
+ },
24
+ examples: [parts, ribbons, meteor, gpu, shield, portal, glaive, renderer, lights]
25
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Lights that move, on the client's side: lights of the world added in code (`client.lights.add(LightDef)`), on
3
+ * always (`when: 'always'`; a lamp's are at night alone), each with a handle to move it and change it
4
+ * (`handle.set({ x, y, z, color })`) and take it away. A `halo` draws the light itself as a glowing ball; a `spot`
5
+ * shines one way. The quality's `lights` says how many are drawn at once
6
+ */
7
+ import type { GameClientApi, LightHandle, Rgb } from '@v12-engine/client'
8
+ import { type ExampleLook, select, slider, toggle } from '../../shell/look'
9
+ import { UNIT } from '../common/units'
10
+ import { aim, everyFrame, idsOf, where } from '../fx/stage'
11
+ import { LIGHTS_AT } from './lights'
12
+
13
+ const PALETTES: Record<string, Rgb[]> = {
14
+ Spirits: [
15
+ [0.4, 0.8, 1],
16
+ [0.7, 0.5, 1],
17
+ [0.4, 1, 0.75]
18
+ ],
19
+ Embers: [
20
+ [1, 0.55, 0.15],
21
+ [1, 0.8, 0.3],
22
+ [1, 0.3, 0.1]
23
+ ],
24
+ Rainbow: [
25
+ [1, 0.25, 0.25],
26
+ [1, 0.8, 0.2],
27
+ [0.3, 1, 0.4],
28
+ [0.3, 0.6, 1],
29
+ [0.8, 0.3, 1]
30
+ ]
31
+ }
32
+
33
+ const chosen = { count: 6, palette: 'Spirits', flicker: false }
34
+ let wisps: LightHandle[] = []
35
+ let lantern: LightHandle | undefined
36
+ let beam: LightHandle | undefined
37
+
38
+ /** The wisps made again, as many as the card says, in its colours */
39
+ function makeWisps(client: GameClientApi): void {
40
+ for (const wisp of wisps) wisp.dispose()
41
+ const colors = PALETTES[chosen.palette]!
42
+ wisps = Array.from({ length: chosen.count }, (_, i) =>
43
+ client.lights.add({
44
+ name: `wisp ${i + 1}`,
45
+ ...LIGHTS_AT,
46
+ y: 1.5,
47
+ color: colors[i % colors.length],
48
+ intensity: 1.6,
49
+ range: 5,
50
+ halo: 0.35,
51
+ when: 'always',
52
+ ...(chosen.flicker && { flicker: { amount: 0.4, speed: 2 } })
53
+ })
54
+ )
55
+ }
56
+
57
+ export const lightsLook: ExampleLook = {
58
+ controls: [
59
+ slider(
60
+ 'Wisps',
61
+ { min: 0, max: 16, step: 1, value: chosen.count },
62
+ (client, count) => {
63
+ chosen.count = count
64
+ makeWisps(client)
65
+ },
66
+ 'How many wisps circle the mage, each a light of its own on the ground under it'
67
+ ),
68
+ select(
69
+ 'Colours',
70
+ Object.keys(PALETTES),
71
+ chosen.palette,
72
+ (client, name) => {
73
+ chosen.palette = name
74
+ makeWisps(client)
75
+ },
76
+ 'The colours the wisps take, in turn'
77
+ ),
78
+ toggle(
79
+ 'Flicker',
80
+ false,
81
+ (client, on) => {
82
+ chosen.flicker = on
83
+ makeWisps(client)
84
+ },
85
+ 'The wisps waver in brightness, as a flame does'
86
+ ),
87
+ toggle('Lighthouse beam', true, (_client, on) => beam?.set({ on }), 'The spot light sweeping round the field, on or off')
88
+ ],
89
+ ready: (client) => {
90
+ Object.assign(chosen, { count: 6, palette: 'Spirits', flicker: false })
91
+ aim(client, LIGHTS_AT, 22, 0.75)
92
+ makeWisps(client)
93
+ lantern = client.lights.add({
94
+ name: 'lantern',
95
+ ...LIGHTS_AT,
96
+ y: 1.6,
97
+ color: [1, 0.75, 0.4],
98
+ intensity: 1.8,
99
+ range: 7,
100
+ halo: 0.18,
101
+ when: 'always',
102
+ flicker: 0.15
103
+ })
104
+ beam = client.lights.add({
105
+ name: 'beam',
106
+ x: LIGHTS_AT.x - 9,
107
+ y: 6,
108
+ z: LIGHTS_AT.z - 7,
109
+ kind: 'spot',
110
+ angle: 0.35,
111
+ focus: 4,
112
+ color: [1, 0.95, 0.8],
113
+ intensity: 3,
114
+ range: 30,
115
+ when: 'always'
116
+ })
117
+ let time = 0
118
+ const stop = everyFrame(client, (dt) => {
119
+ time += dt
120
+ // the wisps on circles of their own about the mage, rising and falling
121
+ const center = where(client, idsOf(client, UNIT.mage)[0] ?? -1) ?? LIGHTS_AT
122
+ wisps.forEach((wisp, i) => {
123
+ const turn = time * (0.5 + (i % 3) * 0.15) + (i / wisps.length) * Math.PI * 2
124
+ const radius = 2.2 + (i % 2) * 1.4
125
+ wisp.set({ x: center.x + Math.cos(turn) * radius, z: center.z + Math.sin(turn) * radius, y: 1.3 + Math.sin(time * 2 + i) * 0.5 })
126
+ })
127
+ // the lantern in the footman's hand, wherever he walks
128
+ const [footman] = idsOf(client, UNIT.footman)
129
+ const hand = footman !== undefined ? client.renderer.kit.point(footman, 'handslot.l') : undefined
130
+ const feet = footman !== undefined ? where(client, footman) : undefined
131
+ if (hand) lantern?.set({ x: hand.x, y: hand.y, z: hand.z })
132
+ else if (feet) lantern?.set({ x: feet.x, y: 1.4, z: feet.z })
133
+ // the beam sweeping round over the field
134
+ const sweep = time * 0.6
135
+ beam?.set({ direction: [Math.cos(sweep), -0.45, Math.sin(sweep)] })
136
+ })
137
+ return () => {
138
+ stop()
139
+ for (const light of [...wisps, lantern, beam]) light?.dispose()
140
+ wisps = []
141
+ lantern = beam = undefined
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lights that move, on the rules' side: a night, a mage in the middle, a footman walking his round. The lights are
3
+ * the screen's (`./lights.look.ts`)
4
+ */
5
+ import { PLAYER, UNIT } from '../common/units'
6
+ import type { ExampleDef } from '../define'
7
+
8
+ export const LIGHTS_AT = { x: 24, z: 24 }
9
+
10
+ export const lights: ExampleDef = {
11
+ id: 'custom-fx-lights',
12
+ category: 'custom-fx',
13
+ title: 'Lights that move',
14
+ summary:
15
+ "Lights added in code (`client.lights.add`), moved every frame: wisps circling the mage, each a glowing ball (`halo`) lighting the ground under it; a lantern carried by a walking footman; a beam sweeping round like a lighthouse's. Change how many wisps, their colours, the flicker.",
16
+ tags: ['lights', 'dynamic lights', 'point light', 'spot light', 'halo', 'night', 'WorldLights', 'LightDef'],
17
+ environment: 'night',
18
+ start: (world) => {
19
+ world.spawn(UNIT.mage, { at: LIGHTS_AT, owner: PLAYER })
20
+ world
21
+ .spawn(UNIT.footman, { at: { x: LIGHTS_AT.x - 8, z: LIGHTS_AT.z + 5 }, owner: PLAYER })
22
+ .order('patrol', { x: LIGHTS_AT.x + 8, z: LIGHTS_AT.z + 5 })
23
+ },
24
+ source: ['examples/custom-fx/lights.ts', 'examples/custom-fx/lights.look.ts']
25
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The examples of effects of your own, on the client's side (`CategoryLooks`): each example's look and controls by its
3
+ * id, and what they add to the game's look: their effects (`fx.defs`, every name starting `customFx.`) and the looks of
4
+ * the units they add
5
+ */
6
+ import type { CategoryLooks } from '../../shell/look'
7
+ import { GLAIVER } from './glaive'
8
+ import { glaiveLook, glaiverLook } from './glaive.look'
9
+ import { gpuFx, gpuLook } from './gpu.look'
10
+ import { lightsLook } from './lights.look'
11
+ import { meteorFx, meteorLook } from './meteor.look'
12
+ import { partsFx, partsLook } from './parts.look'
13
+ import { portalFx, portalLook } from './portal.look'
14
+ import { rendererLook } from './renderer.look'
15
+ import { ribbonsFx, ribbonsLook } from './ribbons.look'
16
+ import { WARDEN } from './shield'
17
+ import { shieldLook, wardenLook } from './shield.look'
18
+
19
+ export const category: CategoryLooks = {
20
+ view: (view) => ({
21
+ ...view,
22
+ units: { ...view.units, looks: { ...view.units.looks, [WARDEN]: wardenLook, [GLAIVER]: glaiverLook } },
23
+ fx: { ...view.fx, defs: { ...view.fx?.defs, ...partsFx, ...ribbonsFx, ...meteorFx, ...gpuFx, ...portalFx } }
24
+ }),
25
+ looks: {
26
+ 'custom-fx-parts': partsLook,
27
+ 'custom-fx-ribbons': ribbonsLook,
28
+ 'custom-fx-meteor': meteorLook,
29
+ 'custom-fx-gpu-particles': gpuLook,
30
+ 'custom-fx-shield': shieldLook,
31
+ 'custom-fx-portal': portalLook,
32
+ 'custom-fx-glaive': glaiveLook,
33
+ 'custom-fx-renderer': rendererLook,
34
+ 'custom-fx-lights': lightsLook
35
+ }
36
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * A meteor, on the client's side: `customFx.meteor`, its fall and its landing in one timeline. The fall is parts on
3
+ * a `path` (a function of the part's age: where it is, from where the effect plays), so the flames, the glowing rock,
4
+ * the ribbon behind it and its light come down together. At `FALL` seconds the landing: ready-made effects as parts
5
+ * (`explosion`, `impact-ring`, `burning-ground`), rocks flung that each trail smoke all their life (`sub` on `birth`),
6
+ * a shake and a crack laid on the ground
7
+ */
8
+ import type { FxDef, FxPath, GameClientApi } from '@v12-engine/client'
9
+ import { button, type ExampleLook, slider } from '../../shell/look'
10
+ import { aim } from '../fx/stage'
11
+ import { METEOR_AT } from './meteor'
12
+
13
+ /** Seconds it falls */
14
+ const FALL = 1.1
15
+
16
+ /** Down from high up and off to one side, faster as it comes: where it is, from where it lands */
17
+ const fall: FxPath = (age, _life, p) => {
18
+ const t = Math.min(age / FALL, 1)
19
+ const left = 1 - t * t
20
+ const size = Number(p.size)
21
+ return [-9 * left * size, 0.4 + 26 * left * size, -6 * left * size]
22
+ }
23
+
24
+ const meteor: FxDef = {
25
+ params: { size: 1 },
26
+ knobs: { size: { min: 0.4, max: 2.5, step: 0.1, about: 'How big the rock, and all it does' } },
27
+ parts: [
28
+ // the fall: a white-hot rock, flames streaming off it, a ribbon of fire, its light
29
+ {
30
+ for: FALL,
31
+ path: fall,
32
+ particles: { rate: 40, lifetime: 0.12, size: (p) => 1.6 * Number(p.size), color: ['#ffffff', '#ffd27a'], texture: 'flare', local: true }
33
+ },
34
+ {
35
+ for: FALL,
36
+ path: fall,
37
+ particles: {
38
+ rate: 260,
39
+ shape: { type: 'sphere', radius: 0.4 },
40
+ speed: [0.5, 2],
41
+ lifetime: [0.3, 0.7],
42
+ size: (p) => [
43
+ [0, 1.2 * Number(p.size)],
44
+ [1, 0.2]
45
+ ],
46
+ color: [
47
+ [0, '#fff0b0'],
48
+ [0.4, '#ff7a1a'],
49
+ [1, '#3a1a0a']
50
+ ],
51
+ texture: 'puff'
52
+ }
53
+ },
54
+ { for: FALL, path: fall, trail: { color: '#ff8a2a', width: (p) => 0.9 * Number(p.size), time: 0.4, soft: true, blend: 'add', taper: true } },
55
+ { for: FALL, path: fall, light: { color: '#ffa040', intensity: 4, range: 14 } },
56
+ // the landing
57
+ { at: FALL, fx: 'explosion', params: (p) => ({ size: 1.6 * Number(p.size), shake: 0 }) },
58
+ { at: FALL, fx: 'impact-ring', params: (p) => ({ size: 1.5 * Number(p.size), shake: 0 }) },
59
+ { at: FALL, shake: (p) => ({ strength: 0.9 * Number(p.size), time: 0.6 }) },
60
+ { at: FALL, decal: { texture: 'crack', size: (p) => 6 * Number(p.size), time: 40, color: '#1a120c' } },
61
+ // rocks flung out on high arcs, each trailing smoke for as long as it flies
62
+ {
63
+ at: FALL,
64
+ particles: {
65
+ bursts: [{ at: 0, count: 26 }],
66
+ shape: 'hemisphere',
67
+ spread: 0.7,
68
+ speed: (p) => [7 * Number(p.size), 13 * Number(p.size)],
69
+ gravity: 16,
70
+ lifetime: [0.8, 1.4],
71
+ size: [0.35, 0.25],
72
+ color: '#ff9a4a',
73
+ texture: 'dot',
74
+ sub: [
75
+ {
76
+ on: 'birth',
77
+ particles: {
78
+ rate: 30,
79
+ lifetime: [0.4, 0.8],
80
+ size: [0.3, 1],
81
+ color: ['#5a4a40', '#2a2420'],
82
+ alpha: [0.7, 0],
83
+ texture: 'puff',
84
+ blend: 'alpha'
85
+ }
86
+ }
87
+ ]
88
+ }
89
+ },
90
+ { at: FALL + 0.1, for: 6, fx: 'burning-ground', params: (p) => ({ radius: 1.6 * Number(p.size), scorch: false }) }
91
+ ]
92
+ }
93
+
94
+ /** The meteor of this example, taken into the game's view */
95
+ export const meteorFx: Record<string, FxDef> = { 'customFx.meteor': meteor }
96
+
97
+ let size = 1
98
+ let shower: number[] = []
99
+
100
+ /** Calls one down a little way from the middle, or (`exact`) on it */
101
+ const call = (client: GameClientApi, exact = false) =>
102
+ client.fx.play('customFx.meteor', {
103
+ at: exact ? METEOR_AT : { x: METEOR_AT.x + (Math.random() - 0.5) * 9, z: METEOR_AT.z + (Math.random() - 0.5) * 7 },
104
+ params: { size: exact ? size : size * (0.5 + Math.random() * 0.6) }
105
+ })
106
+
107
+ export const meteorLook: ExampleLook = {
108
+ controls: [
109
+ button('Call a meteor', (client) => call(client, true), 'One meteor falls on the middle of the field, at the size below'),
110
+ button(
111
+ 'A shower',
112
+ (client) => {
113
+ shower = Array.from({ length: 7 }, (_, i) => window.setTimeout(() => call(client), i * 450 + Math.random() * 200))
114
+ },
115
+ 'Seven meteors, one after another, at spots round the middle'
116
+ ),
117
+ slider(
118
+ 'Size',
119
+ { min: 0.4, max: 2.5, step: 0.1, value: 1 },
120
+ (_client, value) => (size = value),
121
+ 'How big the next meteor is, its blast and crack too (the knob `size`): call one to see it'
122
+ )
123
+ ],
124
+ ready: (client) => {
125
+ size = 1
126
+ aim(client, METEOR_AT, 26, 0.55)
127
+ const first = window.setTimeout(() => call(client, true), 500)
128
+ return () => {
129
+ window.clearTimeout(first)
130
+ for (const one of shower) window.clearTimeout(one)
131
+ }
132
+ }
133
+ }