@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,523 @@
1
+ /**
2
+ * The examples of moves made in code, on the client's side. Two ways in: a look's `pose` (`UnitLook.pose`), called
3
+ * every frame after the client has placed the unit, to move, turn and squash its whole body (the hops, the waddle, the
4
+ * springs, the banking); and the model's bones, turned after the clips have posed them (`./bones.ts`: the look, the
5
+ * reach). And a renderer of the game's own for what is not a unit at all (`./runes.ts`)
6
+ */
7
+ import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder.js'
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 type { EntityView, GameClientApi, GameView, ShapeDef, UnitLook } from '@v12-engine/client'
12
+ import { presetFx } from '@v12-engine/client'
13
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
14
+ import { button, type CategoryLooks, readout, slider, toggle } from '../../shell/look'
15
+ import { character, hold, person, TWO_HANDED } from '../common/looks'
16
+ import { afterClips, aimBone, approach, boneOf, groundUnder, trackPointer, turnBone, worldOf } from './bones'
17
+ import { MOVE, summon } from './index'
18
+ import { runesRenderer } from './runes'
19
+
20
+ // ---- a pose of one's own: what each unit keeps from frame to frame, by its id
21
+
22
+ /** What a pose keeps of a unit between frames: where it was, how fast it went, where it is in its step */
23
+ interface Kept {
24
+ x: number
25
+ z: number
26
+ facing: number
27
+ speed: number
28
+ turn: number
29
+ phase: number
30
+ /** A spring's lean, and how fast it leans, across and along (the springs) */
31
+ lean: { x: number; z: number; vx: number; vz: number }
32
+ health: number
33
+ }
34
+ const kept = new Map<number, Kept>()
35
+ /** What the poses kept, forgotten as an example is left: the next one's units start afresh */
36
+ const forget = () => kept.clear()
37
+
38
+ /** What a pose keeps of this unit, its speed and its turning measured from where it is drawn now and was last frame */
39
+ function measure(entity: EntityView, root: TransformNode, dt: number): Kept {
40
+ let own = kept.get(entity.id)
41
+ if (!own) {
42
+ own = {
43
+ x: root.position.x,
44
+ z: root.position.z,
45
+ facing: root.rotation.y,
46
+ speed: 0,
47
+ turn: 0,
48
+ phase: 0,
49
+ lean: { x: 0, z: 0, vx: 0, vz: 0 },
50
+ health: entity.health
51
+ }
52
+ kept.set(entity.id, own)
53
+ }
54
+ if (dt > 0) {
55
+ const speed = Math.hypot(root.position.x - own.x, root.position.z - own.z) / dt
56
+ let turned = root.rotation.y - own.facing
57
+ turned = Math.atan2(Math.sin(turned), Math.cos(turned))
58
+ // smoothed: the snapshots come a few times a second, a frame's own step is uneven
59
+ own.speed = approach(own.speed, speed, 10, dt)
60
+ own.turn = approach(own.turn, turned / dt, 6, dt)
61
+ }
62
+ own.x = root.position.x
63
+ own.z = root.position.z
64
+ own.facing = root.rotation.y
65
+ return own
66
+ }
67
+
68
+ // ---- the slimes: hops
69
+
70
+ /** How squashy the slimes are, from the card: 0 not at all */
71
+ const slimes = { squash: 0.6 }
72
+
73
+ /** A slime hops while it goes: up in an arc each hop, stretched tall as it leaves and comes down, squashed flat as it lands */
74
+ function hop(entity: EntityView, root: TransformNode, frame: { dt: number; time: number }): void {
75
+ const own = measure(entity, root, frame.dt)
76
+ const going = Math.min(own.speed / 2, 1)
77
+ // it finishes the hop it is in before it stops
78
+ const inHop = own.phase % 1
79
+ if (going > 0.1 || inHop > 0.02) own.phase += frame.dt * 2.4
80
+ else own.phase = 0
81
+ const t = own.phase % 1
82
+ const height = Math.sin(t * Math.PI)
83
+ const rising = Math.cos(t * Math.PI)
84
+ let tall = 1
85
+ if (own.phase > 0) {
86
+ // stretched by how fast it goes up or down; squashed in the moment it touches the ground
87
+ tall += slimes.squash * 0.35 * Math.abs(rising) * height
88
+ if (t < 0.12) tall -= slimes.squash * 0.45 * (1 - t / 0.12)
89
+ if (t > 0.92) tall -= slimes.squash * 0.3 * ((t - 0.92) / 0.08)
90
+ } else tall += Math.sin(frame.time * 3 + entity.id) * 0.04 * (0.5 + slimes.squash)
91
+ root.position.y += height * 0.7 * (own.phase > 0 ? 1 : 0)
92
+ // as much of it as before: what it gains in height it loses across
93
+ root.scaling.y *= tall
94
+ root.scaling.x /= Math.sqrt(tall)
95
+ root.scaling.z /= Math.sqrt(tall)
96
+ }
97
+
98
+ // ---- the waddlers: a walk of rocks and bobs
99
+
100
+ /** A waddler rocks from foot to foot as it walks and bobs on each step, leans forward as fast as it goes, and into its turns */
101
+ function waddle(entity: EntityView, root: TransformNode, frame: { dt: number; time: number }): void {
102
+ const own = measure(entity, root, frame.dt)
103
+ const going = Math.min(own.speed / 2.2, 1)
104
+ own.phase += frame.dt * (1 + own.speed * 2.2)
105
+ root.rotation.z += Math.sin(own.phase * Math.PI) * 0.22 * going - Math.max(-0.35, Math.min(0.35, own.turn * 0.18))
106
+ root.rotation.x += 0.12 * going
107
+ root.position.y += Math.abs(Math.cos(own.phase * Math.PI)) * 0.12 * going
108
+ // standing, it breathes
109
+ root.scaling.y *= 1 + Math.sin(frame.time * 2.5 + entity.id) * 0.02 * (1 - going)
110
+ }
111
+
112
+ // ---- the jellies: springs
113
+
114
+ /** The springs of the jellies, from the card: how stiff (how fast they sway back), how damped (how soon they settle) */
115
+ const springs = { stiffness: 90, damping: 5 }
116
+
117
+ /**
118
+ * A jelly sways away from each blow and back on a spring until it settles: a hit (its health down since last frame)
119
+ * pushes the spring away from the brute, which stands to its west. Each frame the spring pulls it back by how far it
120
+ * leans (`stiffness`) and slows it by how fast it moves (`damping`)
121
+ */
122
+ function wobble(entity: EntityView, root: TransformNode, frame: { dt: number; time: number }): void {
123
+ const own = measure(entity, root, frame.dt)
124
+ const { lean } = own
125
+ if (entity.health < own.health - 3) {
126
+ // away from the brute, which stands to its west, and a little to one side or the other
127
+ lean.vx += 2.6
128
+ lean.vz += Math.sin(frame.time * 7 + entity.id) * 1.2
129
+ }
130
+ own.health = entity.health
131
+ // a few small steps a frame, so a stiff spring does not fly apart on a long frame
132
+ const steps = 4
133
+ const dt = frame.dt / steps
134
+ for (let i = 0; i < steps; i++) {
135
+ lean.vx += (-springs.stiffness * lean.x - springs.damping * lean.vx) * dt
136
+ lean.vz += (-springs.stiffness * lean.z - springs.damping * lean.vz) * dt
137
+ lean.x += lean.vx * dt
138
+ lean.z += lean.vz * dt
139
+ }
140
+ root.rotation.x += lean.z
141
+ root.rotation.z -= lean.x
142
+ // squashed by how hard it is moving: a jelly
143
+ const squash = 1 - Math.min(0.3, Math.hypot(lean.vx, lean.vz) * 0.05)
144
+ root.scaling.y *= squash
145
+ root.scaling.x /= Math.sqrt(squash)
146
+ root.scaling.z /= Math.sqrt(squash)
147
+ }
148
+
149
+ // ---- the glider: banking
150
+
151
+ /** The glider banks as hard as it turns, rises and falls gently along its way, and pitches with it */
152
+ function bank(entity: EntityView, root: TransformNode, frame: { dt: number; time: number }): void {
153
+ const own = measure(entity, root, frame.dt)
154
+ own.phase = approach(own.phase, Math.max(-1.1, Math.min(1.1, own.turn * 0.75)), 4, frame.dt)
155
+ root.rotation.z -= own.phase
156
+ const climb = Math.cos(frame.time * 0.8) * 0.8
157
+ root.position.y += Math.sin(frame.time * 0.8) * 1.0
158
+ root.rotation.x -= climb * 0.25
159
+ }
160
+
161
+ // ---- the looks of the category's units
162
+
163
+ const shape = (
164
+ shape: ShapeDef['shape'],
165
+ size: ShapeDef['size'],
166
+ offset: ShapeDef['offset'],
167
+ color: ShapeDef['color'],
168
+ own: Partial<ShapeDef> = {}
169
+ ): ShapeDef => ({
170
+ shape,
171
+ size,
172
+ offset,
173
+ color,
174
+ ...own
175
+ })
176
+ const WHITE: [number, number, number] = [0.97, 0.97, 0.95]
177
+ const BLACK: [number, number, number] = [0.05, 0.05, 0.08]
178
+
179
+ const looks: Record<string, UnitLook> = {
180
+ [MOVE.customAnimationKnight]: {
181
+ height: 1.8,
182
+ model: character('knight', 0.7, { hold: [hold('sword_1handed'), hold('shield_round', 'l')] }),
183
+ shapes: person(1.8)
184
+ },
185
+ // the slimes, waddlers, jellies and the glider are a few solids on purpose: no model and no clips, every move theirs
186
+ // is the look's `pose`, in code
187
+ [MOVE.customAnimationSlime]: {
188
+ height: 0.9,
189
+ shapes: [
190
+ shape('sphere', [0.95, 0.75, 0.95], [0, 0.36, 0], [0.35, 0.82, 0.4]),
191
+ shape('sphere', [0.22, 0.26, 0.12], [-0.17, 0.5, 0.38], WHITE),
192
+ shape('sphere', [0.22, 0.26, 0.12], [0.17, 0.5, 0.38], WHITE),
193
+ shape('sphere', [0.1, 0.13, 0.06], [-0.17, 0.5, 0.44], BLACK),
194
+ shape('sphere', [0.1, 0.13, 0.06], [0.17, 0.5, 0.44], BLACK)
195
+ ],
196
+ pose: hop
197
+ },
198
+ [MOVE.customAnimationWaddler]: {
199
+ height: 1.1,
200
+ shapes: [
201
+ shape('sphere', [0.7, 0.85, 0.65], [0, 0.5, 0], [0.18, 0.22, 0.3]),
202
+ shape('sphere', [0.5, 0.6, 0.2], [0, 0.48, 0.24], WHITE),
203
+ shape('sphere', [0.46, 0.46, 0.46], [0, 1.05, 0.02], [0.18, 0.22, 0.3]),
204
+ shape('cone', [0.14, 0.22, 0.14], [0, 1.0, 0.3], [1, 0.6, 0.15], { tilt: Math.PI / 2 }),
205
+ shape('sphere', [0.08, 0.08, 0.05], [-0.1, 1.12, 0.22], WHITE),
206
+ shape('sphere', [0.08, 0.08, 0.05], [0.1, 1.12, 0.22], WHITE),
207
+ shape('box', [0.18, 0.06, 0.26], [-0.16, 0.03, 0.08], [1, 0.6, 0.15]),
208
+ shape('box', [0.18, 0.06, 0.26], [0.16, 0.03, 0.08], [1, 0.6, 0.15])
209
+ ],
210
+ pose: waddle
211
+ },
212
+ [MOVE.customAnimationJelly]: {
213
+ height: 1.5,
214
+ shapes: [
215
+ shape('sphere', [1.3, 1.3, 1.3], [0, 0.62, 0], [0.85, 0.3, 0.6]),
216
+ shape('sphere', [0.24, 0.3, 0.12], [-0.22, 0.85, -0.6], WHITE),
217
+ shape('sphere', [0.24, 0.3, 0.12], [0.22, 0.85, -0.6], WHITE),
218
+ shape('sphere', [0.11, 0.14, 0.06], [-0.22, 0.85, -0.66], BLACK),
219
+ shape('sphere', [0.11, 0.14, 0.06], [0.22, 0.85, -0.66], BLACK)
220
+ ],
221
+ pose: wobble
222
+ },
223
+ [MOVE.customAnimationBrute]: {
224
+ height: 2,
225
+ model: character('barbarian', 0.8, { hold: [hold('axe_2handed')] }),
226
+ attacks: [TWO_HANDED],
227
+ shapes: person(2)
228
+ },
229
+ [MOVE.customAnimationGlider]: {
230
+ height: 1,
231
+ altitude: 4,
232
+ motion: { bob: 0 },
233
+ shapes: [
234
+ shape('cone', [0.7, 2.9, 0.7], [0, 0, 0.15], [0.92, 0.9, 0.85], { tilt: Math.PI / 2 }),
235
+ shape('box', [5.2, 0.1, 1.3], [0, 0.03, -0.1], [0.85, 0.25, 0.2]),
236
+ shape('box', [1.8, 0.08, 0.65], [0, 0.08, -1.4], [0.85, 0.25, 0.2]),
237
+ shape('box', [0.08, 0.75, 0.55], [0, 0.36, -1.4], [0.92, 0.9, 0.85])
238
+ ],
239
+ pose: bank
240
+ },
241
+ [MOVE.customAnimationAdept]: { height: 1.8, model: character('mage', 0.7, { hold: [hold('staff')] }), shapes: person(1.8) }
242
+ }
243
+
244
+ /** The one unit of a type the client knows, if it does */
245
+ const one = (client: GameClientApi, type: string) => [...client.state.entities.values()].find((each) => each.def.id === type && each.health > 0)
246
+
247
+ /** Puts the camera at a point, `distance` back and as steep as `pitch` */
248
+ const stage = (client: GameClientApi, x: number, z: number, distance: number, pitch: number, yaw = 0) =>
249
+ client.camera.setPose({ x, z, distance, yaw, pitch })
250
+
251
+ // ---- the look: the head and the chest turned to the point under the pointer
252
+
253
+ const looking = { on: true, most: 1.2 }
254
+
255
+ /** Every frame once the clip has posed the knight: its chest turned a third of the way to the point, its head the rest, eased */
256
+ function lookAtPointer(client: GameClientApi): () => void {
257
+ const { scene } = client
258
+ const now = { yaw: 0, pitch: 0 }
259
+ const pointer = trackPointer(scene)
260
+ const stop = afterClips(scene, (dt) => {
261
+ const knight = one(client, MOVE.customAnimationKnight)
262
+ const root = knight && client.renderer.kit.model(knight.id)?.root
263
+ const head = knight && boneOf(client, knight.id, 'head')
264
+ const chest = knight && boneOf(client, knight.id, 'chest')
265
+ if (!knight || !root || !head || !chest) return
266
+ const facing = root.rotation.y
267
+ const at = worldOf(head)
268
+ const target = looking.on && pointer.at ? groundUnder(scene, pointer.at.x, pointer.at.y) : undefined
269
+ let yaw = 0
270
+ let pitch = 0
271
+ if (target) {
272
+ const dx = target.x - at.x
273
+ const dz = target.z - at.z
274
+ const away = Math.atan2(dx, dz) - facing
275
+ // how far round from where it faces, the short way, no farther than a neck and a waist turn
276
+ yaw = Math.max(-looking.most, Math.min(looking.most, Math.atan2(Math.sin(away), Math.cos(away))))
277
+ pitch = Math.min(0.7, Math.atan2(at.y, Math.hypot(dx, dz)) * 0.8)
278
+ }
279
+ now.yaw = approach(now.yaw, yaw, 8, dt)
280
+ now.pitch = approach(now.pitch, pitch, 8, dt)
281
+ turnBone(chest, now.yaw * 0.35, 0, facing)
282
+ turnBone(head, now.yaw * 0.65, now.pitch, facing + now.yaw * 0.35)
283
+ })
284
+ return () => {
285
+ stop()
286
+ pointer.stop()
287
+ }
288
+ }
289
+
290
+ // ---- the reach: the sword arm aimed at an orb, the lower arm then the upper, twice over
291
+
292
+ const reaching = { pointer: false, on: true }
293
+
294
+ function reachForOrb(client: GameClientApi): () => void {
295
+ const { scene } = client
296
+ const orb = MeshBuilder.CreateSphere('examples.orb', { diameter: 0.22, segments: 12 }, scene)
297
+ const material = new StandardMaterial('examples.orb', scene)
298
+ material.emissiveColor = new Color3(0.4, 0.85, 1)
299
+ material.disableLighting = true
300
+ orb.material = material
301
+ const pointer = trackPointer(scene)
302
+ let time = 0
303
+ const stop = afterClips(scene, (dt) => {
304
+ time += dt
305
+ const knight = one(client, MOVE.customAnimationKnight)
306
+ const root = knight && client.renderer.kit.model(knight.id)?.root
307
+ if (!knight || !root) return
308
+ const shoulder = boneOf(client, knight.id, 'upperarm.r')
309
+ const elbow = boneOf(client, knight.id, 'lowerarm.r')
310
+ const hand = boneOf(client, knight.id, 'hand.r')
311
+ if (!shoulder || !elbow || !hand) return
312
+ const base = root.position
313
+ // the orb: where the pointer is (no farther than a step from the knight), or wandering before it on its own
314
+ let target = new Vector3(
315
+ base.x + Math.sin(time * 0.9) * 0.9,
316
+ base.y + 1.05 + Math.sin(time * 1.7) * 0.45,
317
+ base.z - 0.75 + Math.cos(time * 0.6) * 0.35
318
+ )
319
+ const under = reaching.pointer && pointer.at ? groundUnder(scene, pointer.at.x, pointer.at.y) : undefined
320
+ if (under) {
321
+ const dx = under.x - base.x
322
+ const dz = under.z - base.z
323
+ const far = Math.hypot(dx, dz)
324
+ const k = far > 1.6 ? 1.6 / far : 1
325
+ target = new Vector3(base.x + dx * k, base.y + 1.1, base.z + dz * k)
326
+ }
327
+ orb.position.copyFrom(target)
328
+ if (!reaching.on) return
329
+ // a reach in two joints: the elbow aims the hand at the orb, then the shoulder does, twice over (CCD)
330
+ for (let pass = 0; pass < 2; pass++) {
331
+ aimBone(elbow, worldOf(hand), target, 0.8)
332
+ aimBone(shoulder, worldOf(hand), target, 0.8)
333
+ }
334
+ })
335
+ return () => {
336
+ stop()
337
+ pointer.stop()
338
+ orb.dispose()
339
+ material.dispose()
340
+ }
341
+ }
342
+
343
+ export const category: CategoryLooks = {
344
+ view: (view: GameView<any>) => ({
345
+ ...view,
346
+ units: { ...view.units, looks: { ...view.units.looks, ...looks } },
347
+ fx: { ...view.fx, defs: { ...presetFx(['smoke-trail']), ...view.fx?.defs } }
348
+ }),
349
+ looks: {
350
+ 'custom-animation-look-at': {
351
+ controls: [
352
+ toggle(
353
+ 'Look at the pointer',
354
+ true,
355
+ (_client, on) => (looking.on = on),
356
+ 'On, the knight’s head and chest turn to the ground under the pointer (move it over the field); off, they ease back to the clip'
357
+ ),
358
+ slider(
359
+ 'How far it turns',
360
+ { min: 0.2, max: 1.8, step: 0.05, value: 1.2 },
361
+ (_client, value) => (looking.most = value),
362
+ 'How far the knight may turn to follow the pointer, in radians either way: the waist and the neck together'
363
+ ),
364
+ button(
365
+ 'Play a swing',
366
+ (client) => {
367
+ const knight = one(client, MOVE.customAnimationKnight)
368
+ if (knight) client.playClip(knight.id, 'Melee_1H_Attack_Chop')
369
+ },
370
+ 'The knight swings once: its head keeps looking while the clip moves the rest'
371
+ ),
372
+ readout(
373
+ 'Turned by',
374
+ () => 'scene.onAfterAnimationsObservable, after the clip',
375
+ 'Where the code turns the bones: each frame, once the clip has posed them'
376
+ )
377
+ ],
378
+ ready: (client) => {
379
+ Object.assign(looking, { on: true, most: 1.2 })
380
+ stage(client, 24, 23, 9, 0.75)
381
+ return lookAtPointer(client)
382
+ }
383
+ },
384
+ 'custom-animation-reach': {
385
+ controls: [
386
+ toggle(
387
+ 'Reach',
388
+ true,
389
+ (_client, on) => (reaching.on = on),
390
+ 'On, the knight’s sword arm is turned toward the orb each frame; off, the arm is the clip’s alone'
391
+ ),
392
+ toggle(
393
+ 'The orb follows the pointer',
394
+ false,
395
+ (_client, on) => (reaching.pointer = on),
396
+ 'On, the orb goes where the pointer is over the ground, no farther than a step from the knight; off, it wanders by itself'
397
+ ),
398
+ button(
399
+ 'Play a swing',
400
+ (client) => {
401
+ const knight = one(client, MOVE.customAnimationKnight)
402
+ if (knight) client.playClip(knight.id, 'Melee_1H_Attack_Stab')
403
+ },
404
+ 'The knight stabs once: the reach is laid over the clip'
405
+ )
406
+ ],
407
+ ready: (client) => {
408
+ Object.assign(reaching, { on: true, pointer: false })
409
+ stage(client, 24, 23.6, 8.5, 0.5)
410
+ return reachForOrb(client)
411
+ }
412
+ },
413
+ 'custom-animation-hop': {
414
+ controls: [
415
+ slider(
416
+ 'Squash and stretch',
417
+ { min: 0, max: 1.5, step: 0.05, value: 0.6 },
418
+ (_client, value) => (slimes.squash = value),
419
+ 'How much the slimes stretch as they leave the ground and squash as they land: 0 hops stiff'
420
+ )
421
+ ],
422
+ ready: (client) => {
423
+ slimes.squash = 0.6
424
+ stage(client, 24, 22, 20, 0.7)
425
+ return forget
426
+ }
427
+ },
428
+ 'custom-animation-waddle': {
429
+ controls: [
430
+ readout(
431
+ 'Each frame',
432
+ () => 'rock, bob, lean: from how fast it goes and turns',
433
+ 'What the pose works out for each waddler every frame, from its speed and its turning'
434
+ )
435
+ ],
436
+ ready: (client) => {
437
+ stage(client, 24, 22, 16, 0.65)
438
+ return forget
439
+ }
440
+ },
441
+ 'custom-animation-springs': {
442
+ controls: [
443
+ slider(
444
+ 'Stiffness',
445
+ { min: 10, max: 300, step: 5, value: 90 },
446
+ (_client, value) => (springs.stiffness = value),
447
+ 'How hard the spring pulls it back upright: higher sways faster'
448
+ ),
449
+ slider(
450
+ 'Damping',
451
+ { min: 0.5, max: 25, step: 0.5, value: 5 },
452
+ (_client, value) => (springs.damping = value),
453
+ 'How soon a sway dies down: low wobbles on and on'
454
+ ),
455
+ readout(
456
+ 'Spring',
457
+ () => `stiffness ${springs.stiffness} · damping ${springs.damping}`,
458
+ 'The two sliders’ values, which every jelly’s spring uses'
459
+ )
460
+ ],
461
+ ready: (client) => {
462
+ springs.stiffness = 90
463
+ springs.damping = 5
464
+ stage(client, 25, 24, 21, 0.55)
465
+ return forget
466
+ }
467
+ },
468
+ 'custom-animation-flight': {
469
+ controls: [
470
+ button(
471
+ 'Follow it',
472
+ (client) => {
473
+ const glider = one(client, MOVE.customAnimationGlider)
474
+ if (glider) client.follow(glider.id)
475
+ },
476
+ 'The camera follows the glider round its figure of eight: watch it bank into each turn'
477
+ ),
478
+ button(
479
+ 'Watch from above',
480
+ (client) => {
481
+ client.follow(null)
482
+ stage(client, 24, 23, 30, 0.8)
483
+ },
484
+ 'The camera stops following and looks down on the whole figure of eight'
485
+ )
486
+ ],
487
+ ready: (client) => {
488
+ stage(client, 24, 23, 30, 0.8)
489
+ let trail: ReturnType<GameClientApi['fx']['play']> | undefined
490
+ // the trail once the glider is drawn: on it, following it
491
+ const start = setInterval(() => {
492
+ const glider = one(client, MOVE.customAnimationGlider)
493
+ if (!glider || trail) return
494
+ trail = client.fx.play('smoke-trail', { on: glider.id, params: { color: '#e8eef5' } })
495
+ clearInterval(start)
496
+ }, 250)
497
+ return () => {
498
+ clearInterval(start)
499
+ trail?.stop()
500
+ }
501
+ }
502
+ },
503
+ 'custom-animation-renderer': {
504
+ view: (view) => ({ ...view, renderers: [...(view.renderers ?? []), runesRenderer(MOVE.customAnimationAdept, [0.55, 0.85, 1])] }),
505
+ controls: [
506
+ button(
507
+ 'Summon three more',
508
+ (client) => client.session.send(summon.make({})),
509
+ 'Three more adepts pop in: the renderer is told of each and gives it three runes'
510
+ ),
511
+ readout(
512
+ 'Adepts · runes',
513
+ (client) => {
514
+ const adepts = [...client.state.entities.values()].filter((each) => each.def.id === MOVE.customAnimationAdept && each.health > 0).length
515
+ return `${adepts} · ${adepts * 3}`
516
+ },
517
+ 'How many adepts there are, and the runes the renderer draws for them'
518
+ )
519
+ ],
520
+ ready: (client) => stage(client, 24, 22.5, 16, 0.6)
521
+ }
522
+ }
523
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A renderer of the game's own (`GameView.renderers`): three runes that orbit every adept, made as one comes into view
3
+ * and taken away as it goes, moved every frame round where the client draws it. Made once from the client's render
4
+ * kit (`RenderKit`): its scene to make meshes in, `drawn` for where a unit is this frame, `timers` for a tween
5
+ */
6
+ import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder.js'
7
+ import type { Mesh } from '@babylonjs/core/Meshes/mesh.js'
8
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
9
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
10
+ import { Color3 } from '@babylonjs/core/Maths/math.color.js'
11
+ import { ease, type FeatureRenderer, type RenderKit } from '@v12-engine/client'
12
+
13
+ /** How many runes go round each, how far out, how fast (turns a second) */
14
+ const RUNES = 3
15
+ const RADIUS = 0.95
16
+ const SPEED = 0.35
17
+
18
+ /** The runes round the units of a type: `type` is the unit type's id */
19
+ export const runesRenderer =
20
+ (type: string, color: [number, number, number]) =>
21
+ (kit: RenderKit): FeatureRenderer => {
22
+ const material = new StandardMaterial('examples.runes', kit.scene)
23
+ material.emissiveColor = new Color3(...color)
24
+ material.diffuseColor = Color3.Black()
25
+ material.disableLighting = true
26
+ // one rune made, the others its copies: the same mesh drawn three times a unit
27
+ const source = MeshBuilder.CreatePolyhedron('examples.rune', { type: 1, size: 0.13 }, kit.scene)
28
+ source.material = material
29
+ source.setEnabled(false)
30
+ /** Each adept's runes, by its id: the node they turn round, and the runes */
31
+ const orbits = new Map<number, { node: TransformNode; runes: Mesh[]; tween?: number; born: number }>()
32
+ let time = 0
33
+
34
+ const add = (id: number) => {
35
+ const node = new TransformNode(`examples.runes.${id}`, kit.scene)
36
+ const runes = Array.from({ length: RUNES }, (_, i) => {
37
+ const rune = source.clone(`examples.rune.${id}.${i}`, node)
38
+ rune.setEnabled(true)
39
+ return rune
40
+ })
41
+ node.scaling.setAll(0)
42
+ // popping in: from nothing to its size and a little past, and back (`ease.outBack`), over half a second
43
+ const tween = kit.timers.tween(0.5, (t) => node.scaling.setAll(ease.outBack(t)))
44
+ orbits.set(id, { node, runes, tween, born: time })
45
+ }
46
+ const remove = (id: number) => {
47
+ const orbit = orbits.get(id)
48
+ if (!orbit) return
49
+ if (orbit.tween !== undefined) kit.timers.cancel(orbit.tween)
50
+ orbit.node.dispose()
51
+ orbits.delete(id)
52
+ }
53
+
54
+ return {
55
+ sync: (changes) => {
56
+ for (const entity of changes.added) if (entity.def.id === type) add(entity.id)
57
+ for (const entity of changes.removed) remove(entity.id)
58
+ // one that died stays in the state a while, lying: its runes go at once
59
+ for (const entity of changes.changed) if (entity.health <= 0) remove(entity.id)
60
+ },
61
+ frame: (_alpha, dt) => {
62
+ time += dt
63
+ for (const [id, orbit] of orbits) {
64
+ const drawn = kit.drawn(id)
65
+ orbit.node.setEnabled(!!drawn?.visible)
66
+ if (!drawn?.visible) continue
67
+ orbit.node.position.set(drawn.position.x, drawn.position.y + 1.1, drawn.position.z)
68
+ orbit.runes.forEach((rune, i) => {
69
+ const angle = (time * SPEED + i / RUNES) * Math.PI * 2 + id
70
+ // round in a ring that tilts as it turns, each rune bobbing on a beat of its own, spinning
71
+ rune.position.set(Math.cos(angle) * RADIUS, Math.sin(angle * 2 + i) * 0.18, Math.sin(angle) * RADIUS)
72
+ rune.rotation.set(time * 2 + i, time * 3, 0)
73
+ })
74
+ }
75
+ },
76
+ dispose: () => {
77
+ for (const id of [...orbits.keys()]) remove(id)
78
+ source.dispose()
79
+ material.dispose()
80
+ }
81
+ }
82
+ }