@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,290 @@
1
+ /**
2
+ * The examples' frame, as three.js's examples page has it: the list down the left (searched, by category, the one open
3
+ * marked), and the example on the right with a card over it (its name, what it shows, its controls, where its code is).
4
+ * Plain HTML, so the list's page carries nothing of the client: the game is drawn into `content`
5
+ */
6
+ import { CATEGORIES, type ExampleDef } from '../examples/define'
7
+ import { examples } from '../examples/catalog'
8
+ import type { GameClientApi } from '@v12-engine/client'
9
+ import { errorFields } from '@v12-engine/engine'
10
+ import type { ExampleControl, ExampleLook } from './look'
11
+ import { STYLE } from './style'
12
+
13
+ /** What the frame is told: the example open (none on the list's own page), and how to open another */
14
+ export interface ShellOptions {
15
+ current?: ExampleDef
16
+ open: (id: string) => void
17
+ /** The start screen, from the frame's top */
18
+ home: () => void
19
+ /** Fetches an example's code and files ahead, as the pointer comes over its link */
20
+ warm?: (id: string) => void
21
+ }
22
+
23
+ /** The frame put up in an element: where the example is drawn, and what fills in its card once its game is drawn */
24
+ export interface Shell {
25
+ /** Where the example (or the list's page) is drawn */
26
+ content: HTMLElement
27
+ /** The example's controls, run with its client once its game is drawn */
28
+ controls(look: ExampleLook | undefined, client: GameClientApi): void
29
+ dispose(): void
30
+ }
31
+
32
+ /** Where the list was scrolled to and what was searched, kept from one example to the next */
33
+ const kept = { scroll: 0, search: '', collapsed: false }
34
+
35
+ /** Puts the frame up in `element`; `dispose` takes it away */
36
+ export function mountShell(element: HTMLElement, options: ShellOptions): Shell {
37
+ addStyle()
38
+ const root = make('div', 'vx-root')
39
+ root.classList.toggle('vx-collapsed', kept.collapsed)
40
+ const side = make('aside', 'vx-side')
41
+ const content = make('main', 'vx-content')
42
+ root.append(side, content)
43
+ element.append(root)
44
+
45
+ // the top: the name, the start screen, the list folded away
46
+ const top = make('header', 'vx-top')
47
+ const brand = make('button', 'vx-brand')
48
+ brand.innerHTML = 'V12 <span>examples</span>'
49
+ brand.title = 'All the examples'
50
+ brand.onclick = () => options.open('')
51
+ const home = make('button', 'vx-icon')
52
+ home.textContent = '⌂'
53
+ home.title = 'The start screen'
54
+ home.onclick = options.home
55
+ const fold = make('button', 'vx-icon vx-fold')
56
+ fold.textContent = '⟨'
57
+ fold.title = 'Fold the list away'
58
+ fold.onclick = () => {
59
+ kept.collapsed = !kept.collapsed
60
+ root.classList.toggle('vx-collapsed', kept.collapsed)
61
+ fold.textContent = kept.collapsed ? '⟩' : '⟨'
62
+ // the game's canvas is sized by its element: told the element changed
63
+ window.dispatchEvent(new Event('resize'))
64
+ }
65
+ fold.textContent = kept.collapsed ? '⟩' : '⟨'
66
+ top.append(brand, home, fold)
67
+ for (const each of [brand, home, fold]) keepFocusOff(each)
68
+
69
+ const search = make('input', 'vx-search') as HTMLInputElement
70
+ search.type = 'search'
71
+ search.placeholder = `Search ${examples.length} examples ( / )`
72
+ search.value = kept.search
73
+ search.spellcheck = false
74
+
75
+ const list = make('nav', 'vx-list')
76
+ side.append(top, search, list)
77
+
78
+ const links: { def: ExampleDef; link: HTMLElement; words: string }[] = []
79
+ for (const category of CATEGORIES) {
80
+ const own = examples.filter((each) => each.category === category.id)
81
+ if (!own.length) continue
82
+ const section = make('section', 'vx-section')
83
+ const title = make('h3', '')
84
+ title.textContent = category.title
85
+ title.title = category.summary
86
+ section.append(title)
87
+ for (const def of own) {
88
+ const link = make('a', 'vx-link') as HTMLAnchorElement
89
+ link.href = `?${def.id}`
90
+ link.textContent = def.title
91
+ link.title = def.summary
92
+ if (def.id === options.current?.id) link.classList.add('vx-on')
93
+ keepFocusOff(link)
94
+ // its code and files fetched while the pointer is on its way to press it
95
+ link.onpointerenter = () => def.id !== options.current?.id && options.warm?.(def.id)
96
+ link.onclick = (event) => {
97
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return
98
+ event.preventDefault()
99
+ if (def.id !== options.current?.id) options.open(def.id)
100
+ }
101
+ section.append(link)
102
+ links.push({ def, link, words: [def.title, def.summary, def.id, category.title, ...(def.tags ?? [])].join(' ').toLowerCase() })
103
+ }
104
+ list.append(section)
105
+ }
106
+
107
+ const filter = () => {
108
+ kept.search = search.value
109
+ const words = search.value.toLowerCase().split(/\s+/).filter(Boolean)
110
+ for (const each of links) each.link.hidden = !words.every((word) => each.words.includes(word))
111
+ for (const section of list.querySelectorAll('section'))
112
+ (section as HTMLElement).hidden = ![...section.querySelectorAll('a')].some((a) => !a.hidden)
113
+ }
114
+ search.oninput = filter
115
+ search.onkeydown = (event) => {
116
+ if (event.key === 'Enter') links.find((each) => !each.link.hidden)?.link.click()
117
+ if (event.key === 'Escape') {
118
+ search.value = ''
119
+ filter()
120
+ search.blur()
121
+ }
122
+ // the game's keys are not for what is typed here
123
+ event.stopPropagation()
124
+ }
125
+ filter()
126
+ list.onscroll = () => (kept.scroll = list.scrollTop)
127
+ requestAnimationFrame(() => {
128
+ list.scrollTop = kept.scroll
129
+ const on = list.querySelector<HTMLElement>('.vx-on')
130
+ if (on && (on.offsetTop < list.scrollTop || on.offsetTop > list.scrollTop + list.clientHeight - 40)) on.scrollIntoView({ block: 'center' })
131
+ })
132
+ const slash = (event: KeyboardEvent) => {
133
+ if (event.key !== '/' || document.activeElement === search) return
134
+ event.preventDefault()
135
+ event.stopPropagation()
136
+ search.focus()
137
+ search.select()
138
+ }
139
+ window.addEventListener('keydown', slash, true)
140
+
141
+ // the card over the example: its name, what it shows, its controls, where its code is
142
+ let card: HTMLElement | undefined
143
+ let controlsBox: HTMLElement | undefined
144
+ let hintLine: HTMLElement | undefined
145
+ if (options.current) {
146
+ const def = options.current
147
+ const category = CATEGORIES.find((each) => each.id === def.category)
148
+ card = make('div', 'vx-card')
149
+ const head = make('div', 'vx-card-head')
150
+ const name = make('h2', '')
151
+ name.textContent = def.title
152
+ const shut = make('button', 'vx-icon')
153
+ shut.textContent = '–'
154
+ shut.title = 'Fold the card'
155
+ shut.onclick = () => card!.classList.toggle('vx-card-folded')
156
+ keepFocusOff(shut)
157
+ head.append(name, shut)
158
+ const about = make('p', 'vx-summary')
159
+ about.textContent = def.summary
160
+ controlsBox = make('div', 'vx-controls')
161
+ controlsBox.innerHTML = '<div class="vx-wait">Loading…</div>'
162
+ hintLine = make('p', 'vx-hint')
163
+ hintLine.textContent = POINT_AT
164
+ const where = make('p', 'vx-where')
165
+ const folder = `src/examples/${def.category}/`
166
+ where.innerHTML = `<span>${category?.title ?? ''}</span> · <code>${folder}</code>`
167
+ where.title = `Its rules in ${folder}index.ts, its look and controls in ${folder}looks.ts${def.source?.length ? `, and ${def.source.join(', ')}` : ''}`
168
+ card.append(head, about, controlsBox, hintLine, where)
169
+ content.append(card)
170
+ }
171
+
172
+ const timers: number[] = []
173
+ return {
174
+ content,
175
+ controls(look, client) {
176
+ if (!controlsBox) return
177
+ const shown = (hint?: string) => hintLine && (hintLine.textContent = hint ?? POINT_AT)
178
+ controlsBox.replaceChildren(...(look?.controls ?? []).map((control) => controlOf(control, client, timers, shown)))
179
+ if (hintLine) hintLine.hidden = !look?.controls?.some((control) => control.hint)
180
+ },
181
+ dispose() {
182
+ window.removeEventListener('keydown', slash, true)
183
+ for (const timer of timers) clearInterval(timer)
184
+ root.remove()
185
+ }
186
+ }
187
+ }
188
+
189
+ /** A control as an element, run with the client */
190
+ function controlOf(control: ExampleControl, client: GameClientApi, timers: number[], shown: (hint?: string) => void): HTMLElement {
191
+ const row = make('div', `vx-control vx-${control.kind}`)
192
+ if (control.hint) {
193
+ row.title = control.hint
194
+ row.onpointerenter = () => shown(control.hint)
195
+ row.onpointerleave = () => shown()
196
+ }
197
+ const label = make('label', '')
198
+ label.textContent = control.label
199
+ const run = (action: () => void) => {
200
+ try {
201
+ action()
202
+ } catch (error) {
203
+ client.log.error('examples', `The control "${control.label}" failed`, errorFields(error))
204
+ }
205
+ }
206
+ switch (control.kind) {
207
+ case 'button': {
208
+ const button = make('button', 'vx-button')
209
+ button.textContent = control.label
210
+ button.onclick = () => run(() => control.run(client))
211
+ keepFocusOff(button)
212
+ row.append(button)
213
+ break
214
+ }
215
+ case 'toggle': {
216
+ const box = make('input', '') as HTMLInputElement
217
+ box.type = 'checkbox'
218
+ box.checked = control.value
219
+ box.onchange = () => {
220
+ run(() => control.set(client, box.checked))
221
+ box.blur()
222
+ }
223
+ label.prepend(box)
224
+ row.append(label)
225
+ break
226
+ }
227
+ case 'slider': {
228
+ const value = make('span', 'vx-value')
229
+ const range = make('input', '') as HTMLInputElement
230
+ range.type = 'range'
231
+ range.min = String(control.min)
232
+ range.max = String(control.max)
233
+ range.step = String(control.step ?? (control.max - control.min) / 100)
234
+ range.value = String(control.value)
235
+ value.textContent = String(control.value)
236
+ range.oninput = () => {
237
+ value.textContent = range.value
238
+ run(() => control.set(client, Number(range.value)))
239
+ }
240
+ // let go, the arrow keys are the camera's again
241
+ range.onpointerup = () => range.blur()
242
+ label.append(value)
243
+ row.append(label, range)
244
+ break
245
+ }
246
+ case 'select': {
247
+ const choice = make('select', '') as HTMLSelectElement
248
+ for (const option of control.options) choice.append(new Option(option, option, false, option === control.value))
249
+ choice.onchange = () => {
250
+ run(() => control.set(client, choice.value))
251
+ choice.blur()
252
+ }
253
+ row.append(label, choice)
254
+ break
255
+ }
256
+ case 'text': {
257
+ const value = make('span', 'vx-value')
258
+ const update = () => run(() => (value.textContent = control.text(client)))
259
+ update()
260
+ timers.push(window.setInterval(update, 500))
261
+ label.append(value)
262
+ row.append(label)
263
+ break
264
+ }
265
+ }
266
+ return row
267
+ }
268
+
269
+ /** What the hint line says while the pointer is over no control */
270
+ const POINT_AT = 'Point at a control to see what it does.'
271
+
272
+ /**
273
+ * A button or link pressed with the mouse keeps the focus where it was: the game's keys (the arrows, space) stay the
274
+ * game's, not this element's. The keyboard still reaches it with Tab
275
+ */
276
+ const keepFocusOff = (element: HTMLElement) => element.addEventListener('mousedown', (event) => event.preventDefault())
277
+
278
+ const make = (tag: string, className: string): HTMLElement => {
279
+ const element = document.createElement(tag)
280
+ if (className) element.className = className
281
+ return element
282
+ }
283
+
284
+ function addStyle(): void {
285
+ if (document.getElementById('vx-style')) return
286
+ const style = document.createElement('style')
287
+ style.id = 'vx-style'
288
+ style.textContent = STYLE
289
+ document.head.append(style)
290
+ }
@@ -0,0 +1,202 @@
1
+ /** The frame's styles (`./shell.ts`), put on the page once */
2
+ export const STYLE = `
3
+ .vx-root {
4
+ --vx-bg: #101114;
5
+ --vx-panel: #16181d;
6
+ --vx-line: #262a33;
7
+ --vx-text: #d7dbe3;
8
+ --vx-dim: #8a91a0;
9
+ --vx-accent: #f2b84b;
10
+ --vx-accent-dim: rgba(242, 184, 75, 0.14);
11
+ --vx-side: 280px;
12
+ position: absolute;
13
+ inset: 0;
14
+ display: flex;
15
+ background: var(--vx-bg);
16
+ color: var(--vx-text);
17
+ font: 13px/1.45 system-ui, -apple-system, 'Segoe UI', sans-serif;
18
+ }
19
+ .vx-side *, .vx-card, .vx-card *, .vx-index * { box-sizing: border-box; }
20
+ .vx-side {
21
+ width: var(--vx-side);
22
+ flex: none;
23
+ display: flex;
24
+ flex-direction: column;
25
+ border-right: 1px solid var(--vx-line);
26
+ background: var(--vx-panel);
27
+ transition: margin-left 160ms ease;
28
+ z-index: 2;
29
+ }
30
+ .vx-collapsed .vx-side { margin-left: calc(-1 * var(--vx-side) + 34px); }
31
+ .vx-collapsed .vx-side > :not(.vx-top) { visibility: hidden; }
32
+ .vx-collapsed .vx-top > :not(.vx-fold) { visibility: hidden; }
33
+ .vx-top { display: flex; align-items: center; gap: 4px; padding: 10px 8px 8px 14px; }
34
+ .vx-brand {
35
+ flex: 1;
36
+ text-align: left;
37
+ background: none;
38
+ border: 0;
39
+ color: var(--vx-text);
40
+ font: 700 16px/1 system-ui, sans-serif;
41
+ letter-spacing: 0.02em;
42
+ cursor: pointer;
43
+ padding: 4px 0;
44
+ }
45
+ .vx-brand span { color: var(--vx-accent); font-weight: 500; }
46
+ .vx-icon {
47
+ width: 26px;
48
+ height: 26px;
49
+ border-radius: 6px;
50
+ border: 1px solid var(--vx-line);
51
+ background: transparent;
52
+ color: var(--vx-dim);
53
+ cursor: pointer;
54
+ font-size: 14px;
55
+ line-height: 1;
56
+ }
57
+ .vx-icon:hover { color: var(--vx-text); border-color: var(--vx-dim); }
58
+ .vx-search {
59
+ margin: 0 12px 8px;
60
+ padding: 7px 10px;
61
+ border-radius: 6px;
62
+ border: 1px solid var(--vx-line);
63
+ background: var(--vx-bg);
64
+ color: var(--vx-text);
65
+ font: inherit;
66
+ outline: none;
67
+ }
68
+ .vx-search:focus { border-color: var(--vx-accent); }
69
+ .vx-list { flex: 1; overflow-y: auto; padding: 0 6px 24px; scrollbar-width: thin; }
70
+ .vx-section h3 {
71
+ margin: 14px 8px 4px;
72
+ font-size: 11px;
73
+ font-weight: 600;
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.08em;
76
+ color: var(--vx-dim);
77
+ }
78
+ .vx-link {
79
+ display: block;
80
+ padding: 4px 8px;
81
+ border-radius: 5px;
82
+ color: var(--vx-text);
83
+ text-decoration: none;
84
+ white-space: nowrap;
85
+ overflow: hidden;
86
+ text-overflow: ellipsis;
87
+ }
88
+ .vx-link:hover { background: rgba(255, 255, 255, 0.05); }
89
+ .vx-link.vx-on { background: var(--vx-accent-dim); color: var(--vx-accent); }
90
+ .vx-content { position: relative; flex: 1; min-width: 0; overflow: hidden; }
91
+ .vx-game { position: absolute; inset: 0; }
92
+ .vx-card {
93
+ position: absolute;
94
+ top: 84px;
95
+ right: 12px;
96
+ width: 290px;
97
+ max-height: calc(100% - 300px);
98
+ min-height: 120px;
99
+ overflow-y: auto;
100
+ z-index: 30;
101
+ padding: 12px 14px;
102
+ border-radius: 10px;
103
+ border: 1px solid var(--vx-line);
104
+ background: rgba(16, 17, 20, 0.88);
105
+ backdrop-filter: blur(6px);
106
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
107
+ scrollbar-width: thin;
108
+ }
109
+ .vx-card-head { display: flex; align-items: center; gap: 8px; }
110
+ .vx-card h2 { flex: 1; margin: 0; font-size: 15px; font-weight: 650; }
111
+ .vx-card-folded > :not(.vx-card-head) { display: none; }
112
+ .vx-card-folded { min-height: 0; }
113
+ .vx-summary { margin: 6px 0 10px; color: var(--vx-dim); }
114
+ .vx-hint {
115
+ margin: 8px 0 0;
116
+ padding: 6px 8px;
117
+ min-height: 2.9em;
118
+ border-radius: 6px;
119
+ background: rgba(255, 255, 255, 0.04);
120
+ color: var(--vx-text);
121
+ font-size: 12px;
122
+ }
123
+ .vx-root :focus:not(:focus-visible) { outline: none; }
124
+ .vx-root :focus-visible { outline: 2px solid var(--vx-accent); outline-offset: 1px; }
125
+ .vx-loading {
126
+ position: absolute;
127
+ inset: 0;
128
+ display: flex;
129
+ flex-direction: column;
130
+ align-items: center;
131
+ justify-content: center;
132
+ gap: 14px;
133
+ background: var(--vx-bg);
134
+ color: var(--vx-dim);
135
+ }
136
+ .vx-loading strong { color: var(--vx-text); font-size: 15px; font-weight: 600; }
137
+ .vx-spinner {
138
+ width: 28px;
139
+ height: 28px;
140
+ border-radius: 50%;
141
+ border: 3px solid var(--vx-line);
142
+ border-top-color: var(--vx-accent);
143
+ animation: vx-spin 0.8s linear infinite;
144
+ }
145
+ @keyframes vx-spin { to { transform: rotate(360deg); } }
146
+ .vx-bar { width: 220px; height: 4px; border-radius: 2px; background: var(--vx-line); overflow: hidden; }
147
+ .vx-bar > div { height: 100%; width: 0; background: var(--vx-accent); transition: width 120ms linear; }
148
+ .vx-where { margin: 10px 0 0; font-size: 11px; color: var(--vx-dim); }
149
+ .vx-where code { color: var(--vx-text); }
150
+ .vx-controls { display: flex; flex-direction: column; gap: 6px; }
151
+ .vx-controls:empty { display: none; }
152
+ .vx-wait { color: var(--vx-dim); font-style: italic; }
153
+ .vx-control label { display: flex; align-items: center; gap: 6px; }
154
+ .vx-value { margin-left: auto; color: var(--vx-accent); font-variant-numeric: tabular-nums; }
155
+ .vx-slider input, .vx-select select { width: 100%; }
156
+ .vx-select select {
157
+ margin-top: 2px;
158
+ padding: 4px;
159
+ border-radius: 5px;
160
+ border: 1px solid var(--vx-line);
161
+ background: var(--vx-bg);
162
+ color: var(--vx-text);
163
+ }
164
+ .vx-slider input { accent-color: var(--vx-accent); }
165
+ .vx-toggle input { accent-color: var(--vx-accent); }
166
+ .vx-button {
167
+ width: 100%;
168
+ padding: 6px 10px;
169
+ border-radius: 6px;
170
+ border: 1px solid #4a3d22;
171
+ background: #2a2416;
172
+ color: #f1e4c3;
173
+ font: 600 12.5px system-ui, sans-serif;
174
+ cursor: pointer;
175
+ text-align: left;
176
+ }
177
+ .vx-button:hover { border-color: var(--vx-accent); }
178
+ .vx-button:active { transform: translateY(1px); }
179
+ .vx-index { position: absolute; inset: 0; overflow-y: auto; padding: 32px 40px 60px; }
180
+ .vx-index h1 { margin: 0 0 4px; font-size: 26px; }
181
+ .vx-index > p { margin: 0 0 22px; color: var(--vx-dim); max-width: 760px; }
182
+ .vx-index h2 { margin: 26px 0 2px; font-size: 16px; }
183
+ .vx-index h2 + p { margin: 0 0 10px; color: var(--vx-dim); }
184
+ .vx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
185
+ .vx-tile {
186
+ display: block;
187
+ padding: 12px;
188
+ border-radius: 8px;
189
+ border: 1px solid var(--vx-line);
190
+ background: var(--vx-panel);
191
+ color: inherit;
192
+ text-decoration: none;
193
+ }
194
+ .vx-tile:hover { border-color: var(--vx-accent); }
195
+ .vx-tile strong { display: block; margin-bottom: 3px; }
196
+ .vx-tile span { color: var(--vx-dim); font-size: 12px; }
197
+ @media (max-width: 760px) {
198
+ .vx-root { --vx-side: 220px; }
199
+ .vx-card { width: auto; left: 12px; top: 56px; max-height: 40%; }
200
+ .vx-index { padding: 20px 16px 40px; }
201
+ }
202
+ `
@@ -0,0 +1,50 @@
1
+ /**
2
+ * How the game looks (`GameView`): the client's, fetched as the page first plays an example. The shared units' looks
3
+ * (`src/examples/common/looks.ts`), then what each category adds over them (its `looks.ts`); an example's own look and
4
+ * controls are laid over this as its scene opens (`src/shell/scene.ts`)
5
+ */
6
+ import { type GameView, hotView, presetView } from '@v12-engine/client'
7
+ import { sharedLooks } from './examples/common/looks'
8
+ import { categoryLooks } from './examples/looks'
9
+ import { registry } from './shell/registry'
10
+
11
+ const shared: GameView = presetView({
12
+ world: {
13
+ // trees and scenery in front of the player's own units faded out
14
+ seeThrough: {},
15
+ // looks by name an example can play its map with (`ExampleDef.environment`)
16
+ environments: {
17
+ dusk: {
18
+ sky: [0.55, 0.42, 0.5],
19
+ clouds: { cover: 0.4, color: [1, 0.8, 0.7] },
20
+ light: { sunUp: 12, sunAround: 250, sunColor: [1, 0.62, 0.42], sun: 1, ambient: 0.45, ambientColor: [0.8, 0.7, 0.9] },
21
+ fog: { start: 60, end: 200 },
22
+ mist: { ground: { base: 0, top: 2, density: 0.25 } }
23
+ },
24
+ night: {
25
+ sky: [0.05, 0.07, 0.14],
26
+ light: { sunUp: 35, sunAround: 200, sunColor: [0.55, 0.65, 1], sun: 0.35, ambient: 0.25, ambientColor: [0.35, 0.4, 0.7] },
27
+ fog: { start: 40, end: 160 }
28
+ },
29
+ storm: {
30
+ sky: [0.3, 0.33, 0.38],
31
+ clouds: { cover: 0.9, color: [0.55, 0.57, 0.6] },
32
+ cloudLayer: { height: 18, cover: 0.7, opacity: 0.55, shadows: 0.5 },
33
+ light: { sun: 0.7, ambient: 0.5 },
34
+ wind: { around: 70, strength: 0.8, gusts: 0.6 },
35
+ weather: { kind: 'rain', amount: 0.9, splashes: 0.6 },
36
+ fog: { start: 40, end: 140 }
37
+ }
38
+ }
39
+ },
40
+ units: { looks: sharedLooks }
41
+ })
42
+
43
+ /** The look every example starts from: the shared one, with each category's laid over it in the list's order */
44
+ export const view: GameView = categoryLooks.reduce((made, each) => each.view?.(made) ?? made, shared)
45
+
46
+ // each example's own look and controls, by its id, where its scene finds them as it opens
47
+ registry.looks = Object.assign({}, ...categoryLooks.map((each) => each.looks ?? {}))
48
+
49
+ // saved while it runs (npm run dev): the game and the editor open take its new look, with no new page
50
+ if (import.meta.hot) import.meta.hot.accept((module) => hotView(import.meta.hot, 'examples', view, module?.view))
@@ -22,12 +22,16 @@ A real-time strategy game made with V12, whole: melee. Two races, the humans and
22
22
  <!-- @end -->
23
23
  | Check the types | `npm run typecheck` |
24
24
  | Build the page to host | `npm run build`, into `dist/` |
25
+ <!-- @if desktop -->
26
+ | Play it as an app | `npm run app`: the page in a window of its own (Electron, fetched the first time) |
27
+ | Make the app to hand out | `npm run package`, into `out/` |
28
+ <!-- @end -->
25
29
 
26
30
  ## What is where
27
31
 
28
- - `src/melee/`: the game. `index.ts` puts the rules together (the engine's packs it turns on: stances, capture points, bushes that hide); `units.ts` the humans, `undead.ts` the Restless, `heroSpells.ts` the spells every hero brings along, `abilities.ts` and `items.ts` what they cast and carry; `blight.ts` the Restless' ground; `generator.ts` the random maps and `bay.ts` the Bay made in code; `maps.ts` what a melee map must have (a town hall for someone, and someone to play against); `view/` how it looks; `data.json`, `looks.json`, `hud.json`, `fx.json`, `floats.json`, `pings.json`, `quality.json`, `cellLayers.json` and `sounds.json` what the editor's panels write
29
- - `src/rules.ts`, `src/game.ts`, `src/maps.ts`: the game by the names the page, the server and the tests know it by. `start` in `src/rules.ts` seats the computer at every player but you
30
- - `src/maps/`: the maps, made in the editor. A new one is imported in `src/maps.ts`
32
+ - `src/melee/`: the game. `index.ts` puts the rules together (the engine's packs it turns on: stances, capture points, and the grass of `foliagePack`: tall grass that hides, dry grass that burns); `units.ts` the humans, `undead.ts` the Restless, `heroes.ts` the eight heroes (their callings, innates and abilities; the spells every hero brings two of are the engine's, `defaultHeroSpells`), `abilities.ts` and `items.ts` what they cast and carry; `blight.ts` the Restless' ground; `generator.ts` the random maps and `bay.ts` the Bay made in code; `maps.ts` what a melee map must have (a town hall for someone, and someone to play against); `view/` how it looks; `data.json`, `looks.json`, `hud.json`, `fx.json`, `floats.json`, `pings.json`, `quality.json`, `cellLayers.json` and `sounds.json` what the editor's panels write
33
+ - `src/game.ts`: the game (`createGame`) the page, the server, the editor and the tests take: melee's rules, its look fetched as the page first plays it, its maps (`src/maps.ts`). Its `onStart` seats the computer at every player but you
34
+ - `src/maps/`: the maps, made in the editor. A new one is imported in `src/maps.ts`. The Bay has a wind, tall grass on the road out of each corner and dry grass on the island, which burns
31
35
  - `src/main.ts`: the page, its scenes made by `appScenes`
32
36
 
33
37
  <!-- @if editor -->
@@ -56,6 +60,11 @@ A real-time strategy game made with V12, whole: melee. Two races, the humans and
56
60
 
57
61
  - `tests/`: the rules tested with no page
58
62
 
63
+ <!-- @end -->
64
+ <!-- @if desktop -->
65
+
66
+ - `electron/`: the shell around the page, Electron's main process: `config.mjs` (the app's name, its identifier and the updater's address, yours to settle before it is handed out), `app.mjs` (the window, its full screen, what the page may ask), `preload.cjs` (the bridge the page asks through), `files.mjs` (saves, settings and the error log as files), `updates.mjs`, `presentation.m` (the addon that hides the Dock and the menu bar on macOS), `dev.mjs` (`npm run app`) and `package.mjs` (`npm run package`). `src/shell.ts` is all the page asks of it and `src/desktop.ts` what it does with that: over the whole screen with a Windowed setting, saves and settings as files, the game saved as the window closes, an error log, Exit on the start menu, Quit on the pause menu. The updater's address (`UPDATES`) is a placeholder until the docs' A desktop game, offline says what goes there
67
+
59
68
  <!-- @end -->
60
69
 
61
70
  Every model it plays with is of the shared art, `@v12-engine/assets` (whose each is: its group's `CREDITS.md` in `node_modules/@v12-engine/assets/groups/`)