@v12-engine/create 0.0.0-stage → 0.1.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -2
  3. package/base/_gitignore +10 -0
  4. package/base/index.html +20 -0
  5. package/base/package.json +23 -0
  6. package/base/src/ground.worker.ts +8 -0
  7. package/base/src/main.ts +61 -0
  8. package/base/tsconfig.json +14 -0
  9. package/base/vite.config.ts +25 -0
  10. package/bin.mjs +10 -0
  11. package/cli.mjs +381 -0
  12. package/index.mjs +286 -0
  13. package/layers/assets/package.json +5 -0
  14. package/layers/editor/package.json +8 -0
  15. package/layers/editor/src/editor.ts +13 -0
  16. package/layers/rooms/server.ts +69 -0
  17. package/layers/server/package.json +8 -0
  18. package/layers/server/server.ts +34 -0
  19. package/layers/source/package.json +12 -0
  20. package/layers/source/tsconfig.json +5 -0
  21. package/layers/source/vite.config.ts +45 -0
  22. package/layers/tests/package.json +8 -0
  23. package/package.json +45 -5
  24. package/starters/campaign/README.md +61 -0
  25. package/starters/campaign/files.txt +42 -0
  26. package/starters/campaign/layers/editor/src/editor.ts +18 -0
  27. package/starters/campaign/layers/rooms/server.ts +39 -0
  28. package/starters/campaign/layers/server/server.ts +33 -0
  29. package/starters/campaign/layers/tests/tests/rules.test.ts +320 -0
  30. package/starters/campaign/public/games/campaign/assets.json +1 -0
  31. package/starters/campaign/src/campaign/campaign.json +52 -0
  32. package/starters/campaign/src/campaign/carry.ts +47 -0
  33. package/starters/campaign/src/campaign/cinematics/intro.json +148 -0
  34. package/starters/campaign/src/campaign/cinematics/outro.json +92 -0
  35. package/starters/campaign/src/campaign/data.json +1 -0
  36. package/starters/campaign/src/campaign/fx.json +1 -0
  37. package/starters/campaign/src/campaign/game.ts +6 -0
  38. package/starters/campaign/src/campaign/ids.ts +77 -0
  39. package/starters/campaign/src/campaign/index.ts +121 -0
  40. package/starters/campaign/src/campaign/kills.ts +16 -0
  41. package/starters/campaign/src/campaign/looks.json +1 -0
  42. package/starters/campaign/src/campaign/maps.ts +34 -0
  43. package/starters/campaign/src/campaign/missions/homecoming.ts +160 -0
  44. package/starters/campaign/src/campaign/missions/siege.ts +91 -0
  45. package/starters/campaign/src/campaign/missions/warlord.ts +177 -0
  46. package/starters/campaign/src/campaign/prefabs/watchpost.prefab.json +18 -0
  47. package/starters/campaign/src/campaign/scenes.ts +46 -0
  48. package/starters/campaign/src/campaign/tiles.ts +8 -0
  49. package/starters/campaign/src/campaign/units.ts +390 -0
  50. package/starters/campaign/src/campaign/view/cinematics.ts +59 -0
  51. package/starters/campaign/src/campaign/view/fire.ts +115 -0
  52. package/starters/campaign/src/campaign/view/index.ts +334 -0
  53. package/starters/campaign/src/campaign/view/labels.ts +105 -0
  54. package/starters/campaign/src/campaign/view/rubble.ts +40 -0
  55. package/starters/campaign/src/campaign/view/waveLane.ts +88 -0
  56. package/starters/campaign/src/campaign/waves.json +36 -0
  57. package/starters/campaign/src/campaign/waves.ts +186 -0
  58. package/starters/campaign/src/campaign-editor.ts +43 -0
  59. package/starters/campaign/src/game.ts +2 -0
  60. package/starters/campaign/src/main.ts +70 -0
  61. package/starters/campaign/src/maps/fort.names.ts +31 -0
  62. package/starters/campaign/src/maps/fort.vgmap.json +1162 -0
  63. package/starters/campaign/src/maps/homestead.names.ts +36 -0
  64. package/starters/campaign/src/maps/homestead.vgmap.json +916 -0
  65. package/starters/campaign/src/maps/pass.names.ts +40 -0
  66. package/starters/campaign/src/maps/pass.vgmap.json +522 -0
  67. package/starters/campaign/src/maps.ts +2 -0
  68. package/starters/campaign/src/rules.ts +20 -0
  69. package/starters/campaign/src/waves-panel.ts +355 -0
  70. package/starters/empty/README.md +71 -0
  71. package/starters/empty/layers/tests/tests/rules.test.ts +31 -0
  72. package/starters/empty/src/game.ts +79 -0
  73. package/starters/empty/src/generate.worker.ts +9 -0
  74. package/starters/empty/src/maps.ts +44 -0
  75. package/starters/empty/src/rules.ts +67 -0
  76. package/starters/empty/src/tiles.ts +12 -0
  77. package/starters/melee/README.md +61 -0
  78. package/starters/melee/files.txt +60 -0
  79. package/starters/melee/layers/editor/src/editor.ts +13 -0
  80. package/starters/melee/layers/tests/tests/rules.test.ts +58 -0
  81. package/starters/melee/src/game.ts +2 -0
  82. package/starters/melee/src/maps/bay.names.ts +28 -0
  83. package/starters/melee/src/maps/bay.vgmap.json +3123 -0
  84. package/starters/melee/src/maps.ts +20 -0
  85. package/starters/melee/src/melee/abilities.ts +376 -0
  86. package/starters/melee/src/melee/bay.ts +319 -0
  87. package/starters/melee/src/melee/blight.ts +102 -0
  88. package/starters/melee/src/melee/cellLayers.json +11 -0
  89. package/starters/melee/src/melee/data.json +18 -0
  90. package/starters/melee/src/melee/floats.json +199 -0
  91. package/starters/melee/src/melee/formations.ts +46 -0
  92. package/starters/melee/src/melee/fx.json +1 -0
  93. package/starters/melee/src/melee/game.ts +6 -0
  94. package/starters/melee/src/melee/generator.ts +116 -0
  95. package/starters/melee/src/melee/heroSpells.ts +38 -0
  96. package/starters/melee/src/melee/hud.json +269 -0
  97. package/starters/melee/src/melee/index.ts +182 -0
  98. package/starters/melee/src/melee/items.ts +362 -0
  99. package/starters/melee/src/melee/languages/he.json +96 -0
  100. package/starters/melee/src/melee/looks.json +1 -0
  101. package/starters/melee/src/melee/maps.ts +53 -0
  102. package/starters/melee/src/melee/pings.json +16 -0
  103. package/starters/melee/src/melee/pings.ts +34 -0
  104. package/starters/melee/src/melee/quality.json +1 -0
  105. package/starters/melee/src/melee/saves.ts +12 -0
  106. package/starters/melee/src/melee/sounds.json +1 -0
  107. package/starters/melee/src/melee/undead.ts +683 -0
  108. package/starters/melee/src/melee/units.ts +785 -0
  109. package/starters/melee/src/melee/view/buildings.ts +33 -0
  110. package/starters/melee/src/melee/view/cheerLane.ts +42 -0
  111. package/starters/melee/src/melee/view/heroSpellLooks.ts +21 -0
  112. package/starters/melee/src/melee/view/index.ts +268 -0
  113. package/starters/melee/src/melee/view/languages.ts +10 -0
  114. package/starters/melee/src/melee/view/looks.ts +659 -0
  115. package/starters/melee/src/melee/view/undeadLooks.ts +220 -0
  116. package/starters/melee/src/melee/view/walls.ts +59 -0
  117. package/starters/melee/src/rules.ts +24 -0
  118. package/starters.mjs +35 -0
  119. package/v12.mjs +111 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Doron Torangy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @v12-engine/create
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A project to start a game from, made with V12: its rules, its page, and the parts you choose.
4
+
5
+ ```sh
6
+ npm create @v12-engine my-game
7
+ ```
8
+
9
+ It asks where to make it, which starter, what the game is called, which parts it has, what server, and whether to make a git repository and install. `--yes` asks nothing; `--help` lists every flag.
10
+
11
+ | Starter | What it is |
12
+ | ---------- | -------------------------------------------------------------------------------------------- |
13
+ | `empty` | Hello world: a flat field and a scout to walk it. The default. |
14
+ | `melee` | A whole RTS: two races and their heroes, bases and armies, against the computer. |
15
+ | `campaign` | A campaign of three missions with a hero carried between them, drawn with the shared models. |
16
+
17
+ ```sh
18
+ npm create @v12-engine arena -- --yes --starter melee --server rooms --ratings
19
+ ```
20
+
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`).
22
+
23
+ In code: `createProject(folder, options)` from `@v12-engine/create`.
24
+
25
+ The project it makes is yours: no attribution to this package is asked for in it. The package itself is MIT.
26
+
27
+ The whole guide is the docs' page Starting a project (`docs/start/new-project.md` in the repository).
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ dist
3
+ *.local
4
+ .DS_Store
5
+ # the replays Play here keeps with a map: the designer's own, not the map's
6
+ **/*.files/replays/
7
+ # @if ratings
8
+ # what the server keeps of who played: not code
9
+ ratings.json
10
+ # @end
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>My game</title>
7
+ <style>
8
+ html,
9
+ body,
10
+ #game {
11
+ height: 100%;
12
+ margin: 0;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div id="game"></div>
18
+ <script type="module" src="/src/main.ts"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "my-game",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@v12-engine/engine": "^0.0.0",
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"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^26.6.5",
20
+ "typescript": "^7.0.2",
21
+ "vite": "^8.3.4"
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The page's worker that makes the ground's textures (the client's patterns: grass, gravel, cobbles…), so the page
3
+ * goes on while they are made. Given to the app in `main.ts` (`groundWorker`)
4
+ */
5
+ // the client's ground alone: nothing of Babylon or the HUD is loaded into the worker
6
+ import { serveGroundTextures } from '@v12-engine/client/ground'
7
+
8
+ serveGroundTextures()
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The page: the game's start screen, and what it goes to from there, each a screen of an app of scenes (`startApp`,
3
+ * `@v12-engine/client/app`), one shown at a time. `appScenes` makes them: the start screen, the game, a random map when
4
+ * the rules have a generator, and the settings.
5
+ // @if editor
6
+ * The map editor too, set up in `src/editor.ts`.
7
+ // @end
8
+ // @if server
9
+ * A seat at the game's server too (`npm run server`).
10
+ // @end
11
+ * The list is a plain array: add a scene of your own to it, or take one away. The address opens straight into a scene
12
+ * by its id (`sceneAsked`: `/?settings`, and those the README says), the start screen otherwise. The debug panel (🐞 on
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
+ * `debug` says otherwise
15
+ */
16
+ import { appScenes, sceneAsked, startApp } from '@v12-engine/client/app'
17
+ import '@v12-engine/client/styles.css'
18
+ // the theme's typefaces come with the client: the page's own font stays what it was
19
+ import '@v12-engine/client/fonts.css'
20
+ // @if editor
21
+ import { editor } from './editor'
22
+ // @end
23
+ import { DEFAULT_MAP, maps } from './maps'
24
+ import { rules, start } from './rules'
25
+
26
+ 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,
34
+ // @if editor
35
+ editor,
36
+ // @end
37
+ // @if single
38
+ server: true,
39
+ // @end
40
+ // @if rooms
41
+ server: { rooms: true },
42
+ // @end
43
+ // @if generator
44
+ // 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
+ generatorWorker: () => new Worker(new URL('./generate.worker.ts', import.meta.url), { type: 'module' }),
46
+ // @end
47
+ menu: { description: 'A game made with V12.' },
48
+ // the Credits on the start screen (`creditsScene`): who made the game, by parts, scrolling
49
+ credits: {
50
+ sections: [
51
+ { title: 'Made by', names: ['You'] },
52
+ { title: 'Made with', names: ['V12'] }
53
+ ]
54
+ }
55
+ })
56
+
57
+ /** 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
+ const groundWorker = () => new Worker(new URL('./ground.worker.ts', import.meta.url), { type: 'module' })
59
+
60
+ const app = startApp({ target: '#game', title: rules.name, scenes, start: sceneAsked(scenes, 'home'), groundWorker })
61
+ Object.assign(window, { app })
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["vite/client", "node"],
8
+ "resolveJsonModule": true,
9
+ "strict": true,
10
+ "noEmit": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src", "tests", "server.ts", "vite.config.ts"]
14
+ }
@@ -0,0 +1,25 @@
1
+ // @if editor|assets
2
+ import { defineConfig } from 'vite'
3
+ // @if assets
4
+ import { assetsPackage } from '@v12-engine/client/vite'
5
+ // @end
6
+ // @if editor
7
+ import { editorFiles } from '@v12-engine/editor/vite'
8
+ // @end
9
+
10
+ export default defineConfig({
11
+ plugins: [
12
+ // @if assets
13
+ // the shared art (@v12-engine/assets) served where the game's shared files are (/models/…), behind public/: a file
14
+ // of yours at the same path is the one served. A build copies what the game uses of it. `assetsPackage({ groups:
15
+ // ['forest'] })` takes some of its groups, `copy: 'all'` copies every file of them
16
+ assetsPackage(),
17
+ // @end
18
+ // @if editor
19
+ // the map editor keeps the game's maps in the project through the dev server: Save writes them into `src/maps`.
20
+ // Only in `npm run dev`; a built page keeps maps in the browser
21
+ editorFiles({ gameFolder: () => 'src' }),
22
+ // @end
23
+ ]
24
+ })
25
+ // @end
package/bin.mjs ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /** `npm create @v12-engine [folder]`: a project to start from. `--help` says what it takes (`cli.mjs`) */
3
+ import { run } from './cli.mjs'
4
+
5
+ try {
6
+ await run(process.argv)
7
+ } catch (error) {
8
+ console.error(error instanceof Error ? error.message : error)
9
+ process.exit(1)
10
+ }
package/cli.mjs ADDED
@@ -0,0 +1,381 @@
1
+ /**
2
+ * The command line of `npm create @v12-engine` (`bin.mjs`): what it is told by its flags, and what it asks a person for
3
+ * what they did not say. Everything ends in one object, `createProject`'s options and what is done once it is made
4
+ * (a repository, an install), so a new option is a flag, a question and a field of it, here
5
+ */
6
+ import { execFileSync } from 'node:child_process'
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { dirname, relative, resolve } from 'node:path'
9
+ import * as clack from '@clack/prompts'
10
+ import { Command, InvalidArgumentError, Option } from 'commander'
11
+ import {
12
+ commandsOf,
13
+ createProject,
14
+ DEFAULT_STARTER,
15
+ notEmpty,
16
+ PACKAGE_MANAGERS,
17
+ SERVERS,
18
+ starterNamed,
19
+ starters,
20
+ titleOf,
21
+ titleProblem
22
+ } from './index.mjs'
23
+
24
+ const own = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
25
+
26
+ /** The folder a project is made in when nobody says which */
27
+ const DEFAULT_FOLDER = 'my-game'
28
+
29
+ /** What the options that are parts of the project are, for the help and the questions */
30
+ const PARTS = {
31
+ editor: 'the map editor, at /?editor, saving maps into the project',
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'
34
+ }
35
+
36
+ /** What each kind of server is, for the help and the question */
37
+ const SERVER_KINDS = {
38
+ single: 'one game at a time, joined from the page at /?server',
39
+ rooms: 'many games in rooms: Find a game at /?find-game, New game, Play now',
40
+ none: 'no server: the game is played in the page'
41
+ }
42
+
43
+ /** What the help says after the flags: the starters, and how it is used */
44
+ const helpAfter = () => `
45
+ Starters:
46
+ ${starters.map((starter) => wrap(starter.summary, 13).replace(/^ {13}/, ` ${starter.name.padEnd(10)} `)).join('\n')}
47
+
48
+ Servers:
49
+ ${Object.entries(SERVER_KINDS)
50
+ .map(([kind, what]) => ` ${kind.padEnd(10)} ${what}`)
51
+ .join('\n')}
52
+
53
+ With no flags it asks for what it needs. With --yes, or where nobody can answer
54
+ (CI, a pipe), it asks nothing and takes the defaults for what it was not told:
55
+ the empty starter, the editor, a single server with no ratings, the tests, the
56
+ shared art, a git repository, and no install.
57
+
58
+ Examples:
59
+ npm create @v12-engine ask for everything
60
+ npm create @v12-engine orc-wars in ./orc-wars, asking the rest
61
+ npm create @v12-engine orc-wars -- --yes in ./orc-wars, all the defaults
62
+ npm create @v12-engine . -- --starter melee --name "Orc Wars"
63
+ npm create @v12-engine duel -- -y --server none --no-editor --install
64
+ npm create @v12-engine arena -- -y --starter melee --server rooms --ratings
65
+ npx create-v12 apps/orc-wars -y --source in the library's own repository
66
+ npx @v12-engine/create --list the starters
67
+
68
+ npm hands flags on after a --. With pnpm, yarn and bun they come straight after
69
+ the folder.
70
+ `
71
+
72
+ /** Words in lines of at most 80, each indented by `indent` */
73
+ const wrap = (text, indent) => {
74
+ const lines = []
75
+ let line = ''
76
+ for (const word of text.split(' ')) {
77
+ if (line && indent + line.length + 1 + word.length > 80) {
78
+ lines.push(line)
79
+ line = word
80
+ } else line = line ? `${line} ${word}` : word
81
+ }
82
+ lines.push(line)
83
+ return lines.map((each) => ' '.repeat(indent) + each).join('\n')
84
+ }
85
+
86
+ /** A starter's name, checked as the flag is read */
87
+ const starterFlag = (value) => {
88
+ try {
89
+ return starterNamed(value).name
90
+ } catch (error) {
91
+ throw new InvalidArgumentError(error instanceof Error ? error.message : String(error))
92
+ }
93
+ }
94
+
95
+ /** A game's name, checked as the flag is read */
96
+ const titleFlag = (value) => {
97
+ const problem = titleProblem(value)
98
+ if (problem) throw new InvalidArgumentError(problem)
99
+ return value
100
+ }
101
+
102
+ /** The command: its flags, its help, and what it does with them (`act`, given the folder and the flags) */
103
+ export function program(act) {
104
+ return new Command('create-v12')
105
+ .description('Makes a game to start from, made with V12: its rules, its page, and what you choose to go with them.')
106
+ .argument('[folder]', `where the project is made, empty or not there yet (${DEFAULT_FOLDER} when not said)`)
107
+ .option('-s, --starter <name>', `the game it starts from: ${starters.map((starter) => starter.name).join(', ')}`, starterFlag)
108
+ .option('-n, --name <name>', "the game's name, as players see it (from the folder's name when not said)", titleFlag)
109
+ .option('--editor', `with ${PARTS.editor}`)
110
+ .option('--no-editor', 'without it')
111
+ .addOption(new Option('--server <kind>', 'a server of the game, or none (see Servers)').choices(SERVERS))
112
+ .option('--ratings', 'the server keeps a rating for each player, in ratings.json')
113
+ .option('--no-ratings', 'no ratings')
114
+ .option('--tests', `with ${PARTS.tests}`)
115
+ .option('--no-tests', 'without it')
116
+ .option('--assets', `with ${PARTS.assets}`)
117
+ .option('--no-assets', 'without it (melee and the campaign cannot be made without it)')
118
+ .option('--git', 'make it a git repository, with a first commit (not inside one)')
119
+ .option('--no-git', 'no repository')
120
+ .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")
121
+ .option('--install', 'install its packages once it is made')
122
+ .option('--no-install', 'leave installing to you')
123
+ .addOption(
124
+ new Option(
125
+ '--package-manager <name>',
126
+ 'what installs and runs it, in what it does and what it says: the one this was run with when not said'
127
+ ).choices(PACKAGE_MANAGERS)
128
+ )
129
+ .option('-y, --yes', 'ask nothing: the defaults for what is not said')
130
+ .option('-l, --list', 'list the starters, and make nothing')
131
+ .version(own.version, '-v, --version', 'the version of this command')
132
+ .helpOption('-h, --help', 'this help')
133
+ .addHelpText('after', helpAfter())
134
+ .showHelpAfterError('(--help says what it takes)')
135
+ .action(act)
136
+ }
137
+
138
+ /**
139
+ * Runs the command line on `argv` (`process.argv`). `ask` is whether a person can answer questions; `prompts` are
140
+ * the questions' library (clack), and `exec` runs a command in a folder (git, the install), for a test to stand in
141
+ * for them. Gives what was made, or nothing when nothing was
142
+ */
143
+ export async function run(argv, { ask = canAsk(), prompts = clack, log = console.log, exec = execIn } = {}) {
144
+ let made
145
+ await program(async (folder, flags) => {
146
+ if (flags.list) {
147
+ for (const starter of starters) log(wrap(`${starter.title}: ${starter.summary}`, 11).replace(/^ {11}/, starter.name.padEnd(11)))
148
+ return
149
+ }
150
+ // a folder that is not empty is said before anything is asked
151
+ if (folder !== undefined && notEmpty(resolve(folder)))
152
+ throw new Error(`${resolve(folder)} is not empty: a project is made in an empty folder, or a new one`)
153
+ const questions = ask && !flags.yes
154
+ const options = questions ? await askFor(folder, flags, prompts) : decide(folder, flags)
155
+ if (!options) return
156
+ const { where, git, install, ...rest } = options
157
+ const project = await createProject(where, rest)
158
+ made = { ...project, git: false, installed: false }
159
+ const say = questions ? (text) => prompts.log.step(text) : log
160
+ const warn = questions ? (text) => prompts.log.warn(text) : log
161
+ if (git) made.git = makeRepository(project.where, { exec, say, warn })
162
+ const words = commandsOf(project.packageManager)
163
+ if (install) {
164
+ say(`Installing, with ${words.install}…`)
165
+ try {
166
+ exec(project.packageManager, ['install'], project.where, true)
167
+ made.installed = true
168
+ } catch {
169
+ warn(`${words.install} did not finish: run it in the project`)
170
+ }
171
+ }
172
+ // where it is, from here: the whole path where it is not under here
173
+ const below = relative(process.cwd(), project.where)
174
+ const into = below.startsWith('..') ? project.where : below || '.'
175
+ // a project that reads the library as it is written is a workspace of its repository: installed and run from the root
176
+ const next = project.source
177
+ ? [...(made.installed ? [] : ['npm install']), `npm run dev -w ${project.name}`]
178
+ : [
179
+ ...(into === '.' ? [] : [`cd ${/\s/.test(into) ? JSON.stringify(into) : into}`]),
180
+ ...(made.installed ? [] : [words.install]),
181
+ words.run('dev')
182
+ ]
183
+ const plays = [
184
+ 'The page plays it',
185
+ project.editor && '/?editor is its map editor',
186
+ project.server === 'rooms' && '/?find-game finds games on its server'
187
+ ]
188
+ .filter(Boolean)
189
+ .join('; ')
190
+ .concat('.')
191
+ if (questions) {
192
+ prompts.note(next.join('\n'), 'Next')
193
+ prompts.outro(`${project.title} is in ${into}. ${plays} Its README.md says the rest.`)
194
+ } else {
195
+ log(`\n${project.title}, in ${into}. Next:\n`)
196
+ for (const line of next) log(` ${line}`)
197
+ log(`\n${plays} Its README.md says the rest.`)
198
+ }
199
+ }).parseAsync(argv)
200
+ return made
201
+ }
202
+
203
+ /** Whether a person is there to answer: a terminal, and not CI */
204
+ export const canAsk = () => !!process.stdin.isTTY && !!process.stdout.isTTY && !clack.isCI()
205
+
206
+ /** The package manager this was run with (`npm create`, `pnpm create`, `yarn create`, `bun create`), npm when it cannot tell */
207
+ export function packageManagerOf(agent = process.env.npm_config_user_agent ?? '') {
208
+ const name = agent.split('/')[0]
209
+ return PACKAGE_MANAGERS.includes(name) ? name : 'npm'
210
+ }
211
+
212
+ /** `createProject`'s options and what is done once it is made, from the flags alone: the defaults for what they do not say */
213
+ export function decide(folder, flags) {
214
+ const where = resolve(folder ?? DEFAULT_FOLDER)
215
+ return {
216
+ where,
217
+ starter: flags.starter ?? DEFAULT_STARTER,
218
+ title: flags.name ?? titleOf(where),
219
+ editor: flags.editor ?? true,
220
+ server: flags.server ?? 'single',
221
+ ratings: flags.ratings ?? false,
222
+ tests: flags.tests ?? true,
223
+ assets: assetsOf(flags.starter ?? DEFAULT_STARTER, flags.assets),
224
+ source: flags.source ?? false,
225
+ git: flags.git ?? !insideRepository(where),
226
+ install: flags.install ?? false,
227
+ packageManager: flags.packageManager ?? packageManagerOf()
228
+ }
229
+ }
230
+
231
+ /** `decide`'s options, asking for what the flags do not say. Nothing when the person stops (Ctrl+C, Escape) */
232
+ async function askFor(folder, flags, prompts) {
233
+ prompts.intro(`V12 ${own.version}: a game to start from`)
234
+ const answer = async (asked) => {
235
+ const value = await asked
236
+ if (prompts.isCancel(value)) throw CANCELLED
237
+ return value
238
+ }
239
+ try {
240
+ folder ??= await answer(
241
+ prompts.text({
242
+ message: 'Where should it be made?',
243
+ placeholder: DEFAULT_FOLDER,
244
+ defaultValue: DEFAULT_FOLDER,
245
+ validate: (value) => (value && notEmpty(resolve(value)) ? `${value} is not empty: a new folder, or an empty one` : undefined)
246
+ })
247
+ )
248
+ const starter =
249
+ flags.starter ??
250
+ (await answer(
251
+ prompts.select({
252
+ message: 'Which game should it start from?',
253
+ initialValue: DEFAULT_STARTER,
254
+ options: starters.map((starter) => ({ value: starter.name, label: starter.title, hint: starter.summary }))
255
+ })
256
+ ))
257
+ const suggested = titleOf(folder)
258
+ const title =
259
+ flags.name ??
260
+ (await answer(
261
+ prompts.text({
262
+ message: 'What is the game called? (players see it; you can change it later)',
263
+ placeholder: suggested,
264
+ defaultValue: suggested,
265
+ validate: (value) => (value ? titleProblem(value) : undefined)
266
+ })
267
+ ))
268
+ // a starter that cannot be made without the shared art has it, and is not asked
269
+ const needed = starterNamed(starter).needs?.includes('assets')
270
+ const parts = { editor: flags.editor, tests: flags.tests, assets: needed ? assetsOf(starter, flags.assets) : flags.assets }
271
+ const unsaid = Object.keys(PARTS).filter((part) => parts[part] === undefined)
272
+ if (unsaid.length) {
273
+ const chosen = await answer(
274
+ prompts.multiselect({
275
+ message: 'What should it have? (space to choose, enter when done)',
276
+ options: unsaid.map((part) => ({ value: part, label: part, hint: PARTS[part] })),
277
+ initialValues: unsaid,
278
+ required: false
279
+ })
280
+ )
281
+ for (const part of unsaid) parts[part] = chosen.includes(part)
282
+ }
283
+ const server =
284
+ flags.server ??
285
+ (await answer(
286
+ prompts.select({
287
+ message: 'A server of the game?',
288
+ initialValue: 'single',
289
+ options: Object.entries(SERVER_KINDS).map(([kind, what]) => ({ value: kind, label: kind, hint: what }))
290
+ })
291
+ ))
292
+ const ratings =
293
+ server === 'none'
294
+ ? false
295
+ : (flags.ratings ?? (await answer(prompts.confirm({ message: 'Should the server keep a rating for each player?', initialValue: false }))))
296
+ const where = resolve(folder)
297
+ const packageManager = flags.packageManager ?? packageManagerOf()
298
+ const git =
299
+ flags.git ??
300
+ (insideRepository(where)
301
+ ? false
302
+ : await answer(prompts.confirm({ message: 'Make it a git repository, with a first commit?', initialValue: true })))
303
+ const install =
304
+ flags.install ??
305
+ (await answer(prompts.confirm({ message: `Install its packages now (${commandsOf(packageManager).install})?`, initialValue: true })))
306
+ return {
307
+ where,
308
+ starter,
309
+ title,
310
+ editor: parts.editor,
311
+ server,
312
+ ratings,
313
+ tests: parts.tests,
314
+ assets: parts.assets,
315
+ source: flags.source ?? false,
316
+ git,
317
+ install,
318
+ packageManager
319
+ }
320
+ } catch (error) {
321
+ if (error !== CANCELLED) throw error
322
+ prompts.cancel('Nothing was made.')
323
+ return undefined
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Whether a project has the shared art: as it was told, on when it was not; a starter that plays with it cannot be
329
+ * made without (an error that says so)
330
+ */
331
+ function assetsOf(starter, asked) {
332
+ if (asked === false && starterNamed(starter).needs?.includes('assets'))
333
+ throw new InvalidArgumentError(`the ${starter} starter plays with the shared art (@v12-engine/assets): --no-assets cannot go with it`)
334
+ return asked ?? true
335
+ }
336
+
337
+ /** What a question that was cancelled throws, to stop asking */
338
+ const CANCELLED = Symbol('cancelled')
339
+
340
+ /**
341
+ * Whether a command is run through a shell: on Windows the package managers are `.cmd` files, which need one; git is
342
+ * an executable, and a shell would split its commit message at the spaces
343
+ */
344
+ export const needsShell = (command, platform = process.platform) => platform === 'win32' && command !== 'git'
345
+
346
+ /** Runs a command in a folder: its output shown (`shown`) or kept back. Throws when it fails */
347
+ function execIn(command, args, cwd, shown = false) {
348
+ return execFileSync(command, args, { cwd, stdio: shown ? 'inherit' : 'pipe', encoding: 'utf8', shell: needsShell(command) })
349
+ }
350
+
351
+ /** Whether a folder (or where it will be) is in a git repository already: a project there is not made one of its own */
352
+ function insideRepository(where, exec = execIn) {
353
+ let folder = where
354
+ while (!existsSync(folder)) folder = dirname(folder)
355
+ try {
356
+ return exec('git', ['rev-parse', '--is-inside-work-tree'], folder).trim() === 'true'
357
+ } catch {
358
+ return false
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Makes the project a repository with a first commit of everything. False, and said, where git is not there; a
364
+ * repository with nothing committed where git does not know who commits (its `user.name` and `user.email`)
365
+ */
366
+ function makeRepository(where, { exec, say, warn }) {
367
+ try {
368
+ exec('git', ['init', '--quiet'], where)
369
+ } catch {
370
+ warn('git is not there: the project is not a repository')
371
+ return false
372
+ }
373
+ try {
374
+ exec('git', ['add', '-A'], where)
375
+ exec('git', ['commit', '--quiet', '-m', 'A game to start from, made with npm create @v12-engine'], where)
376
+ say('A git repository, with a first commit')
377
+ } catch {
378
+ warn('A git repository, with nothing committed: git does not know who you are yet (git config user.name, user.email)')
379
+ }
380
+ return true
381
+ }