@v12-engine/create 0.0.0-stage → 0.1.1

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 +46 -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/index.mjs ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * A project to start from (S60): `npm create @v12-engine my-game` puts one together in `my-game` from layers (create
3
+ * N2), each a folder of files laid over the ones before:
4
+ *
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
7
+ * 3. `starters/<starter>/`: the game, its rules, look and maps, and its README
8
+ * 4. `starters/<starter>/layers/<option>/`: what the starter adds with an option on (its tests)
9
+ *
10
+ * A `package.json` of a layer is merged into the one before (scripts, dependencies), not laid over it. A line of
11
+ * `// @if <option>`, `<!-- @if <option> -->` or `# @if <option>` keeps what follows, to its `@end`, only with the option on (`!<option>`
12
+ * with it off, `a|b` with either on); the marker lines are left out. The command line (`cli.mjs`) asks or is told how, and the install test
13
+ * makes its project with this too (`scripts/install-test/run.mjs`), so what is made is what is tested
14
+ */
15
+ import { existsSync, readdirSync } from 'node:fs'
16
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
17
+ import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { DEFAULT_STARTER, starterNamed } from './starters.mjs'
20
+
21
+ export { DEFAULT_STARTER, starterNamed, starters } from './starters.mjs'
22
+
23
+ const here = fileURLToPath(new URL('.', import.meta.url))
24
+ /** What the packages of the library are installed as: the version of this one, as the three are versioned together */
25
+ const own = JSON.parse(await readFile(fileURLToPath(new URL('./package.json', import.meta.url)), 'utf8'))
26
+
27
+ /** What a package may be called (npm's rules): lower case, no spaces, nothing that leads out of a folder */
28
+ export function packageName(folder) {
29
+ const name = basename(resolve(folder))
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9._~-]+/g, '-')
32
+ .replace(/^[._-]+|-+$/g, '')
33
+ return name || 'my-game'
34
+ }
35
+
36
+ /**
37
+ * What a game is called where it is shown, from its folder's name: `orc-wars`, Orc wars; `Orc Wars`, Orc Wars. My game
38
+ * where the folder's name makes no name
39
+ */
40
+ export function titleOf(folder) {
41
+ const words = basename(resolve(folder))
42
+ .replace(/[-_.~\s]+/g, ' ')
43
+ .trim()
44
+ const title = words ? words[0].toUpperCase() + words.slice(1) : ''
45
+ return title && !titleProblem(title) ? title : 'My game'
46
+ }
47
+
48
+ /**
49
+ * Why a game cannot be called this, or nothing: the title is written into its code, its page and its README as it is,
50
+ * so it has no quotes, backslashes or angle brackets, and fits on a line
51
+ */
52
+ export function titleProblem(title) {
53
+ if (!title.trim()) return 'A game needs a name'
54
+ if (title.length > 60) return 'A name of 60 letters at most'
55
+ if (/["'`\\<>\n\r]/.test(title)) return 'A name without quotes, backslashes or angle brackets'
56
+ return undefined
57
+ }
58
+
59
+ /**
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
+ * `layers/`, and is a name its files' `@if` lines may ask about
62
+ */
63
+ export const LAYERS = ['editor', 'server', 'rooms', 'tests', 'assets', 'source']
64
+
65
+ /**
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
+ * (`ratings`; `single`, a server of one game at a time, where `server` is any server and `rooms` one of rooms;
68
+ * `generator`, the starter's, not the command line's: its random map made in a worker, `starters`)
69
+ */
70
+ export const OPTIONS = ['editor', 'server', 'single', 'rooms', 'ratings', 'tests', 'assets', 'source', 'generator']
71
+
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
+ export const SERVERS = ['single', 'rooms', 'none']
74
+
75
+ /** What may install and run a project: its words for that are `commandsOf`'s */
76
+ export const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun']
77
+
78
+ /** A package manager's words: what installs, and what runs a script of the project */
79
+ export function commandsOf(packageManager = 'npm') {
80
+ if (!PACKAGE_MANAGERS.includes(packageManager))
81
+ throw new Error(`There is no package manager "${packageManager}": they are ${PACKAGE_MANAGERS.join(', ')}`)
82
+ const install = packageManager === 'yarn' ? 'yarn' : `${packageManager} install`
83
+ const run = (script) =>
84
+ packageManager === 'npm'
85
+ ? script === 'test'
86
+ ? 'npm test'
87
+ : `npm run ${script}`
88
+ : packageManager === 'bun'
89
+ ? `bun run ${script}`
90
+ : `${packageManager} ${script}`
91
+ return { install, run }
92
+ }
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 */
95
+ const TEXT = new Set(['.ts', '.mjs', '.html', '.md', '.json', '.css'])
96
+ const isText = (path) => TEXT.has(extname(path)) || basename(path) === '_gitignore'
97
+
98
+ /** The order of a project's scripts, as one reads them: the rest after, as they came */
99
+ const SCRIPTS = ['dev', 'build', 'preview', 'server', 'test', 'typecheck']
100
+
101
+ /**
102
+ * Makes a project in `folder`, which is to be empty or not there:
103
+ *
104
+ * - `starter`: the game it starts from (`starters`), `DEFAULT_STARTER` (`empty`) when left out, as on the command line
105
+ * - `title`: what the game is called where it is shown, made from the folder's name when left out (`orc-wars`, Orc wars)
106
+ * - `editor`: its map editor (`/?editor`, `src/editor.ts`, the dev server's plugin); on when left out
107
+ * - `server`: a server of the game: `single`, one game at a time (`server.ts`, `/?server`); `rooms`, a room for each
108
+ * map, games players make and matchmaking (`/?find-game`, Find a game); or `none`. `single` when left out
109
+ * - `ratings`: the server keeps a rating for each player (`ratings.json`); off when left out, and with no server
110
+ * - `tests`: a test of its rules (`tests/`, Vitest); on when left out
111
+ * - `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
+ * copied into a build as the game uses it (`assetsPackage`); on when left out, and always for a starter that plays
113
+ * with it (`needs`)
114
+ * - `packageManager`: what installs and runs it (`PACKAGE_MANAGERS`): its README and comments say its words; `npm`
115
+ * when left out
116
+ * - `source`: the library read as it is written, for a project inside the library's own repository (`apps/`): its
117
+ * configs name the condition the packages' `exports` give their source under, with the plugins the source needs,
118
+ * and `{{root}}` in the layer's files is the way to the repository's root from the project. Off when left out
119
+ * - `dependencies`: written over the template's (the install test's packed packages, `file:./…`); the library's are
120
+ * `^` this package's version otherwise
121
+ *
122
+ * Nothing is written until the whole project is put together. Gives the project's name, its title, where it is, and
123
+ * the options it was made with
124
+ */
125
+ export async function createProject(folder, options = {}) {
126
+ const {
127
+ starter = DEFAULT_STARTER,
128
+ editor = true,
129
+ server = 'single',
130
+ tests = true,
131
+ source = false,
132
+ packageManager = 'npm',
133
+ dependencies = {}
134
+ } = options
135
+ const ratings = !!options.ratings && server !== 'none'
136
+ const words = commandsOf(packageManager)
137
+ const needs = starterNamed(starter).needs ?? []
138
+ if (options.assets === false && needs.includes('assets'))
139
+ throw new Error(`The ${starter} starter plays with the shared art (@v12-engine/assets): it cannot be left out`)
140
+ const assets = options.assets ?? true
141
+ if (!SERVERS.includes(server)) throw new Error(`There is no server "${server}": a server is ${SERVERS.join(', ')}`)
142
+ const where = resolve(folder)
143
+ if (notEmpty(where)) throw new Error(`${where} is not empty: a project is made in an empty folder, or a new one`)
144
+ const name = packageName(where)
145
+ const title = options.title ?? titleOf(where)
146
+ const problem = titleProblem(title)
147
+ if (problem) throw new Error(`"${title}": ${problem}`)
148
+ const generator = !!starterNamed(starter).generator
149
+ const on = { editor, server: server !== 'none', single: server === 'single', rooms: server === 'rooms', ratings, tests, assets, source, generator }
150
+ // the way from the project to the repository's root (`{{root}}` in the source layer): this package is `packages/create` of it
151
+ const root = relative(where, resolve(here, '../..')).split(sep).join('/') || '.'
152
+ const files = await putTogether(starter, on)
153
+ const project = JSON.parse(files.get('package.json'))
154
+ project.name = name
155
+ // the map editor's package only in a project with the editor: one without it installs none of it
156
+ for (const library of [
157
+ '@v12-engine/engine',
158
+ '@v12-engine/client',
159
+ ...(editor ? ['@v12-engine/editor'] : []),
160
+ ...(assets ? ['@v12-engine/assets'] : [])
161
+ ])
162
+ project.dependencies[library] = `^${own.version}`
163
+ // the `v12` command's package too, a dev dependency of a project with the editor: the five are versioned as one
164
+ if (project.devDependencies?.['@v12-engine/create']) project.devDependencies['@v12-engine/create'] = `^${own.version}`
165
+ // what is given for the library's packages stands in for those the project has (the install test's packed ones),
166
+ // a dev dependency (the `v12` command's package) among them
167
+ for (const [dependency, version] of Object.entries(dependencies)) {
168
+ if (project.devDependencies && dependency in project.devDependencies) project.devDependencies[dependency] = version
169
+ else if (!dependency.startsWith('@v12-engine/') || dependency in project.dependencies) project.dependencies[dependency] = version
170
+ }
171
+ files.set('package.json', `${JSON.stringify(project, null, '\t')}\n`)
172
+ for (const [path, content] of files) {
173
+ const file = join(where, path === '_gitignore' ? '.gitignore' : path)
174
+ await mkdir(dirname(file), { recursive: true })
175
+ await writeFile(
176
+ file,
177
+ typeof content === 'string'
178
+ ? said(
179
+ named(content, name, title).replaceAll('{{root}}', () => root),
180
+ words,
181
+ path
182
+ )
183
+ : content
184
+ )
185
+ }
186
+ return { name, title, where, starter, editor, server, ratings, tests, assets, source, packageManager }
187
+ }
188
+
189
+ /**
190
+ * The files of a project of `starter` with the options `on` (by name, true or false), by their path in it: text with
191
+ * its `@if` lines kept or left out, the rest as bytes. Throws for a marker it cannot read, wherever it is
192
+ */
193
+ export async function putTogether(starter, on) {
194
+ const folders = [
195
+ join(here, 'base'),
196
+ ...LAYERS.filter((layer) => on[layer]).map((layer) => join(here, 'layers', layer)),
197
+ join(here, 'starters', starter),
198
+ ...LAYERS.filter((layer) => on[layer]).map((layer) => join(here, 'starters', starter, 'layers', layer))
199
+ ]
200
+ const files = new Map()
201
+ for (const folder of folders) {
202
+ if (!existsSync(folder)) continue
203
+ for (const entry of await readdir(folder, { recursive: true, withFileTypes: true })) {
204
+ if (!entry.isFile()) continue
205
+ const path = relative(folder, join(entry.parentPath, entry.name)).split('\\').join('/')
206
+ // a starter's own layers are laid after it, each when its option is on; its list of the files its page asks
207
+ // for (`files.txt`, which the tests read) is no part of a game
208
+ if (folder.endsWith(join('starters', starter)) && (path.startsWith('layers/') || path === 'files.txt')) continue
209
+ const bytes = await readFile(join(entry.parentPath, entry.name))
210
+ if (!isText(path)) {
211
+ files.set(path, bytes)
212
+ continue
213
+ }
214
+ const text = kept(bytes.toString('utf8'), on, path)
215
+ // a file all of whose lines are for options off is not there: the Vite config of a project with neither the
216
+ // editor nor the shared art
217
+ if (!text.trim()) {
218
+ files.delete(path)
219
+ continue
220
+ }
221
+ files.set(path, basename(path) === 'package.json' && files.has(path) ? merged(files.get(path), text) : text)
222
+ }
223
+ }
224
+ return files
225
+ }
226
+
227
+ /** `text` with what its `@if` lines keep for the options `on`, without them; `path` is for what an error says */
228
+ export function kept(text, on, path = 'a file') {
229
+ const marker = /^\s*(?:\/\/|<!--|#)\s*@(if|end)\b\s*(!?)([\w|-]*)\s*(?:-->)?\s*$/
230
+ const open = []
231
+ const lines = []
232
+ text.split('\n').forEach((line, index) => {
233
+ const found = marker.exec(line)
234
+ if (!found) {
235
+ if (open.every(Boolean)) lines.push(line)
236
+ return
237
+ }
238
+ const [, word, not, asked] = found
239
+ if (word === 'end') {
240
+ if (!open.length) throw new Error(`${path}:${index + 1}: an @end with no @if`)
241
+ open.pop()
242
+ return
243
+ }
244
+ // `@if editor|server`: any of them on
245
+ const options = asked.split('|')
246
+ for (const option of options) {
247
+ if (!(option in on)) throw new Error(`${path}:${index + 1}: @if ${option}, an option there is not (${Object.keys(on).join(', ')})`)
248
+ }
249
+ const any = options.some((option) => on[option])
250
+ open.push(not ? !any : any)
251
+ })
252
+ if (open.length) throw new Error(`${path}: an @if with no @end`)
253
+ return lines.join('\n')
254
+ }
255
+
256
+ /** A layer's `package.json` merged into the one before: its scripts and dependencies added, in the order one reads them */
257
+ function merged(before, layer) {
258
+ const project = JSON.parse(before)
259
+ for (const [key, value] of Object.entries(JSON.parse(layer))) {
260
+ project[key] = value && typeof value === 'object' && !Array.isArray(value) ? { ...project[key], ...value } : value
261
+ }
262
+ const order = (key) => (SCRIPTS.includes(key) ? SCRIPTS.indexOf(key) : SCRIPTS.length)
263
+ if (project.scripts) project.scripts = Object.fromEntries(Object.entries(project.scripts).sort(([a], [b]) => order(a) - order(b)))
264
+ for (const key of ['dependencies', 'devDependencies']) {
265
+ if (project[key]) project[key] = Object.fromEntries(Object.entries(project[key]).sort(([a], [b]) => a.localeCompare(b)))
266
+ }
267
+ return `${JSON.stringify(project, null, '\t')}\n`
268
+ }
269
+
270
+ /** The starters' game is `my-game`, My game: the project's is its own, in its code, its page, its README and its maps */
271
+ const named = (text, name, title) => text.replaceAll('my-game', () => name).replaceAll('My game', () => title)
272
+
273
+ /** Folders git or the system leaves in a folder that is still new: a project is made beside them */
274
+ const LEFT_BY_TOOLS = new Set(['.git', '.DS_Store', 'Thumbs.db'])
275
+
276
+ /** Whether there is something in a folder: a project is made in an empty one (a fresh `git init` is empty) */
277
+ export const notEmpty = (where) => existsSync(where) && readdirSync(where).some((entry) => !LEFT_BY_TOOLS.has(entry))
278
+
279
+ /** The starters say npm's words (`npm run dev`, `npm test`): the project says its package manager's, but in its `package.json` */
280
+ const said = (text, words, path) =>
281
+ basename(path) === 'package.json'
282
+ ? text
283
+ : text
284
+ .replace(/\bnpm run ([\w:-]+)/g, (_, script) => words.run(script))
285
+ .replace(/\bnpm test\b/g, words.run('test'))
286
+ .replace(/\bnpm install\b/g, words.install)
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "@v12-engine/assets": "^0.0.0"
4
+ }
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "scripts": {
3
+ "map": "v12 map"
4
+ },
5
+ "devDependencies": {
6
+ "@v12-engine/create": "^0.0.0"
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The map editor of the game, a scene of the app (Map editor on the start screen, or `/?editor`) that `appScenes` makes
3
+ * (`src/main.ts`) with the editor's `editorScene`: it opens on the game's first map, as a file of the project that Save
4
+ * writes back, and Play here tries it in the page's own game. What is set here is the editor's own: what a game adds to
5
+ * it (`extensions`, made with `defineEditorExtension` of `@v12-engine/editor`), another map to open on (`startMap`,
6
+ * `startFile`)
7
+ */
8
+ import type { AppEditorScene } from '@v12-engine/client/app'
9
+ import '@v12-engine/editor/styles.css'
10
+
11
+ // what the app's scenes say of it (its id, the game, the map it starts on, Play here's scene), and the game's own after.
12
+ // The editor fetched as it first opens: the start screen carries none of it
13
+ export const editor = (scene: AppEditorScene) => import('@v12-engine/editor').then(({ editorScene }) => editorScene({ ...scene }))
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A server of many games of the game (`npm run server`), in rooms that share its port. The page finds them at
3
+ * `/?find-game` (`npm run dev` beside it): the games to join or watch, a New game of any map, and Play now to be matched
4
+ * with someone. `PORT` is the port, 8787 when not said
5
+ */
6
+ import { GameRooms, generatedMode, mapMode, type GameMode, type RoomKind } from '@v12-engine/engine/server'
7
+ // @if ratings
8
+ import { createRatings } from '@v12-engine/engine/server'
9
+ // @end
10
+ import { DEFAULT_MAP, mapNamed, maps } from './src/maps'
11
+ import { rules, start } from './src/rules'
12
+
13
+ /** A game on one of the game's maps, each world started as the page starts it */
14
+ const onMap = (name: string): GameMode => mapMode(rules, mapNamed(name), { start })
15
+
16
+ /**
17
+ * A game on a map the game's generator makes (`rules.generator`): another for each game made, of 128 cells a side for
18
+ * two, turned half about the middle so it is fair; the seed a New game asks for (`settings.seed`) where it says one
19
+ */
20
+ const random: Record<string, RoomKind> = rules.generator
21
+ ? {
22
+ random: {
23
+ name: 'A random map',
24
+ room: (settings) => ({
25
+ mode: generatedMode(rules, rules.generator!, {
26
+ width: 128,
27
+ depth: 128,
28
+ players: 2,
29
+ symmetry: 'half',
30
+ ...(Number(settings.seed) > 0 && { seed: Number(settings.seed) }),
31
+ start
32
+ })
33
+ })
34
+ }
35
+ }
36
+ : {}
37
+
38
+ // @if ratings
39
+ // a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
40
+ const ratings = createRatings({ file: 'ratings.json' })
41
+ // a file that cannot be read stops the server here, rather than being written over
42
+ await ratings.ready
43
+ // @end
44
+ // the seats the computer plays in the room to try the game alone in: every player of the first map but the first
45
+ const computer = mapNamed()
46
+ .players.slice(1)
47
+ .map((player) => player.id)
48
+ const rooms = await GameRooms.start({
49
+ port: Number(process.env.PORT ?? 8787),
50
+ rooms: {
51
+ // a room for each map, for people: ws://host:8787/<map>. Its game starts once every seat is taken
52
+ ...Object.fromEntries(Object.keys(maps).map((name) => [name, { mode: onMap(name) }])),
53
+ // and one to try the game alone in
54
+ practice: { mode: onMap(DEFAULT_MAP), ai: computer }
55
+ },
56
+ // what players make games of (New game), each closed once nobody has been in it for a minute; and are matched for
57
+ // (Play now), once as many wait as it has seats
58
+ kinds: { ...Object.fromEntries(Object.entries(maps).map(([name, map]) => [name, { name: map.name, room: { mode: onMap(name) } }])), ...random },
59
+ // @if ratings
60
+ ratings,
61
+ // @end
62
+ log: (line) => console.log(line)
63
+ })
64
+ console.log(`The game's server is at ws://localhost:${rooms.port}: open the page at /?find-game to find a game on it`)
65
+
66
+ // stopped (Ctrl+C, or SIGTERM where it is hosted): no new games, and those running have 30 seconds to end
67
+ const stop = () => void rooms.close({ drain: 30 }).then(() => process.exit(0))
68
+ process.once('SIGINT', stop)
69
+ process.once('SIGTERM', stop)
@@ -0,0 +1,8 @@
1
+ {
2
+ "scripts": {
3
+ "server": "tsx server.ts"
4
+ },
5
+ "devDependencies": {
6
+ "tsx": "^4.23.15"
7
+ }
8
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A server of the game (`npm run server`): each game on the first map, the first player's seat for whoever joins and
3
+ * the rest played by the computer. The page joins it at `/?server` (`npm run dev` beside it). `PORT` is the port,
4
+ * 8787 when not said
5
+ */
6
+ import { startServer } from '@v12-engine/engine/server'
7
+ // @if ratings
8
+ import { createRatings } from '@v12-engine/engine/server'
9
+ // @end
10
+ import { mapNamed } from './src/maps'
11
+ import { rules, start } from './src/rules'
12
+
13
+ const map = mapNamed()
14
+ // @if ratings
15
+ // a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
16
+ const ratings = createRatings({ file: 'ratings.json' })
17
+ // a file that cannot be read stops the server here, rather than being written over
18
+ await ratings.ready
19
+ // @end
20
+ const server = await startServer({
21
+ game: rules,
22
+ map,
23
+ start,
24
+ // the seats the computer plays: leave it out for a game of people alone, which starts once every seat is taken
25
+ ai: map.players.slice(1).map((player) => player.id),
26
+ // @if ratings
27
+ ratings,
28
+ // @end
29
+ port: Number(process.env.PORT ?? 8787)
30
+ })
31
+ console.log(`The game's server is at ws://localhost:${server.port}: open the page at /?server to play on it`)
32
+
33
+ // stopped (Ctrl+C), it closes its connections first
34
+ process.once('SIGINT', () => void server.close().then(() => process.exit(0)))
@@ -0,0 +1,12 @@
1
+ {
2
+ "scripts": {
3
+ // @if server
4
+ "server": "tsx --conditions=@v12-engine/source server.ts"
5
+ // @end
6
+ },
7
+ "devDependencies": {
8
+ "@tailwindcss/vite": "^4.3.3",
9
+ "tailwindcss": "^4.3.3",
10
+ "vite-plugin-solid": "^2.11.14"
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "{{root}}/tsconfig.base.json",
3
+ "compilerOptions": { "types": ["vite/client", "node"], "resolveJsonModule": true },
4
+ "include": ["src", "tests", "server.ts", "vite.config.ts"]
5
+ }
@@ -0,0 +1,45 @@
1
+ import solid from 'vite-plugin-solid'
2
+ import tailwindcss from '@tailwindcss/vite'
3
+ import { defineConfig } from 'vite'
4
+ // @if tests
5
+ // the `test` key of the config
6
+ import type {} from 'vitest/config'
7
+ // @end
8
+ // what every config of the library's repository says alike: its packages read as they are written
9
+ import { sourceLibrary } from '{{root}}/config/vite.ts'
10
+ // @if editor
11
+ // the editor's plugin as it is written, not as it is built (`@v12-engine/editor/vite`): nothing of the library is built here
12
+ import { editorFiles } from '{{root}}/packages/editor/plugin/editorFiles.ts'
13
+ // @end
14
+ // @if assets
15
+ import { assetsPackage } from '{{root}}/packages/client/plugin/assetsPackage.ts'
16
+ // @end
17
+
18
+ // The library as it is written: a change to the engine or the client shows here at once, and nothing is built. Solid's
19
+ // plugin and Tailwind's are here because the client's source is read (its components, its stylesheet): a project that
20
+ // installs the packages needs neither.
21
+ // @if assets
22
+ // The shared art (@v12-engine/assets) is served where the game's shared files are (/models/…),
23
+ // behind public/: a file of yours at the same path is the one served. A build copies what the game uses of it
24
+ // @end
25
+ // @if editor
26
+ // The map editor keeps the game's maps in the project through the dev server: Save writes them into `src/maps`. Only
27
+ // in `npm run dev`; a built page keeps maps in the browser
28
+ // @end
29
+ export default defineConfig({
30
+ plugins: [
31
+ !process.env.VITEST && solid(),
32
+ tailwindcss(),
33
+ // @if assets
34
+ assetsPackage(),
35
+ // @end
36
+ // @if editor
37
+ editorFiles({ gameFolder: () => 'src' })
38
+ // @end
39
+ ],
40
+ // the packages' source on the page, on the dev server's own side and in the tests (the library's `config/vite.ts`)
41
+ ...sourceLibrary(),
42
+ // @if tests
43
+ test: { environment: 'node' }
44
+ // @end
45
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "scripts": {
3
+ "test": "vitest run"
4
+ },
5
+ "devDependencies": {
6
+ "vitest": "^5.0.5"
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
- "name": "@v12-engine/create",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@v12-engine/create",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/DoronTorangy/V12.git",
11
+ "directory": "packages/create"
12
+ },
13
+ "type": "module",
14
+ "description": "A project to start a game from: npm create @v12-engine my-game",
15
+ "bin": {
16
+ "create": "./bin.mjs",
17
+ "create-v12": "./bin.mjs",
18
+ "v12": "./v12.mjs"
19
+ },
20
+ "exports": {
21
+ ".": "./index.mjs"
22
+ },
23
+ "files": [
24
+ "bin.mjs",
25
+ "v12.mjs",
26
+ "cli.mjs",
27
+ "index.mjs",
28
+ "starters.mjs",
29
+ "base",
30
+ "layers",
31
+ "starters"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22.12"
35
+ },
36
+ "scripts": {
37
+ "test": "vitest run --dir tests",
38
+ "typecheck": "tsc --noEmit && node scripts/typecheckStarters.mjs"
39
+ },
40
+ "devDependencies": {
41
+ "vitest": "^5.0.5"
42
+ },
43
+ "dependencies": {
44
+ "@clack/prompts": "^1.8.1",
45
+ "commander": "^15.0.0"
46
+ }
47
+ }
@@ -0,0 +1,61 @@
1
+ # My game
2
+
3
+ A campaign made with V12: three missions on the long road home, a hero carried from one to the next. Burn the Red Hand's camp, hold the fort through a night of waves, and climb the pass to Varga's hold.
4
+
5
+ | To | Run |
6
+ | ------- | -------------------------------------------------------------------- |
7
+ | Play it | `npm run dev`, and open the page it says: its start screen, Campaign |
8
+ <!-- @if editor -->
9
+ | Make maps | `npm run dev`, and open `/?editor`: Save writes them into `src/maps`, with a names file each |
10
+ | Check the maps | `npm run map check`: every map in `src/maps` against the game's rules |
11
+ <!-- @end -->
12
+ <!-- @if server -->
13
+ <!-- @if !rooms -->
14
+ | Play it on a server | `npm run server`, beside `npm run dev`, and open `/?server` |
15
+ <!-- @end -->
16
+ <!-- @if rooms -->
17
+ | Play it online | `npm run server`, beside `npm run dev`, and open `/?find-game` |
18
+ <!-- @end -->
19
+ <!-- @end -->
20
+ <!-- @if tests -->
21
+ | Test the rules | `npm test` |
22
+ <!-- @end -->
23
+ | Check the types | `npm run typecheck` |
24
+ | Build the page to host | `npm run build`, into `dist/` |
25
+
26
+ ## What is where
27
+
28
+ - `src/campaign/`: the game. `index.ts` puts the rules together and gives each map its mission; `ids.ts` names everything once; `units.ts` the units, buildings, the hero, his abilities and the items; `missions/` a file a mission: `homecoming.ts` (objectives, a prefab, areas, a brain that holds and raids), `siege.ts` (the waves, the gate, effects fired from the rules), `warlord.ts` (the hero carried in, a shop, the talk at the gate and its choice, the prisoners); `waves.ts` the waves, as `waves.json` says them; `view/` how it looks and sounds, with the cinematic of the talk in code; `cinematics/` the intro and the outro, made in the editor's Cinematic panel; `prefabs/` what the editor's Save as a prefab wrote; `campaign.json` the campaign's chapters and missions (their maps, words, briefings, debriefs), what the editor's Campaign panel writes, and `scenes.ts` what of them is code, for the page (`defineCampaign`); `data.json`, `looks.json`, `fx.json` and `waves.json` what the editor's Data, model, Effects and Waves panels write
29
+ - `src/rules.ts`, `src/game.ts`, `src/maps.ts`: the game by the names the page, the server and the tests know it by
30
+ - `src/maps/`: the missions' maps, made in the editor. Each has the names its mission's rules look for (`hall`, `hero`, `camp`, `warcamp`, `gate`, `cells`…): a names file beside each says which
31
+ - `src/main.ts`: the page: the start screen, the campaign's menu and its missions, free play, the settings
32
+
33
+ <!-- @if editor -->
34
+
35
+ - `src/editor.ts`: the map editor, with what `src/campaign-editor.ts` adds as one extension: the Waves panel (`src/waves-panel.ts`: the waves listed, found, changed and undone, where they come out drawn in the scene) and the search's entries. Saved while the editor is open, they change in it
36
+ <!-- @end -->
37
+ <!-- @if server -->
38
+ <!-- @if !rooms -->
39
+
40
+ - `server.ts`: a server of the game, one game at a time, `startServer`: the first mission's map, the computer at the Red Hand's seat
41
+ <!-- @end -->
42
+ <!-- @if rooms -->
43
+
44
+ - `server.ts`: a server of many games, `GameRooms`: a room for each map, games players make (New game) and are matched for (Play now). The page's `/?find-game` is the client's Find a game
45
+ <!-- @end -->
46
+ <!-- @end -->
47
+ <!-- @if tests -->
48
+
49
+ - `tests/`: the missions tested with no page: the maps checked, each mission won and lost as it says, the waves, the carried hero, the choice at the gate, the computer playing a minute (`simulate`)
50
+ <!-- @end -->
51
+ - `public/games/campaign/`: the game's own folder of files: what the editor's Assets panel takes in (models, sounds, pictures), with an `assets.json` saying what is there
52
+ - `@v12-engine/assets`: the shared art every unit and building is drawn with (adventurers with their clips and their gear, buildings, trees), served where `public/`'s files are by `assetsPackage()` in `vite.config.ts`, and copied into a build as the game uses it. Whose each file is: its group's `CREDITS.md` in `node_modules/@v12-engine/assets/groups/`
53
+
54
+ ## Where to go from here
55
+
56
+ The tutorial that ends in this project, chapter by chapter, is the docs' Let's make a basic campaign. Then:
57
+
58
+ 1. **A fourth mission.** A map in the editor (names for what the rules look for), a file in `src/campaign/missions/`, the mission in the Campaign panel (`campaign.json`), its `start` in `scenes.ts` and a line in `index.ts`'s `MISSIONS`.
59
+ 2. **A second ability of the hero's own.** `defineEffect` in `units.ts` for what the engine's effects do not do, and a line in `hero.learns`. The docs' Let's add a hero with abilities and items walks through it.
60
+ 3. **More for the waves.** `waves.json` in the editor's Waves panel; a new kind of raider in `units.ts` and in `WAVE_UNITS`.
61
+ 4. **Your own art.** Drop a glTF on the editor's Assets panel: Set up as a unit writes `looks.json`, which the view lays over its code.