@v12-engine/create 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/base/_gitignore +5 -0
- package/base/package.json +3 -3
- package/base/src/main.ts +36 -10
- package/base/src/navigation.worker.ts +7 -0
- package/base/vite.config.ts +24 -2
- package/cli.mjs +21 -6
- package/index.mjs +27 -7
- package/layers/desktop/electron/app.mjs +237 -0
- package/layers/desktop/electron/config.mjs +16 -0
- package/layers/desktop/electron/dev.mjs +63 -0
- package/layers/desktop/electron/files.mjs +86 -0
- package/layers/desktop/electron/icons/icon.icns +0 -0
- package/layers/desktop/electron/icons/icon.png +0 -0
- package/layers/desktop/electron/main.mjs +12 -0
- package/layers/desktop/electron/package.mjs +75 -0
- package/layers/desktop/electron/preload.cjs +41 -0
- package/layers/desktop/electron/presentation.m +87 -0
- package/layers/desktop/electron/serve.mjs +76 -0
- package/layers/desktop/electron/tools.mjs +63 -0
- package/layers/desktop/electron/updates.mjs +71 -0
- package/layers/desktop/package.json +13 -0
- package/layers/desktop/src/desktop.ts +175 -0
- package/layers/desktop/src/shell.ts +279 -0
- package/layers/desktop/tests/main.test.ts +189 -0
- package/layers/desktop/tests/shell.test.ts +152 -0
- package/layers/rooms/server.ts +13 -10
- package/layers/server/server.ts +4 -5
- package/layers/source/vite.config.ts +21 -1
- package/package.json +2 -1
- package/starters/campaign/README.md +9 -2
- package/starters/campaign/layers/rooms/server.ts +3 -3
- package/starters/campaign/layers/server/server.ts +3 -5
- package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
- package/starters/campaign/src/campaign/game.ts +26 -5
- package/starters/campaign/src/campaign/index.ts +104 -68
- package/starters/campaign/src/campaign/kills.ts +1 -1
- package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
- package/starters/campaign/src/campaign/missions/siege.ts +6 -6
- package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
- package/starters/campaign/src/campaign/scenes.ts +8 -7
- package/starters/campaign/src/campaign/units.ts +59 -20
- package/starters/campaign/src/campaign/view/attacks.ts +19 -0
- package/starters/campaign/src/campaign/view/fire.ts +2 -2
- package/starters/campaign/src/campaign/view/index.ts +54 -43
- package/starters/campaign/src/campaign/waves.ts +18 -17
- package/starters/campaign/src/game.ts +8 -1
- package/starters/campaign/src/main.ts +38 -19
- package/starters/campaign/src/maps/fort.names.ts +2 -2
- package/starters/campaign/src/maps/fort.vgmap.json +102 -2
- package/starters/campaign/src/maps/homestead.names.ts +2 -2
- package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
- package/starters/campaign/src/maps/pass.names.ts +2 -2
- package/starters/campaign/src/maps/pass.vgmap.json +69 -1
- package/starters/campaign/src/waves-panel.ts +2 -1
- package/starters/empty/README.md +18 -10
- package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
- package/starters/empty/src/game.ts +11 -75
- package/starters/empty/src/generate.worker.ts +5 -5
- package/starters/empty/src/rules.ts +83 -25
- package/starters/empty/src/view.ts +82 -0
- package/starters/examples/README.md +83 -0
- package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
- package/starters/examples/package.json +5 -0
- package/starters/examples/src/examples/abilities/areas.ts +165 -0
- package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
- package/starters/examples/src/examples/abilities/buffs.ts +287 -0
- package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
- package/starters/examples/src/examples/abilities/changing.ts +228 -0
- package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
- package/starters/examples/src/examples/abilities/control.ts +158 -0
- package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
- package/starters/examples/src/examples/abilities/damage.ts +156 -0
- package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
- package/starters/examples/src/examples/abilities/family.ts +29 -0
- package/starters/examples/src/examples/abilities/index.ts +32 -0
- package/starters/examples/src/examples/abilities/kit.ts +179 -0
- package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
- package/starters/examples/src/examples/abilities/looks.ts +47 -0
- package/starters/examples/src/examples/abilities/missiles.ts +203 -0
- package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
- package/starters/examples/src/examples/abilities/moves.ts +206 -0
- package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
- package/starters/examples/src/examples/abilities/recasts.ts +181 -0
- package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
- package/starters/examples/src/examples/abilities/resources.ts +166 -0
- package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
- package/starters/examples/src/examples/abilities/summons.ts +296 -0
- package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
- package/starters/examples/src/examples/abilities/triggers.ts +204 -0
- package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
- package/starters/examples/src/examples/abilities/vision.ts +137 -0
- package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
- package/starters/examples/src/examples/animation/index.ts +235 -0
- package/starters/examples/src/examples/animation/looks.ts +444 -0
- package/starters/examples/src/examples/catalog.ts +52 -0
- package/starters/examples/src/examples/cinematics/index.ts +217 -0
- package/starters/examples/src/examples/cinematics/looks.ts +476 -0
- package/starters/examples/src/examples/cinematics/tour.json +43 -0
- package/starters/examples/src/examples/cinematics/village.ts +88 -0
- package/starters/examples/src/examples/common/looks.ts +65 -0
- package/starters/examples/src/examples/common/units.ts +70 -0
- package/starters/examples/src/examples/construction/index.ts +329 -0
- package/starters/examples/src/examples/construction/looks.ts +275 -0
- package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
- package/starters/examples/src/examples/custom-animation/index.ts +190 -0
- package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
- package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
- package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
- package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
- package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
- package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
- package/starters/examples/src/examples/custom-fx/index.ts +25 -0
- package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
- package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
- package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
- package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
- package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
- package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
- package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
- package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
- package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
- package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
- package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
- package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
- package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
- package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
- package/starters/examples/src/examples/define.ts +86 -0
- package/starters/examples/src/examples/destruction/index.ts +570 -0
- package/starters/examples/src/examples/destruction/looks.ts +447 -0
- package/starters/examples/src/examples/dialogs/index.ts +383 -0
- package/starters/examples/src/examples/dialogs/looks.ts +546 -0
- package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
- package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
- package/starters/examples/src/examples/fx/blood.look.ts +62 -0
- package/starters/examples/src/examples/fx/blood.ts +59 -0
- package/starters/examples/src/examples/fx/feel.look.ts +86 -0
- package/starters/examples/src/examples/fx/feel.ts +57 -0
- package/starters/examples/src/examples/fx/fight.ts +28 -0
- package/starters/examples/src/examples/fx/floats.look.ts +108 -0
- package/starters/examples/src/examples/fx/floats.ts +24 -0
- package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
- package/starters/examples/src/examples/fx/gallery.ts +23 -0
- package/starters/examples/src/examples/fx/index.ts +30 -0
- package/starters/examples/src/examples/fx/links.look.ts +83 -0
- package/starters/examples/src/examples/fx/links.ts +49 -0
- package/starters/examples/src/examples/fx/looks.ts +55 -0
- package/starters/examples/src/examples/fx/map.look.ts +93 -0
- package/starters/examples/src/examples/fx/map.ts +33 -0
- package/starters/examples/src/examples/fx/marks.look.ts +100 -0
- package/starters/examples/src/examples/fx/marks.ts +36 -0
- package/starters/examples/src/examples/fx/shots.look.ts +92 -0
- package/starters/examples/src/examples/fx/shots.ts +71 -0
- package/starters/examples/src/examples/fx/spells.look.ts +87 -0
- package/starters/examples/src/examples/fx/spells.ts +122 -0
- package/starters/examples/src/examples/fx/stage.ts +31 -0
- package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
- package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
- package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
- package/starters/examples/src/examples/gui/index.ts +143 -0
- package/starters/examples/src/examples/gui/keys.ts +91 -0
- package/starters/examples/src/examples/gui/layout.ts +42 -0
- package/starters/examples/src/examples/gui/looks.ts +270 -0
- package/starters/examples/src/examples/gui/minimap.ts +127 -0
- package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
- package/starters/examples/src/examples/gui/plates.ts +87 -0
- package/starters/examples/src/examples/gui/radial.ts +104 -0
- package/starters/examples/src/examples/gui/solidPart.ts +107 -0
- package/starters/examples/src/examples/looks.ts +45 -0
- package/starters/examples/src/examples/mechanics/battle.ts +51 -0
- package/starters/examples/src/examples/mechanics/damage.ts +80 -0
- package/starters/examples/src/examples/mechanics/flying.ts +93 -0
- package/starters/examples/src/examples/mechanics/formations.ts +58 -0
- package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
- package/starters/examples/src/examples/mechanics/index.ts +44 -0
- package/starters/examples/src/examples/mechanics/looks.ts +289 -0
- package/starters/examples/src/examples/mechanics/maze.ts +119 -0
- package/starters/examples/src/examples/mechanics/orders.ts +68 -0
- package/starters/examples/src/examples/mechanics/selection.ts +30 -0
- package/starters/examples/src/examples/moba/index.ts +377 -0
- package/starters/examples/src/examples/moba/looks.ts +349 -0
- package/starters/examples/src/examples/moba/maps.ts +69 -0
- package/starters/examples/src/examples/moba/spells.ts +34 -0
- package/starters/examples/src/examples/moba/units.ts +196 -0
- package/starters/examples/src/examples/nature/camera.ts +15 -0
- package/starters/examples/src/examples/nature/grass.ts +64 -0
- package/starters/examples/src/examples/nature/index.ts +327 -0
- package/starters/examples/src/examples/nature/land.ts +173 -0
- package/starters/examples/src/examples/nature/looks.ts +577 -0
- package/starters/examples/src/examples/nature/maps.ts +370 -0
- package/starters/examples/src/examples/nature/skies.ts +324 -0
- package/starters/examples/src/examples/nature/weather.ts +80 -0
- package/starters/examples/src/examples/performance/index.ts +263 -0
- package/starters/examples/src/examples/performance/looks.ts +372 -0
- package/starters/examples/src/examples/performance/meter.ts +158 -0
- package/starters/examples/src/examples/post/grades.ts +86 -0
- package/starters/examples/src/examples/post/index.ts +202 -0
- package/starters/examples/src/examples/post/looks.ts +884 -0
- package/starters/examples/src/examples/post/passes.ts +213 -0
- package/starters/examples/src/examples/rts/ai.ts +49 -0
- package/starters/examples/src/examples/rts/airlift.ts +43 -0
- package/starters/examples/src/examples/rts/base.ts +129 -0
- package/starters/examples/src/examples/rts/build.ts +65 -0
- package/starters/examples/src/examples/rts/camps.ts +36 -0
- package/starters/examples/src/examples/rts/carry.ts +103 -0
- package/starters/examples/src/examples/rts/creeps.ts +61 -0
- package/starters/examples/src/examples/rts/data.ts +406 -0
- package/starters/examples/src/examples/rts/ferry.ts +41 -0
- package/starters/examples/src/examples/rts/garrison.ts +50 -0
- package/starters/examples/src/examples/rts/harvest.ts +29 -0
- package/starters/examples/src/examples/rts/hero.ts +67 -0
- package/starters/examples/src/examples/rts/index.ts +77 -0
- package/starters/examples/src/examples/rts/looks.ts +721 -0
- package/starters/examples/src/examples/rts/shop.ts +46 -0
- package/starters/examples/src/examples/rts/tech.ts +39 -0
- package/starters/examples/src/examples/rts/train.ts +48 -0
- package/starters/examples/src/examples/rts/waterside.ts +185 -0
- package/starters/examples/src/examples/showcase/index.ts +714 -0
- package/starters/examples/src/examples/showcase/looks.ts +238 -0
- package/starters/examples/src/game.ts +17 -0
- package/starters/examples/src/main.ts +82 -0
- package/starters/examples/src/rules.ts +31 -0
- package/starters/examples/src/shell/look.ts +79 -0
- package/starters/examples/src/shell/registry.ts +7 -0
- package/starters/examples/src/shell/scenes.ts +178 -0
- package/starters/examples/src/shell/shell.ts +290 -0
- package/starters/examples/src/shell/style.ts +202 -0
- package/starters/examples/src/view.ts +50 -0
- package/starters/melee/README.md +12 -3
- package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
- package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
- package/starters/melee/src/game.ts +32 -2
- package/starters/melee/src/maps/bay.names.ts +2 -2
- package/starters/melee/src/maps/bay.vgmap.json +198 -1
- package/starters/melee/src/maps.ts +11 -9
- package/starters/melee/src/melee/abilities.ts +1 -194
- package/starters/melee/src/melee/blight.ts +7 -7
- package/starters/melee/src/melee/cellLayers.json +3 -3
- package/starters/melee/src/melee/data.json +1 -18
- package/starters/melee/src/melee/game.ts +6 -4
- package/starters/melee/src/melee/heroes.ts +1025 -0
- package/starters/melee/src/melee/index.ts +64 -61
- package/starters/melee/src/melee/items.ts +15 -362
- package/starters/melee/src/melee/languages/he.json +14 -2
- package/starters/melee/src/melee/pings.ts +5 -6
- package/starters/melee/src/melee/saves.ts +1 -1
- package/starters/melee/src/melee/undead.ts +15 -230
- package/starters/melee/src/melee/units.ts +37 -171
- package/starters/melee/src/melee/view/attacks.ts +19 -0
- package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
- package/starters/melee/src/melee/view/index.ts +56 -52
- package/starters/melee/src/melee/view/looks.ts +86 -63
- package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
- package/starters/melee/src/melee/view/walls.ts +1 -1
- package/starters.mjs +6 -0
- package/v12.mjs +14 -15
- package/starters/campaign/src/rules.ts +0 -20
- package/starters/empty/src/maps.ts +0 -44
- package/starters/empty/src/tiles.ts +0 -12
- package/starters/melee/src/melee/heroSpells.ts +0 -38
- 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": "
|
|
15
|
-
"@babylonjs/gui": "
|
|
16
|
-
"@babylonjs/loaders": "
|
|
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 {
|
|
24
|
-
|
|
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
|
|
28
|
-
//
|
|
29
|
-
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
|
-
|
|
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 })
|
package/base/vite.config.ts
CHANGED
|
@@ -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({
|
|
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
|
|
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 = {
|
|
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 = {
|
|
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
|
-
|
|
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'
|