@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
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @v12-engine/create
2
2
 
3
+ > **Alpha.** V12 is in active development: APIs, file formats and saves can change between releases. Pin an exact version for anything you ship. Docs: https://v12-docs.vercel.app
4
+
3
5
  A project to start a game from, made with V12: its rules, its page, and the parts you choose.
4
6
 
5
7
  ```sh
@@ -13,12 +15,13 @@ It asks where to make it, which starter, what the game is called, which parts it
13
15
  | `empty` | Hello world: a flat field and a scout to walk it. The default. |
14
16
  | `melee` | A whole RTS: two races and their heroes, bases and armies, against the computer. |
15
17
  | `campaign` | A campaign of three missions with a hero carried between them, drawn with the shared models. |
18
+ | `examples` | What the engine can do, a small scene each, browsed from a list as three.js's examples are. |
16
19
 
17
20
  ```sh
18
21
  npm create @v12-engine arena -- --yes --starter melee --server rooms --ratings
19
22
  ```
20
23
 
21
- The parts: the map editor (`--no-editor`), a server of one game, of many games in rooms, or none (`--server single|rooms|none`), ratings (`--ratings`), a test of the rules (`--no-tests`), a git repository (`--no-git`), and installing at once (`--install`).
24
+ The parts: the map editor (`--no-editor`), a server of one game, of many games in rooms, or none (`--server single|rooms|none`), ratings (`--ratings`), a test of the rules (`--no-tests`), the shared art (`--no-assets`), a desktop app in an Electron shell (`--desktop`: `npm run app` opens it, `npm run package` makes the app to hand out), a git repository (`--no-git`), and installing at once (`--install`).
22
25
 
23
26
  In code: `createProject(folder, options)` from `@v12-engine/create`.
24
27
 
package/base/_gitignore CHANGED
@@ -4,6 +4,11 @@ dist
4
4
  .DS_Store
5
5
  # the replays Play here keeps with a map: the designer's own, not the map's
6
6
  **/*.files/replays/
7
+ # @if desktop
8
+ # the app made to hand out (`npm run package`), and the shell's addon, compiled from its source on macOS
9
+ out
10
+ electron/presentation.node
11
+ # @end
7
12
  # @if ratings
8
13
  # what the server keeps of who played: not code
9
14
  ratings.json
package/base/package.json CHANGED
@@ -11,9 +11,9 @@
11
11
  "dependencies": {
12
12
  "@v12-engine/engine": "^0.0.0",
13
13
  "@v12-engine/client": "^0.0.0",
14
- "@babylonjs/core": "^9.31.0",
15
- "@babylonjs/gui": "^9.31.0",
16
- "@babylonjs/loaders": "^9.31.0"
14
+ "@babylonjs/core": "~9.31.0",
15
+ "@babylonjs/gui": "~9.31.0",
16
+ "@babylonjs/loaders": "~9.31.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^26.6.5",
package/base/src/main.ts CHANGED
@@ -13,24 +13,32 @@
13
13
  * the top bar, Alt+Shift+D) and its cheats are in the game while developing (`npm run dev`), not in a built page:
14
14
  * `debug` says otherwise
15
15
  */
16
+ // @if !desktop
16
17
  import { appScenes, sceneAsked, startApp } from '@v12-engine/client/app'
18
+ // @end
19
+ // @if desktop
20
+ import { appScenes, exitButton, sceneAsked, startApp } from '@v12-engine/client/app'
21
+ // @end
17
22
  import '@v12-engine/client/styles.css'
18
23
  // the theme's typefaces come with the client: the page's own font stays what it was
19
24
  import '@v12-engine/client/fonts.css'
20
25
  // @if editor
21
26
  import { editor } from './editor'
22
27
  // @end
23
- import { DEFAULT_MAP, maps } from './maps'
24
- import { rules, start } from './rules'
28
+ import { game } from './game'
29
+ // @if desktop
30
+ // what the desktop app adds: its shell's stores, the game saved as the window closes, an error log, updates, Exit
31
+ import { desktop } from './desktop'
32
+ // @end
25
33
 
34
+ // @if desktop
35
+ const shell = desktop()
36
+
37
+ // @end
26
38
  const scenes = appScenes({
27
- // the game's code (its looks, and the client that draws them) fetched as a scene first plays it: the start screen
28
- // shows at once, its rules telling it what to say of the game
29
- game: () => import('./game').then((module) => module.game),
30
- rules,
31
- maps,
32
- map: DEFAULT_MAP,
33
- start,
39
+ // the game (`src/game.ts`): its rules say what the start screen shows, its maps and its start are the scenes', and its
40
+ // look (and the client that draws it) is fetched as a scene first plays it, so that the start screen shows at once
41
+ game,
34
42
  // @if editor
35
43
  editor,
36
44
  // @end
@@ -44,7 +52,17 @@ const scenes = appScenes({
44
52
  // the random map's preview made in a worker of the page's (`src/generate.worker.ts`): the menu goes on while it is made
45
53
  generatorWorker: () => new Worker(new URL('./generate.worker.ts', import.meta.url), { type: 'module' }),
46
54
  // @end
55
+ // @if desktop
56
+ // the game saved from its pause menu, as files, and as the window closes
57
+ saves: {},
58
+ // @end
59
+ // @if !desktop
47
60
  menu: { description: 'A game made with V12.' },
61
+ // @end
62
+ // @if desktop
63
+ // Exit, last on the start screen: in the shell alone, as a page cannot close its tab
64
+ menu: { description: 'A game made with V12.', buttons: [exitButton(shell)] },
65
+ // @end
48
66
  // the Credits on the start screen (`creditsScene`): who made the game, by parts, scrolling
49
67
  credits: {
50
68
  sections: [
@@ -56,6 +74,14 @@ const scenes = appScenes({
56
74
 
57
75
  /** The ground's textures made in a worker of the page's (`src/ground.worker.ts`), made at the first texture: the page goes on while they are made */
58
76
  const groundWorker = () => new Worker(new URL('./ground.worker.ts', import.meta.url), { type: 'module' })
77
+ /** The navigations of its worlds built in another (`src/navigation.worker.ts`), made at the first build */
78
+ const navigationWorker = () => new Worker(new URL('./navigation.worker.ts', import.meta.url), { type: 'module' })
59
79
 
60
- const app = startApp({ target: '#game', title: rules.name, scenes, start: sceneAsked(scenes, 'home'), groundWorker })
80
+ // @if !desktop
81
+ const app = startApp({ target: '#game', game, scenes, start: sceneAsked(scenes, 'home'), groundWorker, navigationWorker })
82
+ // @end
83
+ // @if desktop
84
+ const app = startApp({ target: '#game', game, scenes, start: sceneAsked(scenes, 'home'), groundWorker, navigationWorker, ...shell.options })
85
+ shell.started(app)
86
+ // @end
61
87
  Object.assign(window, { app })
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The page's worker that builds the navigations of its worlds, so the page goes on while a large map is made. Given to the
3
+ * app in `main.ts` (`navigationWorker`)
4
+ */
5
+ import { serveNavigationBuilds } from '@v12-engine/engine'
6
+
7
+ serveNavigationBuilds()
@@ -1,4 +1,4 @@
1
- // @if editor|assets
1
+ // @if editor|assets|desktop
2
2
  import { defineConfig } from 'vite'
3
3
  // @if assets
4
4
  import { assetsPackage } from '@v12-engine/client/vite'
@@ -8,6 +8,13 @@ import { editorFiles } from '@v12-engine/editor/vite'
8
8
  // @end
9
9
 
10
10
  export default defineConfig({
11
+ // @if desktop
12
+ // the app made to hand out is not the page's to watch
13
+ server: { watch: { ignored: ['**/out/**'] } },
14
+ // the shell's own Chromium (Electron's) is the one browser the built page runs in
15
+ build: { target: 'esnext' },
16
+ // @end
17
+ // @if editor|assets
11
18
  plugins: [
12
19
  // @if assets
13
20
  // the shared art (@v12-engine/assets) served where the game's shared files are (/models/…), behind public/: a file
@@ -18,8 +25,23 @@ export default defineConfig({
18
25
  // @if editor
19
26
  // the map editor keeps the game's maps in the project through the dev server: Save writes them into `src/maps`.
20
27
  // Only in `npm run dev`; a built page keeps maps in the browser
21
- editorFiles({ gameFolder: () => 'src' }),
28
+ editorFiles({
29
+ gameFolder: () => 'src',
30
+ // where New with code in the editor may write a stub (never over a file)
31
+ scaffold: ['src'],
32
+ // the scripts the editor's Run panel may start and stop while developing, and no others
33
+ run: [
34
+ // @if server
35
+ 'server',
36
+ // @end
37
+ // @if desktop
38
+ // (`app` starts the dev server itself, which the editor is open on: this one uses that one)
39
+ 'app:editor',
40
+ // @end
41
+ ]
42
+ }),
22
43
  // @end
23
44
  ]
45
+ // @end
24
46
  })
25
47
  // @end
package/cli.mjs CHANGED
@@ -30,9 +30,13 @@ const DEFAULT_FOLDER = 'my-game'
30
30
  const PARTS = {
31
31
  editor: 'the map editor, at /?editor, saving maps into the project',
32
32
  tests: 'a test of the rules with no page, in Vitest',
33
- assets: 'shared models to start with (@v12-engine/assets): adventurers with their clips, buildings, trees and rocks, props, weapons'
33
+ assets: 'shared models to start with (@v12-engine/assets): adventurers with their clips, buildings, trees and rocks, props, weapons',
34
+ desktop: 'a desktop app: the page in a window of its own, an Electron shell in electron/: npm run app'
34
35
  }
35
36
 
37
+ /** The parts a project has when nobody says: every part but the desktop app, whose shell (Electron) is some 100 to 260 MB to fetch */
38
+ const DEFAULT_PARTS = ['editor', 'tests', 'assets']
39
+
36
40
  /** What each kind of server is, for the help and the question */
37
41
  const SERVER_KINDS = {
38
42
  single: 'one game at a time, joined from the page at /?server',
@@ -53,7 +57,7 @@ ${Object.entries(SERVER_KINDS)
53
57
  With no flags it asks for what it needs. With --yes, or where nobody can answer
54
58
  (CI, a pipe), it asks nothing and takes the defaults for what it was not told:
55
59
  the empty starter, the editor, a single server with no ratings, the tests, the
56
- shared art, a git repository, and no install.
60
+ shared art, no desktop app, a git repository, and no install.
57
61
 
58
62
  Examples:
59
63
  npm create @v12-engine ask for everything
@@ -62,6 +66,7 @@ Examples:
62
66
  npm create @v12-engine . -- --starter melee --name "Orc Wars"
63
67
  npm create @v12-engine duel -- -y --server none --no-editor --install
64
68
  npm create @v12-engine arena -- -y --starter melee --server rooms --ratings
69
+ npm create @v12-engine quest -- -y --desktop a desktop app too (Electron)
65
70
  npx create-v12 apps/orc-wars -y --source in the library's own repository
66
71
  npx @v12-engine/create --list the starters
67
72
 
@@ -114,7 +119,9 @@ export function program(act) {
114
119
  .option('--tests', `with ${PARTS.tests}`)
115
120
  .option('--no-tests', 'without it')
116
121
  .option('--assets', `with ${PARTS.assets}`)
117
- .option('--no-assets', 'without it (melee and the campaign cannot be made without it)')
122
+ .option('--no-assets', 'without it (melee, the campaign and the examples need it)')
123
+ .option('--desktop', `with ${PARTS.desktop}`)
124
+ .option('--no-desktop', 'without it')
118
125
  .option('--git', 'make it a git repository, with a first commit (not inside one)')
119
126
  .option('--no-git', 'no repository')
120
127
  .option('--source', "read the library as it is written: for a project inside the library's own repository (apps/), to work on both at once")
@@ -183,7 +190,8 @@ export async function run(argv, { ask = canAsk(), prompts = clack, log = console
183
190
  const plays = [
184
191
  'The page plays it',
185
192
  project.editor && '/?editor is its map editor',
186
- project.server === 'rooms' && '/?find-game finds games on its server'
193
+ project.server === 'rooms' && '/?find-game finds games on its server',
194
+ project.desktop && `${words.run('app')} opens it in a window of its own`
187
195
  ]
188
196
  .filter(Boolean)
189
197
  .join('; ')
@@ -221,6 +229,7 @@ export function decide(folder, flags) {
221
229
  ratings: flags.ratings ?? false,
222
230
  tests: flags.tests ?? true,
223
231
  assets: assetsOf(flags.starter ?? DEFAULT_STARTER, flags.assets),
232
+ desktop: flags.desktop ?? false,
224
233
  source: flags.source ?? false,
225
234
  git: flags.git ?? !insideRepository(where),
226
235
  install: flags.install ?? false,
@@ -267,14 +276,19 @@ async function askFor(folder, flags, prompts) {
267
276
  ))
268
277
  // a starter that cannot be made without the shared art has it, and is not asked
269
278
  const needed = starterNamed(starter).needs?.includes('assets')
270
- const parts = { editor: flags.editor, tests: flags.tests, assets: needed ? assetsOf(starter, flags.assets) : flags.assets }
279
+ const parts = {
280
+ editor: flags.editor,
281
+ tests: flags.tests,
282
+ assets: needed ? assetsOf(starter, flags.assets) : flags.assets,
283
+ desktop: flags.desktop
284
+ }
271
285
  const unsaid = Object.keys(PARTS).filter((part) => parts[part] === undefined)
272
286
  if (unsaid.length) {
273
287
  const chosen = await answer(
274
288
  prompts.multiselect({
275
289
  message: 'What should it have? (space to choose, enter when done)',
276
290
  options: unsaid.map((part) => ({ value: part, label: part, hint: PARTS[part] })),
277
- initialValues: unsaid,
291
+ initialValues: unsaid.filter((part) => DEFAULT_PARTS.includes(part)),
278
292
  required: false
279
293
  })
280
294
  )
@@ -312,6 +326,7 @@ async function askFor(folder, flags, prompts) {
312
326
  ratings,
313
327
  tests: parts.tests,
314
328
  assets: parts.assets,
329
+ desktop: parts.desktop,
315
330
  source: flags.source ?? false,
316
331
  git,
317
332
  install,
package/index.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * N2), each a folder of files laid over the ones before:
4
4
  *
5
5
  * 1. `base/`: what every project has: its page, its `package.json`, its TypeScript config
6
- * 2. `layers/<option>/`: what an option on adds (the editor, a server, the tests), in `LAYERS`'s order
6
+ * 2. `layers/<option>/`: what an option on adds (the editor, a server, the tests, a desktop app), in `LAYERS`'s order
7
7
  * 3. `starters/<starter>/`: the game, its rules, look and maps, and its README
8
8
  * 4. `starters/<starter>/layers/<option>/`: what the starter adds with an option on (its tests)
9
9
  *
@@ -60,14 +60,14 @@ export function titleProblem(title) {
60
60
  * The options that are layers, in the order they are laid: each may have a folder in `layers/` and in a starter's
61
61
  * `layers/`, and is a name its files' `@if` lines may ask about
62
62
  */
63
- export const LAYERS = ['editor', 'server', 'rooms', 'tests', 'assets', 'source']
63
+ export const LAYERS = ['editor', 'server', 'rooms', 'tests', 'assets', 'desktop', 'source']
64
64
 
65
65
  /**
66
66
  * What a file's `@if` lines may ask about: the layers, and what changes a few lines and has no files of its own
67
67
  * (`ratings`; `single`, a server of one game at a time, where `server` is any server and `rooms` one of rooms;
68
68
  * `generator`, the starter's, not the command line's: its random map made in a worker, `starters`)
69
69
  */
70
- export const OPTIONS = ['editor', 'server', 'single', 'rooms', 'ratings', 'tests', 'assets', 'source', 'generator']
70
+ export const OPTIONS = ['editor', 'server', 'single', 'rooms', 'ratings', 'tests', 'assets', 'desktop', 'source', 'generator']
71
71
 
72
72
  /** What a server of the game is: one game at a time (`single`), many games in rooms that players find and make (`rooms`), or none */
73
73
  export const SERVERS = ['single', 'rooms', 'none']
@@ -91,10 +91,13 @@ export function commandsOf(packageManager = 'npm') {
91
91
  return { install, run }
92
92
  }
93
93
 
94
- /** The files whose words may be the game's name and may have `@if` lines: its code, its page, its README, its maps */
94
+ /** The files whose words may be the game's name and may have `@if` lines: its code, its page, its README, its maps, its shell's main process */
95
95
  const TEXT = new Set(['.ts', '.mjs', '.html', '.md', '.json', '.css'])
96
96
  const isText = (path) => TEXT.has(extname(path)) || basename(path) === '_gitignore'
97
97
 
98
+ /** The order of the keys of a project's `package.json`: the rest after, as they came */
99
+ const TOP = ['name', 'private', 'version', 'type', 'main']
100
+
98
101
  /** The order of a project's scripts, as one reads them: the rest after, as they came */
99
102
  const SCRIPTS = ['dev', 'build', 'preview', 'server', 'test', 'typecheck']
100
103
 
@@ -111,6 +114,9 @@ const SCRIPTS = ['dev', 'build', 'preview', 'server', 'test', 'typecheck']
111
114
  * - `assets`: the shared art (`@v12-engine/assets`: models only: adventurers with their clips, buildings, trees and rocks, props, weapons), served by the dev server and
112
115
  * copied into a build as the game uses it (`assetsPackage`); on when left out, and always for a starter that plays
113
116
  * with it (`needs`)
117
+ * - `desktop`: the page as a desktop app, in an Electron shell (`electron/`, `npm run app`, `npm run package`): the dev
118
+ * server on the port the shell opens, and the page built for the shell's Chromium. Off when left out: Electron is
119
+ * some 100 to 260 MB, fetched the first time the app is opened
114
120
  * - `packageManager`: what installs and runs it (`PACKAGE_MANAGERS`): its README and comments say its words; `npm`
115
121
  * when left out
116
122
  * - `source`: the library read as it is written, for a project inside the library's own repository (`apps/`): its
@@ -128,6 +134,7 @@ export async function createProject(folder, options = {}) {
128
134
  editor = true,
129
135
  server = 'single',
130
136
  tests = true,
137
+ desktop = false,
131
138
  source = false,
132
139
  packageManager = 'npm',
133
140
  dependencies = {}
@@ -146,7 +153,18 @@ export async function createProject(folder, options = {}) {
146
153
  const problem = titleProblem(title)
147
154
  if (problem) throw new Error(`"${title}": ${problem}`)
148
155
  const generator = !!starterNamed(starter).generator
149
- const on = { editor, server: server !== 'none', single: server === 'single', rooms: server === 'rooms', ratings, tests, assets, source, generator }
156
+ const on = {
157
+ editor,
158
+ server: server !== 'none',
159
+ single: server === 'single',
160
+ rooms: server === 'rooms',
161
+ ratings,
162
+ tests,
163
+ assets,
164
+ desktop,
165
+ source,
166
+ generator
167
+ }
150
168
  // the way from the project to the repository's root (`{{root}}` in the source layer): this package is `packages/create` of it
151
169
  const root = relative(where, resolve(here, '../..')).split(sep).join('/') || '.'
152
170
  const files = await putTogether(starter, on)
@@ -183,7 +201,7 @@ export async function createProject(folder, options = {}) {
183
201
  : content
184
202
  )
185
203
  }
186
- return { name, title, where, starter, editor, server, ratings, tests, assets, source, packageManager }
204
+ return { name, title, where, starter, editor, server, ratings, tests, assets, desktop, source, packageManager }
187
205
  }
188
206
 
189
207
  /**
@@ -264,7 +282,9 @@ function merged(before, layer) {
264
282
  for (const key of ['dependencies', 'devDependencies']) {
265
283
  if (project[key]) project[key] = Object.fromEntries(Object.entries(project[key]).sort(([a], [b]) => a.localeCompare(b)))
266
284
  }
267
- return `${JSON.stringify(project, null, '\t')}\n`
285
+ // what a layer adds at the top (the desktop app's `main`) where one reads it, before the scripts
286
+ const top = (key) => (TOP.includes(key) ? TOP.indexOf(key) : TOP.length)
287
+ return `${JSON.stringify(Object.fromEntries(Object.entries(project).sort(([a], [b]) => top(a) - top(b))), null, '\t')}\n`
268
288
  }
269
289
 
270
290
  /** The starters' game is `my-game`, My game: the project's is its own, in its code, its page, its README and its maps */
@@ -0,0 +1,237 @@
1
+ // The shell: a window with the page in it. Its full screen is the game's, on macOS a plain window over the screen
2
+ // (`setSimpleFullScreen`), no Space (`fullscreenable: false`), and the Dock and the menu bar hidden by the app's
3
+ // presentation options, which Electron's API has no way to say: the small addon `presentation.m` says them
4
+ // (`npm run app` and `npm run package` compile it). Without the addon the bars auto-hide, as `setSimpleFullScreen` leaves
5
+ // them. Esc is then a key of the page's, the game's pause menu. Elsewhere it is the window's own full screen.
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
7
+ import { createRequire } from 'node:module'
8
+ import { dirname, join } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+ import { app, autoUpdater, BrowserWindow, ipcMain, Menu, protocol, shell } from 'electron'
11
+ import { IDENTIFIER, NAME, UPDATES } from './config.mjs'
12
+ import { logFiles, recordFiles } from './files.mjs'
13
+ import { answer, contentPolicy } from './serve.mjs'
14
+ import { feedOf, updatesOf } from './updates.mjs'
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url))
17
+
18
+ /** `NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar` */
19
+ export const HIDE_BARS = 0x2 | 0x8
20
+ /** How long the page has to save the game as the window closes, past which the window closes anyway */
21
+ const CLOSE_WITHIN = 6000
22
+ /** What AppKit's move of the window over the screen and back takes, and a little */
23
+ const ANIMATION = 600
24
+
25
+ // (before the app is ready, as the scheme has to be: `app:` is a page's origin like `https:`)
26
+ protocol.registerSchemesAsPrivileged([
27
+ { scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true, codeCache: true } }
28
+ ])
29
+
30
+ /** The addon, where it is built and the system is macOS */
31
+ export function presentation() {
32
+ const file = join(here, 'presentation.node')
33
+ if (process.platform !== 'darwin' || !existsSync(file)) return undefined
34
+ try {
35
+ return createRequire(import.meta.url)(file)
36
+ } catch (error) {
37
+ console.error('The presentation options cannot be said:', error)
38
+ return undefined
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Starts the shell. `main.mjs` gives no option but `page` while developing; the rest are for checks of the shell
44
+ * (`apps/desktop/measure`). `page`: the dev server's address, in the place of the built page. `dist`: the built page.
45
+ * `query`: what its address asks. `connect`: other addresses its policy lets it ask. `window`: said over the window's
46
+ * options. `over`: whether it starts over the screen. `userData`: the app's folder. `prepared`: told of the window
47
+ * before the page is asked for
48
+ */
49
+ export async function startShell(options = {}) {
50
+ const { page, dist = join(here, '..', 'dist'), query = '', connect = [], window: asked = {}, over: startsOver = true, userData, prepared } = options
51
+ app.setName(NAME)
52
+ app.setPath('userData', userData ?? join(app.getPath('appData'), IDENTIFIER))
53
+ await app.whenReady()
54
+
55
+ // the keys a player expects of the system, and none of the browser's (reload, its tools, its full screen) but while developing
56
+ Menu.setApplicationMenu(
57
+ Menu.buildFromTemplate([{ role: 'appMenu' }, { role: 'editMenu' }, ...(page ? [{ role: 'viewMenu' }] : []), { role: 'windowMenu' }])
58
+ )
59
+ const policy = contentPolicy(connect)
60
+ protocol.handle('app', (request) => answer(dist, request, policy))
61
+ /** Whether an address is the page's own: the app's scheme, or the dev server's (Node's URL gives a scheme of its own no origin) */
62
+ const ours = (address) => (page ? new URL(address).origin === new URL(page).origin : new URL(address).protocol === 'app:')
63
+
64
+ const records = recordFiles(app.getPath('userData'))
65
+ const logs = logFiles(app.getPath('logs'))
66
+ const native = presentation()
67
+ const sizeFile = join(app.getPath('userData'), 'window.json')
68
+ const kept = readKept(sizeFile)
69
+ const updates = updatesOf(autoUpdater, feedOf(UPDATES), app.isPackaged)
70
+
71
+ const win = new BrowserWindow({
72
+ width: kept?.width ?? 1280,
73
+ height: kept?.height ?? 800,
74
+ ...(kept && { x: kept.x, y: kept.y }),
75
+ useContentSize: true,
76
+ minWidth: 960,
77
+ minHeight: 600,
78
+ title: NAME,
79
+ show: false,
80
+ // AppKit's own way in (the green button, Ctrl+Cmd+F): a Space, with the bars back at its edges, would be a second full screen
81
+ fullscreenable: process.platform !== 'darwin',
82
+ backgroundColor: '#000000',
83
+ // (macOS takes the bundle's; Windows and Linux the window's)
84
+ ...(process.platform !== 'darwin' && { icon: join(here, 'icons/icon.png') }),
85
+ webPreferences: { preload: join(here, 'preload.cjs'), contextIsolation: true, sandbox: true, nodeIntegration: false },
86
+ ...asked
87
+ })
88
+
89
+ let over = false
90
+ /** The window's place while it is over the screen */
91
+ let windowed
92
+ let closing = false
93
+ let closeIsHeard = false
94
+
95
+ /** The window's size and place kept for the next start: a window's, never the screen's */
96
+ const keep = () => {
97
+ if (over || win.isDestroyed()) return
98
+ try {
99
+ mkdirSync(dirname(sizeFile), { recursive: true })
100
+ writeFileSync(sizeFile, JSON.stringify(win.getContentBounds()))
101
+ } catch (error) {
102
+ console.error('The window size cannot be kept:', error)
103
+ }
104
+ }
105
+
106
+ const hideBars = () => {
107
+ if (over) native?.setOptions(HIDE_BARS)
108
+ }
109
+
110
+ const setOver = (on) => {
111
+ if (on === over) return
112
+ if (on) {
113
+ keep()
114
+ windowed = win.getContentBounds()
115
+ }
116
+ over = on
117
+ if (process.platform === 'darwin') {
118
+ win.setSimpleFullScreen(on)
119
+ hideBars()
120
+ // (the window's style changes under the page, which is left without the keyboard: read back `document.hasFocus()` false)
121
+ win.webContents.focus()
122
+ setTimeout(() => win.isDestroyed() || win.webContents.focus(), ANIMATION)
123
+ // (AppKit animates the way there, and a way back asked during it ends at a size of its own: the size is put back when it is done)
124
+ if (!on && windowed) {
125
+ const back = windowed
126
+ setTimeout(() => over || win.isDestroyed() || win.setContentBounds(back), ANIMATION)
127
+ }
128
+ } else {
129
+ win.setFullScreen(on)
130
+ }
131
+ }
132
+
133
+ win.on('focus', hideBars)
134
+ win.on('resize', () => win.webContents.send('v12:resized'))
135
+ win.on('close', (event) => {
136
+ keep()
137
+ if (closing || !closeIsHeard) return
138
+ // held until the page has saved the game: it destroys the window, or the time is up
139
+ event.preventDefault()
140
+ closing = true
141
+ win.webContents.send('v12:close-requested')
142
+ setTimeout(() => win.isDestroyed() || win.destroy(), CLOSE_WITHIN).unref()
143
+ })
144
+ app.on('window-all-closed', () => app.quit())
145
+
146
+ // a question is the app's own page's, and only that
147
+ const own = (event) => {
148
+ try {
149
+ return ours(event.senderFrame?.url ?? 'about:blank')
150
+ } catch {
151
+ return false
152
+ }
153
+ }
154
+ const allowed =
155
+ (handler) =>
156
+ (event, ...rest) => {
157
+ if (!own(event)) throw new Error('Asked by something other than the page of the app')
158
+ return handler(...rest)
159
+ }
160
+ ipcMain.handle(
161
+ 'v12:fullscreen',
162
+ allowed((on) => {
163
+ if (typeof on === 'boolean') setOver(on)
164
+ return over
165
+ })
166
+ )
167
+ ipcMain.handle(
168
+ 'v12:close',
169
+ allowed(() => win.close())
170
+ )
171
+ ipcMain.handle(
172
+ 'v12:destroy',
173
+ allowed(() => win.destroy())
174
+ )
175
+ ipcMain.handle(
176
+ 'v12:records',
177
+ allowed((action, ...rest) => {
178
+ if (!Object.hasOwn(records, action)) throw new Error(`Nothing is called ${action}`)
179
+ return records[action](...rest)
180
+ })
181
+ )
182
+ ipcMain.handle(
183
+ 'v12:log',
184
+ allowed((action, ...rest) => {
185
+ if (!Object.hasOwn(logs, action)) throw new Error(`Nothing is called ${action}`)
186
+ return logs[action](...rest)
187
+ })
188
+ )
189
+ ipcMain.handle(
190
+ 'v12:open-folder',
191
+ allowed(async (folder) => {
192
+ if (folder !== 'saves' && folder !== 'logs') throw new Error(`"${folder}" is not a folder of the app's`)
193
+ const path = folder === 'logs' ? app.getPath('logs') : join(app.getPath('userData'), 'saves')
194
+ mkdirSync(path, { recursive: true })
195
+ await shell.openPath(path)
196
+ })
197
+ )
198
+ ipcMain.handle(
199
+ 'v12:update-check',
200
+ allowed(() => updates.check())
201
+ )
202
+ ipcMain.handle(
203
+ 'v12:update-install',
204
+ allowed(() => {
205
+ // the page saved the game before it asked: the window is not held again
206
+ closing = true
207
+ updates.install()
208
+ })
209
+ )
210
+ ipcMain.on('v12:close-is-heard', (event, heard) => {
211
+ if (own(event)) closeIsHeard = heard === true
212
+ })
213
+
214
+ // the page stays where it is: another address is for the player's browser, or for nothing
215
+ win.webContents.on('will-navigate', (event, url) => {
216
+ if (!ours(url)) event.preventDefault()
217
+ })
218
+ win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
219
+
220
+ prepared?.(win)
221
+ if (startsOver) setOver(true)
222
+ const shown = new Promise((resolve) => win.webContents.once('did-finish-load', resolve))
223
+ await win.loadURL(page ? new URL(query, page).href : `app://game/${query}`)
224
+ await shown.catch(() => undefined)
225
+ if (!asked.paintWhenInitiallyHidden) win.show()
226
+ hideBars()
227
+ return { win, native, setOver, isOver: () => over }
228
+ }
229
+
230
+ function readKept(file) {
231
+ try {
232
+ const kept = JSON.parse(readFileSync(file, 'utf8'))
233
+ return Number.isFinite(kept.width) && Number.isFinite(kept.height) ? kept : undefined
234
+ } catch {
235
+ return undefined
236
+ }
237
+ }
@@ -0,0 +1,16 @@
1
+ // What the app is called and where it keeps its things: yours to settle before it is handed out. The identifier names
2
+ // the app's folder of the system (`~/Library/Application Support/<identifier>`, `%APPDATA%\<identifier>`): changing it
3
+ // after a release leaves the players' saves in the old one. The version is the project's `package.json`'s.
4
+
5
+ /** The app's name, on its window, its menu and its bundle */
6
+ export const NAME = 'My game'
7
+
8
+ /** The app's identifier: its bundle's on macOS, and its data folder's name everywhere */
9
+ export const IDENTIFIER = 'com.example.my-game'
10
+
11
+ /**
12
+ * Where its releases are, for the updater (`updates.mjs`): a folder of static files by system and processor, as
13
+ * Electron's docs lay it out (`<address>/darwin/arm64/RELEASES.json`, `<address>/win32/x64/RELEASES`). A placeholder,
14
+ * which asks nothing: a host under `.invalid` is never asked. `docs/scenes/desktop.md`, Updates, says what goes here
15
+ */
16
+ export const UPDATES = 'https://updates.invalid/REPLACE-WITH-THE-ADDRESS'