@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,372 @@
1
+ /**
2
+ * The performance examples on the client's side: their units drawn as baked crowds, the cards' sliders that ask for
3
+ * thousands, and a meter on every card (`./meter.ts`) that says how fast the picture is made. Numbers to watch, not
4
+ * to trust to the last frame: they are this machine's, with whatever else it runs
5
+ */
6
+ import type { GameClientApi, GameView, UnitLook } from '@v12-engine/client'
7
+ import { presetFx } from '@v12-engine/client'
8
+ import type { MapDoodad } from '@v12-engine/engine'
9
+ import { button, type CategoryLooks, readout, select, slider, toggle } from '../../shell/look'
10
+ import { character, hold, ONE_HANDED, person } from '../common/looks'
11
+ import { army, battle, charge, flow, march, PERF, volley } from './index'
12
+ import { countUnits, metered, meterReadouts, startMeter } from './meter'
13
+
14
+ /** The category's units' looks. `baked: true`: each drawn as a copy of one mesh, its clips baked into a texture */
15
+ const looks: Record<string, UnitLook> = {
16
+ [PERF.performanceFootman]: {
17
+ height: 1.8,
18
+ model: character('knight', 0.7, { hold: [hold('sword_1handed'), hold('shield_round', 'l')] }),
19
+ attacks: [ONE_HANDED],
20
+ baked: true,
21
+ shapes: person(1.8)
22
+ },
23
+ [PERF.performanceArcher]: {
24
+ height: 1.7,
25
+ model: character('ranger', 0.68, { hold: [hold('bow')], animations: { attack: 'Ranged_2H_Shoot' } }),
26
+ shot: { color: [0.85, 0.75, 0.55], size: 0.2 },
27
+ baked: true,
28
+ shapes: person(1.7)
29
+ },
30
+ // the shared art's training dummy; its parts show while it loads
31
+ [PERF.performanceTarget]: {
32
+ height: 1.6,
33
+ model: { src: 'props/dummy.glb', scale: 0.86 },
34
+ shapes: [
35
+ { shape: 'cylinder', size: [0.12, 1.6, 0.12], offset: [0, 0.8, 0], color: [0.45, 0.31, 0.18] },
36
+ { shape: 'cylinder', size: [1, 0.12, 1], offset: [0, 1.3, 0], tilt: Math.PI / 2, color: [0.85, 0.2, 0.15] }
37
+ ]
38
+ }
39
+ }
40
+
41
+ /** The effects the effects example plays: the ready-made ones, taken into the game's look */
42
+ const EFFECTS = ['fire', 'campfire', 'explosion', 'sparkle', 'spark-burst', 'smoke', 'glow', 'aura'] as const
43
+
44
+ /**
45
+ * Calls `send` once the slider has stood still a moment: a slider tells every step it is dragged through, and an army
46
+ * of thousands laid out at each would be thousands laid out many times over
47
+ */
48
+ function settled(send: (client: GameClientApi, value: number) => void, ms = 250): (client: GameClientApi, value: number) => void {
49
+ let timer: ReturnType<typeof setTimeout> | undefined
50
+ return (client, value) => {
51
+ clearTimeout(timer)
52
+ timer = setTimeout(() => send(client, value), ms)
53
+ }
54
+ }
55
+
56
+ /** Puts the camera over the middle of a field `size` cells a side, `distance` back, and lets it go as far as `far` */
57
+ function overlook(client: GameClientApi, size: number, distance: number, far = distance * 1.6, pitch = 0.9): void {
58
+ client.camera.setLimits({ max: far })
59
+ client.camera.setPose({ x: size / 2, z: size / 2, distance, yaw: 0, pitch })
60
+ }
61
+
62
+ /** The sides of the examples' fields, in cells, as their maps say (`./index.ts`) */
63
+ const SIZE = { army: 128, battle: 128, volley: 96, flow: 112, forest: 192, effects: 96 }
64
+
65
+ /** Sends a command of the game's from the card */
66
+ const send = (client: GameClientApi, command: ReturnType<(typeof army)['make']>) => client.session.send(command)
67
+
68
+ // ---- the forest: trees as scenery, laid out in code and handed to the client's scenery whole
69
+
70
+ const TREES = [
71
+ 'trees/pine_1.glb',
72
+ 'trees/pine_2.glb',
73
+ 'trees/pine_3.glb',
74
+ 'trees/pine_4.glb',
75
+ 'trees/oak_1.glb',
76
+ 'trees/oak_2.glb',
77
+ 'trees/oak_5.glb',
78
+ 'trees/columnar_2.glb'
79
+ ]
80
+
81
+ /** `count` trees over the field, in clumps with clearings, the same every time for the same count (a seeded random) */
82
+ function forest(count: number, size: number): MapDoodad[] {
83
+ let seed = 12345
84
+ const random = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff
85
+ const trees: MapDoodad[] = []
86
+ // clumps of woods: each tree near one, so the forest has glades between
87
+ const clumps = Array.from({ length: 60 }, () => ({ x: 8 + random() * (size - 16), z: 8 + random() * (size - 16), r: 8 + random() * 22 }))
88
+ for (let id = 1; trees.length < count && id < count * 4; id++) {
89
+ const clump = clumps[Math.floor(random() * clumps.length)]!
90
+ const angle = random() * Math.PI * 2
91
+ const away = Math.sqrt(random()) * clump.r
92
+ const x = clump.x + Math.cos(angle) * away
93
+ const z = clump.z + Math.sin(angle) * away
94
+ if (x < 2 || z < 2 || x > size - 2 || z > size - 2) continue
95
+ const scale = 0.7 + random() * 0.6
96
+ trees.push({
97
+ id: 100000 + id,
98
+ model: TREES[Math.floor(random() * TREES.length)]!,
99
+ x,
100
+ z,
101
+ yaw: random() * Math.PI * 2,
102
+ scale: [scale, scale, scale]
103
+ })
104
+ }
105
+ return trees
106
+ }
107
+
108
+ // ---- the effects: many playing at once, the bursts played again as they end
109
+
110
+ /** How many of the effects example's give light */
111
+ const LIT = 12
112
+
113
+ /** What the effects example plays now, to stop it as another count or kind is chosen */
114
+ const playing = new WeakMap<GameClientApi, { count: number; kind: string; stop: () => void }>()
115
+
116
+ /** Plays `count` effects of a kind over the field, in a grid round the middle: bursts again and again, the rest held on */
117
+ function playEffects(client: GameClientApi, count: number, kind: string): void {
118
+ playing.get(client)?.stop()
119
+ const x = SIZE.effects / 2
120
+ const z = SIZE.effects / 2
121
+ const side = Math.ceil(Math.sqrt(count))
122
+ const places = Array.from({ length: count }, (_, i) => ({
123
+ x: x + ((i % side) - (side - 1) / 2) * 2.6,
124
+ z: z + (Math.floor(i / side) - (side - 1) / 2) * 2.6
125
+ }))
126
+ const kinds = kind === 'mixed' ? EFFECTS : [kind]
127
+ const handles = new Set<ReturnType<GameClientApi['fx']['play']>>()
128
+ let stopped = false
129
+ const play = (i: number) => {
130
+ if (stopped) return
131
+ const name = kinds[i % kinds.length]!
132
+ // a light of its own for the first few alone: lights are dear (each lights everything near it), and the scene
133
+ // has room for a few dozen at once; the rest glow by their particles
134
+ const handle = client.fx.play(name, { at: places[i]!, ...(i >= LIT && { params: { light: 0 } }) })
135
+ handles.add(handle)
136
+ // one that ends by itself (a blast, a burst of sparks) is played again a moment after, at the same place
137
+ void handle.done.then(() => {
138
+ handles.delete(handle)
139
+ if (!stopped) setTimeout(() => play(i), 200 + ((i * 37) % 600))
140
+ })
141
+ }
142
+ for (let i = 0; i < count; i++) setTimeout(() => play(i), (i * 7) % 800)
143
+ playing.set(client, {
144
+ count,
145
+ kind,
146
+ stop: () => {
147
+ stopped = true
148
+ for (const handle of handles) handle.stop()
149
+ handles.clear()
150
+ }
151
+ })
152
+ }
153
+
154
+ export const category: CategoryLooks = {
155
+ view: (view: GameView<any>) => ({
156
+ ...view,
157
+ units: { ...view.units, looks: { ...view.units.looks, ...looks } },
158
+ fx: { ...view.fx, defs: { ...presetFx([...EFFECTS]), ...view.fx?.defs } }
159
+ }),
160
+ looks: {
161
+ 'performance-army': {
162
+ view: metered,
163
+ controls: [
164
+ slider(
165
+ 'Footmen',
166
+ { min: 100, max: 5000, step: 100, value: 400 },
167
+ settled((client, count) => send(client, army.make({ count }))),
168
+ 'How many footmen stand in the block in the middle: let go and the army is laid out anew'
169
+ ),
170
+ select(
171
+ 'Formation',
172
+ ['square', 'circle', 'rows'],
173
+ 'square',
174
+ (client, value) => formation.set(client, value as Formation),
175
+ 'The shape the army takes where the buttons below send it: it shows as they march'
176
+ ),
177
+ button(
178
+ 'March north',
179
+ (client) => send(client, march.make({ to: 'north', formation: formation.get(client) ?? 'square' })),
180
+ 'One order for the whole army to the north of the field, in the formation chosen'
181
+ ),
182
+ button(
183
+ 'March south',
184
+ (client) => send(client, march.make({ to: 'south', formation: formation.get(client) ?? 'square' })),
185
+ 'The whole army to the south of the field'
186
+ ),
187
+ button(
188
+ 'Back to the middle',
189
+ (client) => send(client, march.make({ to: 'middle', formation: formation.get(client) ?? 'square' })),
190
+ 'The whole army back to the middle'
191
+ ),
192
+ toggle(
193
+ 'Baked crowd',
194
+ true,
195
+ (client, on) => {
196
+ // the look the same but for its baking: each footman drawn again as a copy, or with a skeleton of its own
197
+ const look = client.looks.get(PERF.performanceFootman)
198
+ if (look) void client.looks.set(PERF.performanceFootman, { ...look, baked: on })
199
+ },
200
+ 'On, the footmen are drawn as copies of a few baked clips; off, every footman has a skeleton of its own, as a hero has: much slower past a few hundred'
201
+ ),
202
+ readout('Footmen', (client) => countUnits(client).toLocaleString(), 'Footmen on the field now'),
203
+ ...meterReadouts()
204
+ ],
205
+ ready: (client) => {
206
+ overlook(client, SIZE.army, 75, 140)
207
+ return startMeter(client)
208
+ }
209
+ },
210
+ 'performance-battle': {
211
+ view: metered,
212
+ controls: [
213
+ slider(
214
+ 'Footmen a side',
215
+ { min: 100, max: 2500, step: 100, value: 150 },
216
+ settled((client, count) => {
217
+ lastBattle.set(client, count)
218
+ send(client, battle.make({ count }))
219
+ }),
220
+ 'How many footmen each side has: let go and both armies are laid out anew'
221
+ ),
222
+ button(
223
+ 'Charge',
224
+ (client) => send(client, charge.make({})),
225
+ 'Both armies at each other, fighting what they meet: the costly part is the melee'
226
+ ),
227
+ button(
228
+ 'Lay them out again',
229
+ (client) => send(client, battle.make({ count: lastBattle.get(client) ?? 150 })),
230
+ 'Both armies whole again and facing each other, as many as the slider says'
231
+ ),
232
+ readout('Blue · red', sides, 'Footmen each side has left alive'),
233
+ ...meterReadouts()
234
+ ],
235
+ ready: (client) => {
236
+ overlook(client, SIZE.battle, 60, 140, 0.75)
237
+ return startMeter(client)
238
+ }
239
+ },
240
+ 'performance-volley': {
241
+ view: metered,
242
+ controls: [
243
+ slider(
244
+ 'Archers',
245
+ { min: 50, max: 2000, step: 50, value: 120 },
246
+ settled((client, count) => send(client, volley.make({ count }))),
247
+ 'How many archers shoot at the targets: let go and they are laid out anew, a target for every eight'
248
+ ),
249
+ readout(
250
+ 'Archers · arrows in the air',
251
+ (client) => {
252
+ const { projectiles } = client.session.snapshot()
253
+ return `${countUnits(client, (type) => type === PERF.performanceArcher)} · ${projectiles.length}`
254
+ },
255
+ 'Archers on the field, and the arrows flying now: each a missile the rules move'
256
+ ),
257
+ ...meterReadouts()
258
+ ],
259
+ ready: (client) => {
260
+ overlook(client, SIZE.volley, 45, 120, 0.7)
261
+ return startMeter(client)
262
+ }
263
+ },
264
+ 'performance-flow': {
265
+ view: metered,
266
+ controls: [
267
+ slider(
268
+ 'Footmen a crowd',
269
+ { min: 50, max: 1000, step: 50, value: 100 },
270
+ settled((client, count) => send(client, flow.make({ count }))),
271
+ 'How many footmen in each of the two crowds that cross the field through each other'
272
+ ),
273
+ readout('Footmen', (client) => countUnits(client).toLocaleString(), 'Footmen on the field now, both crowds'),
274
+ ...meterReadouts()
275
+ ],
276
+ ready: (client) => {
277
+ overlook(client, SIZE.flow, 70, 140, 0.95)
278
+ return startMeter(client)
279
+ }
280
+ },
281
+ 'performance-forest': {
282
+ view: metered,
283
+ controls: [
284
+ slider(
285
+ 'Trees',
286
+ { min: 1000, max: 40000, step: 1000, value: 8000 },
287
+ settled((client, count) => client.doodads.set(forest(count, SIZE.forest)), 400),
288
+ 'How many trees stand in the forest: let go and it is planted anew'
289
+ ),
290
+ button(
291
+ 'Fly across',
292
+ (client) => {
293
+ const x = SIZE.forest / 2
294
+ const z = SIZE.forest / 2
295
+ client.camera.setPose({ x: x - 60, z: z - 60, distance: 30, yaw: Math.PI / 4, pitch: 0.55 })
296
+ client.camera.panTo({ x: x + 60, z: z + 60 }, 12, 'linear')
297
+ },
298
+ 'The camera low over the trees, flying corner to corner for twelve seconds'
299
+ ),
300
+ button('Look over it all', (client) => overlook(client, SIZE.forest, 150, 220, 0.75), 'The camera high, the whole forest in view'),
301
+ readout(
302
+ 'Trees · drawn as',
303
+ (client) => {
304
+ const stats = client.doodads.stats
305
+ return `${stats.doodads.toLocaleString()} · ${stats.copies.toLocaleString()} copies, ${stats.merged.toLocaleString()} merged, ${stats.own} own`
306
+ },
307
+ 'Trees, and how they are drawn: as copies of one mesh, merged into big meshes, or each a mesh of its own'
308
+ ),
309
+ ...meterReadouts()
310
+ ],
311
+ ready: (client) => {
312
+ client.doodads.set(forest(8000, SIZE.forest))
313
+ overlook(client, SIZE.forest, 90, 220, 0.7)
314
+ return startMeter(client)
315
+ }
316
+ },
317
+ 'performance-effects': {
318
+ view: metered,
319
+ controls: [
320
+ slider(
321
+ 'Effects',
322
+ { min: 25, max: 300, step: 25, value: 150 },
323
+ settled((client, count) => playEffects(client, count, playing.get(client)?.kind ?? 'mixed')),
324
+ 'How many effects play at once in a grid over the field, the first twelve with a light'
325
+ ),
326
+ select(
327
+ 'Kind',
328
+ ['mixed', ...EFFECTS],
329
+ 'mixed',
330
+ (client, kind) => playEffects(client, playing.get(client)?.count ?? 150, kind),
331
+ 'Which effect plays in every place, or a mix of all of them'
332
+ ),
333
+ readout(
334
+ 'Effects · particles · lights',
335
+ (client) => {
336
+ const stats = client.fx.stats()
337
+ return `${stats.effects} · ${stats.particles.toLocaleString()} · ${stats.lights}`
338
+ },
339
+ 'Effects playing, the particles they hold, and the lights they give'
340
+ ),
341
+ ...meterReadouts()
342
+ ],
343
+ ready: (client) => {
344
+ overlook(client, SIZE.effects, 50, 120, 0.8)
345
+ playEffects(client, 150, 'mixed')
346
+ const stop = startMeter(client)
347
+ return () => {
348
+ playing.get(client)?.stop()
349
+ stop()
350
+ }
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ type Formation = 'square' | 'circle' | 'rows'
357
+ /** The formation chosen on the army example's card, by client */
358
+ const formation = new WeakMap<GameClientApi, Formation>()
359
+ /** The size of the battle last laid out, by client */
360
+ const lastBattle = new WeakMap<GameClientApi, number>()
361
+
362
+ /** The two sides of the battle, alive: blue's and red's */
363
+ function sides(client: GameClientApi): string {
364
+ let blue = 0
365
+ let red = 0
366
+ for (const each of client.session.snapshot().entities) {
367
+ if (each.health <= 0) continue
368
+ if (each.owner === 1) blue++
369
+ else if (each.owner === 2) red++
370
+ }
371
+ return `${blue.toLocaleString()} · ${red.toLocaleString()}`
372
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * A meter of how fast the game is made, for the performance examples' cards: frames a second and the milliseconds
3
+ * from one frame to the next (the slowest of the last half second too), the CPU's work of a frame and the
4
+ * simulation's part of it, and the draws a frame takes. The CPU's work is timed by phases of the frame of our own
5
+ * (`GameView.framePhases`), one before the client's first phase and one after its last, and two round `simulate`; the
6
+ * draws are counted by Babylon (`SceneInstrumentation`). `metered` adds the phases to an example's look; `startMeter`,
7
+ * in its `ready`, starts adding up; the card's readouts (`meterReadouts`) say what it found
8
+ */
9
+ import { SceneInstrumentation } from '@babylonjs/core/Instrumentation/sceneInstrumentation.js'
10
+ import type { GameClientApi, GameView } from '@v12-engine/client'
11
+ import { type ExampleControl, readout } from '../../shell/look'
12
+
13
+ /** What the meter has measured over the last half second */
14
+ export interface Measured {
15
+ /** Frames drawn a second */
16
+ fps: number
17
+ /** Milliseconds from one frame to the next, on average */
18
+ frameMs: number
19
+ /** The slowest frame of the last half second, in milliseconds */
20
+ worstMs: number
21
+ /** Milliseconds of the CPU's work a frame: every phase of the client's, the picture's draw calls with them */
22
+ cpuMs: number
23
+ /** Of it, the simulation's: the world stepped and its snapshot taken (`simulate`) */
24
+ simMs: number
25
+ /** Draws a frame: the view and every shadow pass */
26
+ draws: number
27
+ }
28
+
29
+ /** The sums of the half second under way, and what they last came to */
30
+ interface Meter {
31
+ measured: Measured
32
+ frameAt: number
33
+ simAt: number
34
+ cpu: number
35
+ sim: number
36
+ stop(): void
37
+ }
38
+
39
+ /** The meter of each client playing an example now */
40
+ const meters = new WeakMap<GameClientApi, Meter>()
41
+
42
+ /** An example's look with the meter's phases of the frame: what times the CPU's work and the simulation's */
43
+ export function metered(view: GameView<any>): GameView<any> {
44
+ const now = () => performance.now()
45
+ return {
46
+ ...view,
47
+ framePhases: [
48
+ ...(view.framePhases ?? []),
49
+ {
50
+ name: 'performance.frameStart',
51
+ before: 'measure',
52
+ run: (_frame, client) => void (meters.get(client) && (meters.get(client)!.frameAt = now()))
53
+ },
54
+ { name: 'performance.simStart', before: 'simulate', run: (_frame, client) => void (meters.get(client) && (meters.get(client)!.simAt = now())) },
55
+ {
56
+ name: 'performance.simEnd',
57
+ after: 'simulate',
58
+ run: (_frame, client) => {
59
+ const meter = meters.get(client)
60
+ if (meter) meter.sim += now() - meter.simAt
61
+ }
62
+ },
63
+ {
64
+ name: 'performance.frameEnd',
65
+ after: 'hud',
66
+ run: (_frame, client) => {
67
+ const meter = meters.get(client)
68
+ if (meter) meter.cpu += now() - meter.frameAt
69
+ }
70
+ }
71
+ ]
72
+ }
73
+ }
74
+
75
+ /** Starts adding up the client's frames; what it gives back stops it */
76
+ export function startMeter(client: GameClientApi): () => void {
77
+ meters.get(client)?.stop()
78
+ const instruments = new SceneInstrumentation(client.scene)
79
+ const measured: Measured = { fps: 0, frameMs: 0, worstMs: 0, cpuMs: 0, simMs: 0, draws: 0 }
80
+ let last = performance.now()
81
+ let since = last
82
+ let frames = 0
83
+ let gaps = 0
84
+ let worst = 0
85
+ let draws = 0
86
+ const observer = client.scene.onAfterRenderObservable.add(() => {
87
+ const now = performance.now()
88
+ const gap = now - last
89
+ last = now
90
+ frames++
91
+ gaps += gap
92
+ worst = Math.max(worst, gap)
93
+ draws += instruments.drawCallsCounter.current
94
+ if (now - since < 500) return
95
+ measured.fps = (frames * 1000) / (now - since)
96
+ measured.frameMs = gaps / frames
97
+ measured.worstMs = worst
98
+ measured.cpuMs = meter.cpu / frames
99
+ measured.simMs = meter.sim / frames
100
+ measured.draws = Math.round(draws / frames)
101
+ since = now
102
+ frames = gaps = worst = draws = meter.cpu = meter.sim = 0
103
+ })
104
+ const meter: Meter = {
105
+ measured,
106
+ frameAt: last,
107
+ simAt: last,
108
+ cpu: 0,
109
+ sim: 0,
110
+ stop: () => {
111
+ client.scene.onAfterRenderObservable.remove(observer)
112
+ instruments.dispose()
113
+ if (meters.get(client) === meter) meters.delete(client)
114
+ }
115
+ }
116
+ meters.set(client, meter)
117
+ return meter.stop
118
+ }
119
+
120
+ /** What the client's meter has measured, if it is measuring */
121
+ export const meterOf = (client: GameClientApi): Measured | undefined => meters.get(client)?.measured
122
+
123
+ /** The meter's lines on a card: frames a second and frame time, the CPU's work and the simulation's, draws */
124
+ export function meterReadouts(): ExampleControl[] {
125
+ return [
126
+ readout(
127
+ 'Frames a second',
128
+ (client) => {
129
+ const m = meterOf(client)
130
+ return m ? `${m.fps.toFixed(0)} fps · ${m.frameMs.toFixed(1)} ms (worst ${m.worstMs.toFixed(0)})` : '…'
131
+ },
132
+ 'Frames drawn a second, the time between them, and the slowest frame of late'
133
+ ),
134
+ readout(
135
+ 'CPU a frame · of it the simulation',
136
+ (client) => {
137
+ const m = meterOf(client)
138
+ return m ? `${m.cpuMs.toFixed(1)} ms · ${m.simMs.toFixed(1)} ms` : '…'
139
+ },
140
+ 'What the page’s own work takes a frame, and how much of that is the game’s rules (the rest is drawing)'
141
+ ),
142
+ readout(
143
+ 'Draws a frame',
144
+ (client) => {
145
+ const m = meterOf(client)
146
+ return m ? String(m.draws) : '…'
147
+ },
148
+ 'Draw calls the graphics card is sent a frame: fewer is cheaper, and a baked crowd is a handful'
149
+ )
150
+ ]
151
+ }
152
+
153
+ /** How many of the world's units are alive, by what they are (a type's id, or any of them), as the player sees them */
154
+ export function countUnits(client: GameClientApi, test: (type: string) => boolean = () => true): number {
155
+ let count = 0
156
+ for (const each of client.session.snapshot().entities) if (each.health > 0 && test(each.type)) count++
157
+ return count
158
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Colour grades made in code: look-up tables (`.3dl`, a colour in for every colour out) written as text from a
3
+ * function of a colour, and handed to the picture as files of their own. A grade is usually a file a colourist made
4
+ * (`MapPictureLook.grade: 'grades/film.3dl'` in the game's `textures/`); here the picture's `resolve` is taught where
5
+ * these are, so the same field takes them
6
+ */
7
+ import type { GameClientApi } from '@v12-engine/client'
8
+
9
+ type Rgb = [number, number, number]
10
+
11
+ /** How many steps of each colour the tables have: 17 is what colourists' tools write */
12
+ const SIZE = 17
13
+
14
+ const clamp = (v: number) => Math.min(1, Math.max(0, v))
15
+ const luma = ([r, g, b]: Rgb) => r * 0.2126 + g * 0.7152 + b * 0.0722
16
+ const mix = (a: Rgb, b: Rgb, t: number): Rgb => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
17
+ /** More apart from the middle grey, by `amount` (1 as it is) */
18
+ const contrast = (v: number, amount: number) => clamp(0.5 + (v - 0.5) * amount)
19
+ /** An S curve: darks darker, lights lighter, by `amount` (0 as it is) */
20
+ const curve = (v: number, amount: number) => clamp(v + (v - v * v) * (v - 0.5) * amount * 4)
21
+
22
+ /** The grades, by name: what each does to a colour */
23
+ export const GRADES: Record<string, (color: Rgb) => Rgb> = {
24
+ // the blockbuster's: shadows to teal, skin and highlights to orange
25
+ 'Teal and orange': (color) => {
26
+ const y = luma(color)
27
+ const toned = mix([0.1, 0.32, 0.38], [1, 0.72, 0.45], y)
28
+ return mix(color, toned, 0.35).map((v) => curve(v, 0.6)) as Rgb
29
+ },
30
+ // silver kept in the print: less colour, harder light
31
+ 'Bleach bypass': (color) => {
32
+ const y = luma(color)
33
+ return mix(color, [y, y, y], 0.6).map((v) => curve(contrast(v, 1.15), 0.9)) as Rgb
34
+ },
35
+ // slide film in the wrong chemicals: greens and yellows pushed, blue shadows
36
+ 'Cross process': ([r, g, b]) => [curve(r, 1.1), clamp(g * 1.05 + 0.03), clamp(b * 0.75 + 0.12)],
37
+ // a warm old print, faded blacks
38
+ 'Warm film': ([r, g, b]) => [clamp(r * 0.92 + 0.08), clamp(g * 0.86 + 0.06), clamp(b * 0.7 + 0.05)],
39
+ // cold and blue, as films shoot night by day
40
+ Moonlight: (color) => {
41
+ const y = luma(color)
42
+ return [clamp(y * 0.55), clamp(y * 0.75 + 0.02), clamp(y * 1.05 + 0.06)]
43
+ },
44
+ // as the colour came out of an early colour film: two strips, red and cyan
45
+ 'Two strip': ([r, g, b]) => {
46
+ const cyan = (g + b) / 2
47
+ return [clamp(r), clamp(cyan * 0.9 + r * 0.1), clamp(cyan)]
48
+ }
49
+ }
50
+
51
+ /** A grade as the text of a `.3dl` file: the steps on the first line, then a colour out for each in, red slowest, blue fastest */
52
+ export function threeDl(grade: (color: Rgb) => Rgb): string {
53
+ const top = 4095
54
+ const steps = Array.from({ length: SIZE }, (_, i) => Math.round((i / (SIZE - 1)) * 1023))
55
+ const lines = [steps.join(' ')]
56
+ for (let r = 0; r < SIZE; r++)
57
+ for (let g = 0; g < SIZE; g++)
58
+ for (let b = 0; b < SIZE; b++) {
59
+ const out = grade([r / (SIZE - 1), g / (SIZE - 1), b / (SIZE - 1)])
60
+ lines.push(out.map((v) => Math.round(clamp(v) * top)).join(' '))
61
+ }
62
+ // Babylon reads the table as a share of its brightest value: one more of full white past its end (read, and left
63
+ // out of the table) so that a grade that darkens is not brightened back
64
+ lines.push(`${top} ${top} ${top}`)
65
+ return lines.join('\n')
66
+ }
67
+
68
+ /**
69
+ * Teaches the picture where the grades are: `grades/<name>.3dl` resolved to a file made in the page (a blob, which
70
+ * Babylon reads as a `.3dl`). Gives back what forgets them again
71
+ */
72
+ export function teachGrades(client: GameClientApi): () => void {
73
+ const picture = client.picture
74
+ const before = picture.resolve
75
+ const urls = new Map<string, string>()
76
+ for (const [name, grade] of Object.entries(GRADES))
77
+ urls.set(gradePath(name), URL.createObjectURL(new Blob([threeDl(grade)], { type: 'text/plain' })))
78
+ picture.resolve = (path) => urls.get(path) ?? before(path)
79
+ return () => {
80
+ picture.resolve = before
81
+ for (const url of urls.values()) URL.revokeObjectURL(url)
82
+ }
83
+ }
84
+
85
+ /** The path a grade is known by */
86
+ export const gradePath = (name: string): string => `grades/${name.toLowerCase().replace(/ /g, '-')}.3dl`