@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,152 @@
1
+ // @if tests
2
+ // @vitest-environment happy-dom
3
+ /** The page's side of the shell (`src/shell.ts`), on a pretend bridge in the place of `electron/preload.cjs`'s */
4
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
+ import {
6
+ autosaveOnClose,
7
+ canQuit,
8
+ checkForUpdate,
9
+ createErrorLog,
10
+ fileStore,
11
+ gameFullscreen,
12
+ inShell,
13
+ quit,
14
+ setWindowed,
15
+ watchWindowed
16
+ } from '../src/shell'
17
+
18
+ function pretendBridge() {
19
+ const files = new Map<string, string>()
20
+ const calls: unknown[] = []
21
+ const listeners = { resized: [] as (() => void)[], close: [] as (() => void)[] }
22
+ let over = false
23
+ let update: { version: string; notes?: string } | undefined
24
+ const bridge = {
25
+ fullscreen: async (on?: boolean) => {
26
+ calls.push(['fullscreen', on])
27
+ if (on !== undefined) over = on
28
+ return over
29
+ },
30
+ close: async () => void calls.push(['close']),
31
+ destroy: async () => void calls.push(['destroy']),
32
+ onResized: (listener: () => void) => (listeners.resized.push(listener), () => (listeners.resized.length = 0)),
33
+ onCloseRequested: (listener: () => void) => (listeners.close.push(listener), () => (listeners.close.length = 0)),
34
+ openFolder: async (folder: string) => void calls.push(['openFolder', folder]),
35
+ records: {
36
+ list: async (folder: string) => [...files].filter(([key]) => key.startsWith(`${folder}/`)).map(([, text]) => text),
37
+ read: async (folder: string, id: string) => files.get(`${folder}/${id}`),
38
+ write: async (folder: string, id: string, text: string) => void files.set(`${folder}/${id}`, text),
39
+ remove: async (folder: string, id: string) => void files.delete(`${folder}/${id}`)
40
+ },
41
+ log: { append: async () => undefined, size: async () => 0, rename: async () => undefined },
42
+ updates: {
43
+ check: async () => (calls.push(['check']), update),
44
+ install: async () => void calls.push(['install'])
45
+ }
46
+ }
47
+ ;(window as any).v12Shell = bridge
48
+ return { files, calls, listeners, found: (version: typeof update) => (update = version) }
49
+ }
50
+
51
+ afterEach(() => {
52
+ delete (window as any).v12Shell
53
+ })
54
+
55
+ describe('in a browser', () => {
56
+ it('is nothing of the shell', async () => {
57
+ expect(inShell()).toBe(false)
58
+ expect(canQuit()).toBe(false)
59
+ expect(await gameFullscreen(true)).toBe(false)
60
+ await quit()
61
+ expect(await checkForUpdate()).toBeUndefined()
62
+ })
63
+ })
64
+
65
+ describe('in the shell', () => {
66
+ it('asks it for the screen, and is told which it is', async () => {
67
+ const { calls } = pretendBridge()
68
+ expect(inShell()).toBe(true)
69
+ expect(await gameFullscreen(true)).toBe(true)
70
+ expect(await gameFullscreen()).toBe(true)
71
+ await setWindowed(true)
72
+ expect(calls).toEqual([
73
+ ['fullscreen', true],
74
+ ['fullscreen', undefined],
75
+ ['fullscreen', false]
76
+ ])
77
+ })
78
+
79
+ it('keeps records as JSON, and leaves out one that does not read', async () => {
80
+ const { files } = pretendBridge()
81
+ const store = fileStore<{ id: string; n: number }>('saves')
82
+ await store.write({ id: 'a', n: 1 })
83
+ files.set('saves/broken', '{ cut sho')
84
+ expect(await store.read('a')).toEqual({ id: 'a', n: 1 })
85
+ expect(await store.read('broken')).toBeUndefined()
86
+ expect(await store.list()).toEqual([{ id: 'a', n: 1 }])
87
+ await store.remove('a')
88
+ expect(await store.read('a')).toBeUndefined()
89
+ })
90
+
91
+ it('saves as the window closes and only then lets it close, and not for longer than the limit', async () => {
92
+ const { calls, listeners } = pretendBridge()
93
+ let saved = false
94
+ const client = {
95
+ saves: {
96
+ canSave: () => true,
97
+ refresh: async () => undefined,
98
+ list: () => [],
99
+ save: async () => ((saved = true), { id: 's', name: 'Autosave' }),
100
+ remove: async () => undefined
101
+ }
102
+ }
103
+ await autosaveOnClose(() => client as never)
104
+ await listeners.close[0]!()
105
+ expect(saved).toBe(true)
106
+ expect(calls).toEqual([['destroy']])
107
+
108
+ const hung = pretendBridge()
109
+ vi.useFakeTimers()
110
+ await autosaveOnClose(() => ({ saves: { ...client.saves, save: () => new Promise(() => undefined) } }) as never, { limit: 50 })
111
+ const closing = hung.listeners.close[0]!() as unknown as Promise<void>
112
+ await vi.advanceTimersByTimeAsync(60)
113
+ await closing
114
+ vi.useRealTimers()
115
+ expect(hung.calls).toEqual([['destroy']])
116
+ })
117
+
118
+ it('keeps the Windowed setting to the window', async () => {
119
+ pretendBridge()
120
+ const values = new Map<string, boolean>([['windowed', true]])
121
+ const client = { settings: { get: (id: string) => values.get(id), set: (id: string, on: boolean) => values.set(id, on) } }
122
+ let heard = () => undefined as unknown
123
+ await watchWindowed(client as never, { onResized: async (handler) => ((heard = handler), () => undefined) }, async () => true)
124
+ await new Promise((resolve) => setTimeout(resolve, 0))
125
+ vi.useFakeTimers({ now: Date.now() + 5000 })
126
+ await heard()
127
+ vi.useRealTimers()
128
+ expect(values.get('windowed')).toBe(false)
129
+ })
130
+
131
+ it('writes the error log in order, and a failing log does not throw', async () => {
132
+ const lines: string[] = []
133
+ const log = createErrorLog(
134
+ { append: async (_file, text) => void lines.push(text), size: async () => 0, rename: async () => undefined },
135
+ { now: () => new Date(0) }
136
+ )
137
+ await log.write('here', 'it broke')
138
+ expect(lines).toEqual(['1970-01-01T00:00:00.000Z [here] it broke\n'])
139
+ await createErrorLog({ append: () => Promise.reject(new Error('no')), size: async () => 0, rename: async () => undefined }).write('x', 'y')
140
+ })
141
+
142
+ it('offers a version the shell found, and installs it by the shell', async () => {
143
+ const { calls, found } = pretendBridge()
144
+ expect(await checkForUpdate()).toBeUndefined()
145
+ found({ version: '1.2.0', notes: 'What is new' })
146
+ const update = await checkForUpdate()
147
+ expect(update).toMatchObject({ version: '1.2.0', notes: 'What is new' })
148
+ await update!.install()
149
+ expect(calls).toEqual([['check'], ['check'], ['install']])
150
+ })
151
+ })
152
+ // @end
@@ -7,11 +7,17 @@ import { GameRooms, generatedMode, mapMode, type GameMode, type RoomKind } from
7
7
  // @if ratings
8
8
  import { createRatings } from '@v12-engine/engine/server'
9
9
  // @end
10
- import { DEFAULT_MAP, mapNamed, maps } from './src/maps'
11
- import { rules, start } from './src/rules'
10
+ import { game } from './src/game'
12
11
 
13
- /** A game on one of the game's maps, each world started as the page starts it */
14
- const onMap = (name: string): GameMode => mapMode(rules, mapNamed(name), { start })
12
+ /** The game's rules: a server reads the game's (`src/game.ts`), never its look */
13
+ const rules = game.toRules()
14
+ /** The game's maps by name, each read */
15
+ const maps = Object.fromEntries(await Promise.all(Object.keys(game.toMaps()).map(async (name) => [name, await game.loadMap(name)] as const)))
16
+ /** The first: the room to try the game alone in plays it */
17
+ const DEFAULT_MAP = Object.keys(maps)[0]
18
+
19
+ /** A game on one of the game's maps, each world started as the page starts it (`game.onStart`) */
20
+ const onMap = (name: string): GameMode => mapMode(game, maps[name])
15
21
 
16
22
  /**
17
23
  * A game on a map the game's generator makes (`rules.generator`): another for each game made, of 128 cells a side for
@@ -22,13 +28,12 @@ const random: Record<string, RoomKind> = rules.generator
22
28
  random: {
23
29
  name: 'A random map',
24
30
  room: (settings) => ({
25
- mode: generatedMode(rules, rules.generator!, {
31
+ mode: generatedMode(game, rules.generator!, {
26
32
  width: 128,
27
33
  depth: 128,
28
34
  players: 2,
29
35
  symmetry: 'half',
30
- ...(Number(settings.seed) > 0 && { seed: Number(settings.seed) }),
31
- start
36
+ ...(Number(settings.seed) > 0 && { seed: Number(settings.seed) })
32
37
  })
33
38
  })
34
39
  }
@@ -42,9 +47,7 @@ const ratings = createRatings({ file: 'ratings.json' })
42
47
  await ratings.ready
43
48
  // @end
44
49
  // the seats the computer plays in the room to try the game alone in: every player of the first map but the first
45
- const computer = mapNamed()
46
- .players.slice(1)
47
- .map((player) => player.id)
50
+ const computer = maps[DEFAULT_MAP].players.slice(1).map((player) => player.id)
48
51
  const rooms = await GameRooms.start({
49
52
  port: Number(process.env.PORT ?? 8787),
50
53
  rooms: {
@@ -7,10 +7,9 @@ import { startServer } from '@v12-engine/engine/server'
7
7
  // @if ratings
8
8
  import { createRatings } from '@v12-engine/engine/server'
9
9
  // @end
10
- import { mapNamed } from './src/maps'
11
- import { rules, start } from './src/rules'
10
+ import { game } from './src/game'
12
11
 
13
- const map = mapNamed()
12
+ const map = await game.loadMap()
14
13
  // @if ratings
15
14
  // a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
16
15
  const ratings = createRatings({ file: 'ratings.json' })
@@ -18,9 +17,9 @@ const ratings = createRatings({ file: 'ratings.json' })
18
17
  await ratings.ready
19
18
  // @end
20
19
  const server = await startServer({
21
- game: rules,
20
+ // the game: its rules and its start, never its look (`src/game.ts`), on its first map
21
+ game,
22
22
  map,
23
- start,
24
23
  // the seats the computer plays: leave it out for a game of people alone, which starts once every seat is taken
25
24
  ai: map.players.slice(1).map((player) => player.id),
26
25
  // @if ratings
@@ -34,11 +34,31 @@ export default defineConfig({
34
34
  assetsPackage(),
35
35
  // @end
36
36
  // @if editor
37
- editorFiles({ gameFolder: () => 'src' })
37
+ editorFiles({
38
+ gameFolder: () => 'src',
39
+ // where New with code in the editor may write a stub (never over a file)
40
+ scaffold: ['src'],
41
+ // the scripts the editor's Run panel may start and stop while developing, and no others
42
+ run: [
43
+ // @if server
44
+ 'server',
45
+ // @end
46
+ // @if desktop
47
+ // (`app` starts the dev server itself, which the editor is open on: this one uses that one)
48
+ 'app:editor',
49
+ // @end
50
+ ]
51
+ })
38
52
  // @end
39
53
  ],
40
54
  // the packages' source on the page, on the dev server's own side and in the tests (the library's `config/vite.ts`)
41
55
  ...sourceLibrary(),
56
+ // @if desktop
57
+ // the app made to hand out is not the page's to watch
58
+ server: { watch: { ignored: ['**/out/**'] } },
59
+ // the shell's own Chromium (Electron's) is the one browser the built page runs in
60
+ build: { target: 'esnext' },
61
+ // @end
42
62
  // @if tests
43
63
  test: { environment: 'node' }
44
64
  // @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v12-engine/create",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,12 +22,16 @@ A campaign made with V12: three missions on the long road home, a hero carried f
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
32
  - `src/campaign/`: the game. `index.ts` puts the rules together and gives each map its mission; `ids.ts` names everything once; `units.ts` the units, buildings, the hero, his abilities and the items; `missions/` a file a mission: `homecoming.ts` (objectives, a prefab, areas, a brain that holds and raids), `siege.ts` (the waves, the gate, effects fired from the rules), `warlord.ts` (the hero carried in, a shop, the talk at the gate and its choice, the prisoners); `waves.ts` the waves, as `waves.json` says them; `view/` how it looks and sounds, with the cinematic of the talk in code; `cinematics/` the intro and the outro, made in the editor's Cinematic panel; `prefabs/` what the editor's Save as a prefab wrote; `campaign.json` the campaign's chapters and missions (their maps, words, briefings, debriefs), what the editor's Campaign panel writes, and `scenes.ts` what of them is code, for the page (`defineCampaign`); `data.json`, `looks.json`, `fx.json` and `waves.json` what the editor's Data, model, Effects and Waves 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
30
- - `src/maps/`: the missions' maps, made in the editor. Each has the names its mission's rules look for (`hall`, `hero`, `camp`, `warcamp`, `gate`, `cells`…): a names file beside each says which
33
+ - `src/game.ts`: the game (`createGame`) the page, the server, the editor, the tests and the campaign's scenes take: the campaign's rules, its look fetched as the page first plays it, its maps (`src/maps.ts`), and the mission of a map given as a game of it starts
34
+ - `src/maps/`: the missions' maps, made in the editor, each with a meadow (the rules' `grass` layer, `foliagePack`: a meadow that burns and grows back, and on the homestead tall grass that hides). Each has the names its mission's rules look for (`hall`, `hero`, `camp`, `warcamp`, `gate`, `cells`…): a names file beside each says which
31
35
  - `src/main.ts`: the page: the start screen, the campaign's menu and its missions, free play, the settings
32
36
 
33
37
  <!-- @if editor -->
@@ -48,6 +52,9 @@ A campaign made with V12: three missions on the long road home, a hero carried f
48
52
 
49
53
  - `tests/`: the missions tested with no page: the maps checked, each mission won and lost as it says, the waves, the carried hero, the choice at the gate, the computer playing a minute (`simulate`)
50
54
  <!-- @end -->
55
+ <!-- @if desktop -->
56
+ - `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, whose example is this campaign in such a shell
57
+ <!-- @end -->
51
58
  - `public/games/campaign/`: the game's own folder of files: what the editor's Assets panel takes in (models, sounds, pictures), with an `assets.json` saying what is there
52
59
  - `@v12-engine/assets`: the shared art every unit and building is drawn with (adventurers with their clips and their gear, buildings, trees), served where `public/`'s files are by `assetsPackage()` in `vite.config.ts`, and copied into a build as the game uses it. Whose each file is: its group's `CREDITS.md` in `node_modules/@v12-engine/assets/groups/`
53
60
 
@@ -7,11 +7,11 @@ import { GameRooms, mapMode, type GameMode } from '@v12-engine/engine/server'
7
7
  // @if ratings
8
8
  import { createRatings } from '@v12-engine/engine/server'
9
9
  // @end
10
+ import { game, PLAYER } from './src/game'
10
11
  import { mapNamed, maps } from './src/maps'
11
- import { PLAYER, rules, start } from './src/rules'
12
12
 
13
- /** A game on one of the missions' maps: the one seat is Aldric's, the Red Hand the mission's own computer, which `start` seats */
14
- const onMap = (name: string): GameMode => mapMode(rules, mapNamed(name), { start, seats: [{ player: PLAYER, name: 'Aldric' }], ai: null })
13
+ /** A game on one of the missions' maps: the one seat is Aldric's, the Red Hand the mission's own computer, which its start seats */
14
+ const onMap = (name: string): GameMode => mapMode(game, mapNamed(name), { seats: [{ player: PLAYER, name: 'Aldric' }], ai: null })
15
15
 
16
16
  // @if ratings
17
17
  // a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
@@ -6,8 +6,7 @@ import { startServer } from '@v12-engine/engine/server'
6
6
  // @if ratings
7
7
  import { createRatings } from '@v12-engine/engine/server'
8
8
  // @end
9
- import { mapNamed } from './src/maps'
10
- import { PLAYER, rules, start } from './src/rules'
9
+ import { game, PLAYER } from './src/game'
11
10
 
12
11
  // @if ratings
13
12
  // a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
@@ -16,9 +15,8 @@ const ratings = createRatings({ file: 'ratings.json' })
16
15
  await ratings.ready
17
16
  // @end
18
17
  const server = await startServer({
19
- game: rules,
20
- map: mapNamed(),
21
- start,
18
+ // the game: its rules and its start (the mission of its map), never its look (`src/game.ts`), on its first map
19
+ game,
22
20
  // the one seat is Aldric's: the Red Hand is the mission's own computer, which `start` seats
23
21
  seats: [{ player: PLAYER, name: 'Aldric' }],
24
22
  computer: null,
@@ -14,7 +14,8 @@ import {
14
14
  newCampaignProgress,
15
15
  readCampaignProgress,
16
16
  type World,
17
- heroOf
17
+ heroOf,
18
+ heroCallings
18
19
  } from '@v12-engine/engine'
19
20
  import { checkDeterminism, order, run, simulate, testWorld } from '@v12-engine/engine/testing'
20
21
  import { describe, expect, it } from 'vitest'
@@ -23,16 +24,19 @@ import { kills } from '../src/campaign/kills'
23
24
  import { builtWaves, FIRST_WAIT, readWaves, siegeWaves, wavesOf } from '../src/campaign/waves'
24
25
  import { warlord } from '../src/campaign/missions/warlord'
25
26
  import { mapNamed, maps } from '../src/maps'
26
- import { ENEMY, PLAYER, rules, start, UNIT } from '../src/rules'
27
+ import { ENEMY, game, PLAYER, UNIT } from '../src/game'
27
28
  import wavesFile from '../src/campaign/waves.json'
28
29
 
30
+ const rules = game.toRules()
29
31
  const registry = createRegistry(rules)
32
+ /** What the game does as a world of it starts: the mission of its map (`game.onStart`), at once */
33
+ const onStart = game.toStart()!
30
34
 
31
- /** A mission's world, started as the page starts it */
35
+ /** A mission's world, started as the page starts it (`game.onStart`: the mission of its map) */
32
36
  async function mission(name: string): Promise<World> {
33
37
  const map = mapNamed(name)
34
- const world = await createWorldFromMap({ map, game: rules, seed: 1 })
35
- start(world, { map })
38
+ const world = await createWorldFromMap({ map, rules, seed: 1 })
39
+ await onStart(world, { map })
36
40
  return world
37
41
  }
38
42
 
@@ -95,7 +99,7 @@ describe('mission one, Homecoming', () => {
95
99
  it('plays, the Red Hand’s computer thinking, with no error for a minute (simulate)', async () => {
96
100
  const map = mapNamed('homestead')
97
101
  // the mission seats the Red Hand's computer itself (`start`): simulate is told of none
98
- const [game] = await simulate({ game: rules, map, computers: [], seconds: 60, start: (world) => start(world, { map }) }, { seeds: [1] })
102
+ const [game] = await simulate({ rules: rules, map, computers: [], seconds: 60, onStart: (world) => void onStart(world, { map }) }, { seeds: [1] })
99
103
  expect(game.failed).toBeUndefined()
100
104
  expect(game.ticks).toBeGreaterThan(0)
101
105
  })
@@ -169,7 +173,7 @@ describe('what goes on to the next mission', () => {
169
173
  const footmen = standing(home, PLAYER, UNIT.footman)
170
174
  const more = [home.spawn(UNIT.footman, PLAYER, { x: 20, z: 20 }), home.spawn(UNIT.footman, PLAYER, { x: 22, z: 20 })]
171
175
  const hurt = more[0]
172
- hurt.hp = 10
176
+ hurt.health = 10
173
177
  home.objectives.complete(PLAYER, 'campaign.caravan')
174
178
  const carried = homecomingCarry(home)
175
179
  expect(footmen.length + more.length).toBe(4)
@@ -182,8 +186,8 @@ describe('what goes on to the next mission', () => {
182
186
  home.dispose()
183
187
 
184
188
  const map = mapNamed('fort')
185
- const fort = await createWorldFromMap({ map, game: rules, seed: 1 })
186
- start(fort, { map })
189
+ const fort = await createWorldFromMap({ map, rules, seed: 1 })
190
+ onStart(fort, { map })
187
191
  const before = { footmen: standing(fort, PLAYER, UNIT.footman).length, gold: fort.player(PLAYER).resources.gold ?? 0 }
188
192
  siegeCarriedIn(fort, map, kept.data as Partial<RoadData>)
189
193
  expect(standing(fort, PLAYER, UNIT.footman)).toHaveLength(before.footmen + 3)
@@ -206,9 +210,9 @@ describe('mission three, The Warlord', () => {
206
210
 
207
211
  it('brings the carried hero in where the map has one, with what he earned', async () => {
208
212
  const map = mapNamed('pass')
209
- const world = await createWorldFromMap({ map, game: rules, seed: 1 })
213
+ const world = await createWorldFromMap({ map, rules, seed: 1 })
210
214
  const [hero] = carryIn(world, [seasoned], PLAYER, map.players[0].start!)
211
- start(world, { map })
215
+ onStart(world, { map })
212
216
  expect(heroOf(hero)?.level).toBe(3)
213
217
  expect(hero.abilities.find((ability) => ability.id === 'holyLight')?.level).toBe(2)
214
218
  expect(standing(world, PLAYER, UNIT.knight)).toHaveLength(1)
@@ -218,7 +222,7 @@ describe('mission three, The Warlord', () => {
218
222
  it('is harder on hard and easier on easy, its computer seated on the campaign’s difficulty', async () => {
219
223
  const on = async (difficulty: string) => {
220
224
  const map = mapNamed('pass')
221
- const world = await createWorldFromMap({ map, game: rules, seed: 1 })
225
+ const world = await createWorldFromMap({ map, rules, seed: 1 })
222
226
  // what the campaign's scene does before a mission seats its computers
223
227
  world.computers.defaults = { difficulty }
224
228
  warlord(world, true, difficulty)
@@ -229,7 +233,7 @@ describe('mission three, The Warlord', () => {
229
233
  expect((hard.computers.get(ENEMY)?.computer as { profile?: { difficulty?: string } }).profile?.difficulty).toBe('hard')
230
234
  const easy = await on('easy')
231
235
  const varga = easy.named('warlord')!
232
- expect(varga.hp).toBe(Math.round(varga.maxHp * 0.75))
236
+ expect(varga.health).toBe(Math.round(varga.maxHealth * 0.75))
233
237
  expect(easy.named('guard4')).toBeUndefined()
234
238
  hard.dispose()
235
239
  easy.dispose()
@@ -271,15 +275,30 @@ describe('mission three, The Warlord', () => {
271
275
  })
272
276
 
273
277
  describe('the hero', () => {
278
+ it('is a Vanguard and a Keeper, of the engine’s callings', () => {
279
+ expect(heroCallings(registry, registry.unit(UNIT.knight)).map(({ calling, strength }) => [calling.name, strength])).toEqual([
280
+ ['Vanguard', 3],
281
+ ['Keeper', 2]
282
+ ])
283
+ })
284
+
285
+ it('brings two of the engine’s hero spells, Barrier and Purify, and the shop names his items’ tags', () => {
286
+ expect(registry.unit(UNIT.knight).hero!.spells).toEqual(['barrier', 'purify'])
287
+ expect(['barrier', 'purify'].map((id) => registry.ability(id).name)).toEqual(['Barrier', 'Purify'])
288
+ // of the set, only what he brings
289
+ expect(rules.abilities!.some((ability) => ability.id === 'flash')).toBe(false)
290
+ expect(rules.tags!.map((tag) => tag.id).sort()).toEqual(['attack', 'consumable', 'health', 'movement'])
291
+ })
292
+
274
293
  it('heals a friend with Holy Light once he has learned it', async () => {
275
294
  const world = await testWorld(rules)
276
295
  const hero = world.spawn(UNIT.knight, PLAYER, { x: 20, z: 20 })
277
296
  const footman = world.spawn(UNIT.footman, PLAYER, { x: 22, z: 20 })
278
297
  learn(world, hero, 'holyLight')
279
- footman.hp = 100
298
+ footman.health = 100
280
299
  order(world, [hero], { type: 'cast', ability: 'holyLight', target: footman.id })
281
- run(world, 3, () => footman.hp > 200)
282
- expect(footman.hp).toBeGreaterThan(200)
300
+ run(world, 3, () => footman.health > 200)
301
+ expect(footman.health).toBeGreaterThan(200)
283
302
  world.dispose()
284
303
  })
285
304
 
@@ -289,13 +308,13 @@ describe('the hero', () => {
289
308
  // something of the enemy's that does not hit back: what the strike took is then the strike's alone
290
309
  const captive = world.spawn(UNIT.prisoner, ENEMY, { x: 23, z: 20 })
291
310
  learn(world, hero, 'judgement')
292
- hero.hp = 300
311
+ hero.health = 300
293
312
  // a tick first: an enemy is a target once it is seen, and the fog is worked out as the game steps
294
313
  run(world, 0.1)
295
314
  order(world, [hero], { type: 'cast', ability: 'judgement', target: captive.id })
296
- run(world, 3, () => captive.hp <= captive.maxHp - 120)
297
- expect(captive.maxHp - captive.hp).toBeGreaterThanOrEqual(120)
298
- expect(hero.hp).toBeGreaterThanOrEqual(300 + 120 - 1)
315
+ run(world, 3, () => captive.health <= captive.maxHealth - 120)
316
+ expect(captive.maxHealth - captive.health).toBeGreaterThanOrEqual(120)
317
+ expect(hero.health).toBeGreaterThanOrEqual(300 + 120 - 1)
299
318
  world.dispose()
300
319
  })
301
320
  })
@@ -312,7 +331,7 @@ describe('each mission', () => {
312
331
  it.each(Object.keys(maps))(
313
332
  '%s comes out the same, played twice and on from a save',
314
333
  async (name) => {
315
- const report = await checkDeterminism(() => mission(name), { ticks: 600, from: 150, game: rules })
334
+ const report = await checkDeterminism(() => mission(name), { ticks: 600, from: 150, rules })
316
335
  expect(report).toEqual({ twice: null, fromSave: null })
317
336
  },
318
337
  120_000
@@ -1,6 +1,27 @@
1
- import { defineGame } from '@v12-engine/client'
2
- import { campaign } from './index'
3
- import { campaignView } from './view'
1
+ import { createGame } from '@v12-engine/engine'
2
+ import { campaign, startMission } from './index'
3
+ import { maps } from './maps'
4
4
 
5
- /** The campaign's game, whole: its rules and how it looks. What a page launches, and its editor makes maps for */
6
- export const campaignGame = defineGame({ rules: campaign, view: campaignView })
5
+ /**
6
+ * The campaign's game, whole (`createGame`): its rules, its missions' maps, the mission of a map given as a game of it
7
+ * starts, and its look, fetched as a page first plays it (`./view`, and the client that draws it: not part of a start
8
+ * screen's code, nor of what a server or a test reads). What a page launches, its editor makes maps for and its
9
+ * campaign's scenes play (`scenes.ts`)
10
+ */
11
+ export const campaignGame = createGame(campaign).maps(maps)
12
+
13
+ /**
14
+ * What the game does once a world is made from a map, before anyone plays: the map's mission is given (each map is
15
+ * a mission's; a map of none plays on with no objectives). The page's free play, the server, the tests and the
16
+ * editor's Play here each run it with the map; the campaign's own scene gives the mission itself, knowing whether
17
+ * the game is new or loaded from a save
18
+ */
19
+ campaignGame.onStart((world, { map }) => startMission(world, map))
20
+
21
+ // @if !desktop
22
+ campaignGame.view(() => import('./view').then((module) => module.campaignView))
23
+ // @end
24
+ // @if desktop
25
+ // its look with what the desktop app adds to it: a Windowed setting, Quit on the pause menu
26
+ campaignGame.view(() => Promise.all([import('./view'), import('../desktop')]).then(([{ campaignView }, { desktopView }]) => desktopView(campaignView)))
27
+ // @end