gamedev 0.1.4 → 0.1.5

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.
@@ -365,6 +365,12 @@ export function scaffoldBaseProject({
365
365
  report,
366
366
  })
367
367
 
368
+ writeFileWithPolicy(path.join(rootDir, 'tmp', '.gitkeep'), '', {
369
+ force,
370
+ writeFile,
371
+ report,
372
+ })
373
+
368
374
  if (fs.existsSync(README_TEMPLATE)) {
369
375
  const readmeContent = readText(README_TEMPLATE)
370
376
  if (readmeContent != null) {
@@ -9,6 +9,22 @@ Apps live in `apps/` and each app folder contains blueprint JSON plus a script e
9
9
  Local-first (project files):
10
10
 
11
11
  - Run `npm run apps:new <AppName>` (creates `apps/<AppName>/` with `index.js` + blueprint)
12
+ - Batch-edit `world.json` entities: `npm run world:entities -- add --template-id <id> --transforms <file>` / `npm run world:entities -- delete --blueprint <name>`
13
+ - To duplicate one app many times (eg, make a forest from one `Tree`), generate many transforms and run `npm run world:entities -- add --template-id <TreeEntityId> --transforms tmp/forest.json --yes`, then delete `tmp/forest.json`
14
+ - See `docs/world-entities-cli.md` for full flags (`--replace`, `--ids`, `--yes`, `--world`)
15
+ - Transform file format for `--transforms` (JSON array):
16
+ ```json
17
+ [
18
+ {
19
+ "position": [0, 0, 0],
20
+ "quaternion": [0, 0, 0, 1],
21
+ "scale": [1, 1, 1],
22
+ "pinned": false,
23
+ "props": {},
24
+ "state": {}
25
+ }
26
+ ]
27
+ ```
12
28
  - Put assets in top-level `assets/` and reference them from blueprint JSON for app.config props
13
29
  - Run `npm run dev` for hot reload
14
30
 
@@ -9,6 +9,22 @@ Apps live in `apps/` and each app folder contains blueprint JSON plus a script e
9
9
  Local-first (project files):
10
10
 
11
11
  - Run `npm run apps:new <AppName>` (creates `apps/<AppName>/` with `index.js` + blueprint)
12
+ - Batch-edit `world.json` entities: `npm run world:entities -- add --template-id <id> --transforms <file>` / `npm run world:entities -- delete --blueprint <name>`
13
+ - To duplicate one app many times (eg, make a forest from one `Tree`), generate many transforms and run `npm run world:entities -- add --template-id <TreeEntityId> --transforms tmp/forest.json --yes`, then delete `tmp/forest.json`
14
+ - See `docs/world-entities-cli.md` for full flags (`--replace`, `--ids`, `--yes`, `--world`)
15
+ - Transform file format for `--transforms` (JSON array):
16
+ ```json
17
+ [
18
+ {
19
+ "position": [0, 0, 0],
20
+ "quaternion": [0, 0, 0, 1],
21
+ "scale": [1, 1, 1],
22
+ "pinned": false,
23
+ "props": {},
24
+ "state": {}
25
+ }
26
+ ]
27
+ ```
12
28
  - Put assets in top-level `assets/` and reference them from blueprint JSON for app.config props
13
29
  - Run `npm run dev` for hot reload
14
30
 
@@ -9,6 +9,22 @@ Apps live in `apps/` and each app folder contains blueprint JSON plus a script e
9
9
  Local-first (project files):
10
10
 
11
11
  - Run `npm run apps:new <AppName>` (creates `apps/<AppName>/` with `index.js` + blueprint)
12
+ - Batch-edit `world.json` entities: `npm run world:entities -- add --template-id <id> --transforms <file>` / `npm run world:entities -- delete --blueprint <name>`
13
+ - To duplicate one app many times (eg, make a forest from one `Tree`), generate many transforms and run `npm run world:entities -- add --template-id <TreeEntityId> --transforms tmp/forest.json --yes`, then delete `tmp/forest.json`
14
+ - See `docs/world-entities-cli.md` for full flags (`--replace`, `--ids`, `--yes`, `--world`)
15
+ - Transform file format for `--transforms` (JSON array):
16
+ ```json
17
+ [
18
+ {
19
+ "position": [0, 0, 0],
20
+ "quaternion": [0, 0, 0, 1],
21
+ "scale": [1, 1, 1],
22
+ "pinned": false,
23
+ "props": {},
24
+ "state": {}
25
+ }
26
+ ]
27
+ ```
12
28
  - Put assets in top-level `assets/` and reference them from blueprint JSON for app.config props
13
29
  - Run `npm run dev` for hot reload
14
30
 
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+ import { randomBytes } from 'node:crypto'
3
+ import { promises as fs } from 'node:fs'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+ import readline from 'node:readline/promises'
7
+
8
+ const usage = `Usage:
9
+ npm run world:entities -- add --template-id <entityId> --transforms <path> [--world <path>] [--replace] [--yes]
10
+ npm run world:entities -- delete --blueprint <name> [--world <path>] [--yes]
11
+ npm run world:entities -- delete --ids <path> [--world <path>] [--yes]
12
+
13
+ Commands:
14
+ add Create entity clones from a template entity and a transform array.
15
+ delete Delete entities by blueprint name or explicit ID list.
16
+
17
+ Flags:
18
+ --transforms <path> (add only) JSON array file. Recommended: tmp/<name>.json (delete after run)
19
+ --ids <path> (delete only) JSON array of entity IDs. Recommended: tmp/<name>.json (delete after run)
20
+ --world <path> Path to world JSON file (default: world.json)
21
+ --yes Skip confirmations and allow no-op operations
22
+ --replace (add only) Delete existing entities using template blueprint before adding
23
+ `
24
+
25
+ const ID_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
26
+ const ID_LENGTH = 10
27
+
28
+ main().catch(error => {
29
+ const message = error instanceof Error ? error.message : String(error)
30
+ console.error(`Error: ${message}`)
31
+ process.exitCode = 1
32
+ })
33
+
34
+ async function main() {
35
+ const argv = process.argv.slice(2)
36
+
37
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
38
+ console.log(usage.trimEnd())
39
+ return
40
+ }
41
+
42
+ const { command, options, extraArgs } = parseArgs(argv)
43
+
44
+ if (extraArgs.length > 0) {
45
+ throw new Error(`Unexpected positional arguments: ${extraArgs.join(' ')}`)
46
+ }
47
+
48
+ const worldPath = path.resolve(process.cwd(), getStringOption(options, 'world', 'world.json'))
49
+ const yes = Boolean(options.yes)
50
+
51
+ const world = await readWorld(worldPath)
52
+
53
+ if (command === 'add') {
54
+ await runAdd({ worldPath, world, options, yes })
55
+ return
56
+ }
57
+
58
+ if (command === 'delete') {
59
+ await runDelete({ worldPath, world, options, yes })
60
+ return
61
+ }
62
+
63
+ throw new Error(`Unknown command: ${command}`)
64
+ }
65
+
66
+ function parseArgs(argv) {
67
+ const [command, ...rest] = argv
68
+ const options = {}
69
+ const extraArgs = []
70
+
71
+ for (let i = 0; i < rest.length; i += 1) {
72
+ const token = rest[i]
73
+
74
+ if (!token.startsWith('--')) {
75
+ extraArgs.push(token)
76
+ continue
77
+ }
78
+
79
+ const key = token.slice(2)
80
+ if (!key) {
81
+ throw new Error(`Invalid flag: ${token}`)
82
+ }
83
+
84
+ const maybeValue = rest[i + 1]
85
+ if (!maybeValue || maybeValue.startsWith('--')) {
86
+ options[key] = true
87
+ continue
88
+ }
89
+
90
+ options[key] = maybeValue
91
+ i += 1
92
+ }
93
+
94
+ return {
95
+ command,
96
+ options,
97
+ extraArgs,
98
+ }
99
+ }
100
+
101
+ async function runAdd({ worldPath, world, options, yes }) {
102
+ const templateId = getRequiredStringOption(options, 'template-id')
103
+ const transformsPath = path.resolve(process.cwd(), getRequiredStringOption(options, 'transforms'))
104
+ const replace = Boolean(options.replace)
105
+
106
+ const unsupported = ['blueprint', 'ids']
107
+ for (const flag of unsupported) {
108
+ if (options[flag] !== undefined) {
109
+ throw new Error(`--${flag} is not valid for the add command`)
110
+ }
111
+ }
112
+
113
+ const template = world.entities.find(entity => entity?.id === templateId)
114
+ if (!template) {
115
+ throw new Error(`Template entity not found: ${templateId}`)
116
+ }
117
+
118
+ if (typeof template.blueprint !== 'string' || template.blueprint.length === 0) {
119
+ throw new Error(`Template entity ${templateId} is missing a valid blueprint`)
120
+ }
121
+
122
+ const transforms = await readTransforms(transformsPath)
123
+
124
+ const baseEntities = replace
125
+ ? world.entities.filter(entity => entity?.blueprint !== template.blueprint)
126
+ : [...world.entities]
127
+
128
+ const existingIds = new Set(baseEntities.map(entity => entity.id))
129
+ const newEntities = []
130
+
131
+ for (let i = 0; i < transforms.length; i += 1) {
132
+ const transform = validateTransformItem(transforms[i], i)
133
+ const id = transform.id ?? generateUniqueId(existingIds)
134
+
135
+ if (existingIds.has(id)) {
136
+ throw new Error(`Transform #${i} id already exists in world.json: ${id}`)
137
+ }
138
+
139
+ existingIds.add(id)
140
+ newEntities.push(buildEntityFromTemplate(template, transform, id))
141
+ }
142
+
143
+ const removedCount = world.entities.length - baseEntities.length
144
+ const addedCount = newEntities.length
145
+
146
+ if (addedCount === 0 && removedCount === 0) {
147
+ if (!yes) {
148
+ throw new Error('No changes to apply. Pass --yes to acknowledge and continue.')
149
+ }
150
+ console.log('No changes applied (no-op acknowledged with --yes).')
151
+ return
152
+ }
153
+
154
+ if (replace) {
155
+ const confirmed = await confirmDangerousAction({
156
+ yes,
157
+ summary: `Replace mode will delete ${removedCount} existing \"${template.blueprint}\" entities, then add ${addedCount}.`,
158
+ })
159
+ if (!confirmed) {
160
+ console.log('Cancelled.')
161
+ return
162
+ }
163
+ }
164
+
165
+ world.entities = baseEntities.concat(newEntities)
166
+ await writeWorld(worldPath, world)
167
+
168
+ console.log(`Updated ${path.basename(worldPath)}`)
169
+ console.log(`Blueprint: ${template.blueprint}`)
170
+ console.log(`Removed: ${removedCount}`)
171
+ console.log(`Added: ${addedCount}`)
172
+ console.log(`Total entities: ${world.entities.length}`)
173
+ }
174
+
175
+ async function runDelete({ worldPath, world, options, yes }) {
176
+ const byBlueprint = options.blueprint
177
+ const idsPath = options.ids
178
+
179
+ if (options['template-id'] !== undefined || options.transforms !== undefined || options.replace !== undefined) {
180
+ throw new Error('--template-id, --transforms, and --replace are only valid for add')
181
+ }
182
+
183
+ if ((byBlueprint && idsPath) || (!byBlueprint && !idsPath)) {
184
+ throw new Error('Delete requires exactly one of --blueprint <name> or --ids <path>')
185
+ }
186
+
187
+ let keepEntities = []
188
+ let removedCount = 0
189
+ let missingIds = []
190
+ let summary = ''
191
+
192
+ if (byBlueprint) {
193
+ const blueprint = getStringOption(options, 'blueprint')
194
+ keepEntities = world.entities.filter(entity => entity?.blueprint !== blueprint)
195
+ removedCount = world.entities.length - keepEntities.length
196
+ summary = `Delete ${removedCount} entities with blueprint \"${blueprint}\".`
197
+ } else {
198
+ const absoluteIdsPath = path.resolve(process.cwd(), getStringOption(options, 'ids'))
199
+ const ids = await readIdArray(absoluteIdsPath)
200
+ const requested = new Set(ids)
201
+ const found = new Set()
202
+
203
+ keepEntities = world.entities.filter(entity => {
204
+ if (requested.has(entity.id)) {
205
+ found.add(entity.id)
206
+ return false
207
+ }
208
+ return true
209
+ })
210
+
211
+ removedCount = world.entities.length - keepEntities.length
212
+ missingIds = [...requested].filter(id => !found.has(id))
213
+ summary = `Delete ${removedCount} entities by explicit ID list (${ids.length} requested).`
214
+ }
215
+
216
+ if (removedCount === 0) {
217
+ if (!yes) {
218
+ throw new Error('No matching entities to delete. Pass --yes to acknowledge and continue.')
219
+ }
220
+ console.log('No changes applied (no-op acknowledged with --yes).')
221
+ if (missingIds.length > 0) {
222
+ console.log(`Missing IDs: ${missingIds.length}`)
223
+ }
224
+ return
225
+ }
226
+
227
+ const confirmed = await confirmDangerousAction({ yes, summary })
228
+ if (!confirmed) {
229
+ console.log('Cancelled.')
230
+ return
231
+ }
232
+
233
+ world.entities = keepEntities
234
+ await writeWorld(worldPath, world)
235
+
236
+ console.log(`Updated ${path.basename(worldPath)}`)
237
+ console.log(`Removed: ${removedCount}`)
238
+ console.log(`Total entities: ${world.entities.length}`)
239
+ if (missingIds.length > 0) {
240
+ console.log(`Missing IDs: ${missingIds.length}`)
241
+ }
242
+ }
243
+
244
+ function buildEntityFromTemplate(template, transform, id) {
245
+ const entity = deepClone(template)
246
+
247
+ entity.id = id
248
+ entity.blueprint = template.blueprint
249
+ entity.position = transform.position
250
+ entity.quaternion = transform.quaternion ?? [0, 0, 0, 1]
251
+ entity.scale = transform.scale ?? [1, 1, 1]
252
+ entity.pinned = transform.pinned ?? false
253
+ entity.props = transform.props ?? deepClone(template.props ?? {})
254
+ entity.state = transform.state ?? deepClone(template.state ?? {})
255
+
256
+ return entity
257
+ }
258
+
259
+ function validateTransformItem(item, index) {
260
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
261
+ throw new Error(`Transform #${index} must be an object`)
262
+ }
263
+
264
+ const validated = {}
265
+
266
+ if (item.id !== undefined) {
267
+ if (typeof item.id !== 'string' || item.id.length === 0) {
268
+ throw new Error(`Transform #${index} field \"id\" must be a non-empty string`)
269
+ }
270
+ validated.id = item.id
271
+ }
272
+
273
+ if (item.position === undefined) {
274
+ throw new Error(`Transform #${index} is missing required field \"position\"`)
275
+ }
276
+ validated.position = validateVector(item.position, 3, `Transform #${index} field \"position\"`)
277
+
278
+ if (item.quaternion !== undefined) {
279
+ validated.quaternion = validateVector(item.quaternion, 4, `Transform #${index} field \"quaternion\"`)
280
+ }
281
+
282
+ if (item.scale !== undefined) {
283
+ validated.scale = validateVector(item.scale, 3, `Transform #${index} field \"scale\"`)
284
+ }
285
+
286
+ if (item.pinned !== undefined) {
287
+ if (typeof item.pinned !== 'boolean') {
288
+ throw new Error(`Transform #${index} field \"pinned\" must be boolean`)
289
+ }
290
+ validated.pinned = item.pinned
291
+ }
292
+
293
+ if (item.props !== undefined) {
294
+ if (!isPlainObject(item.props)) {
295
+ throw new Error(`Transform #${index} field \"props\" must be an object`)
296
+ }
297
+ validated.props = deepClone(item.props)
298
+ }
299
+
300
+ if (item.state !== undefined) {
301
+ if (!isPlainObject(item.state)) {
302
+ throw new Error(`Transform #${index} field \"state\" must be an object`)
303
+ }
304
+ validated.state = deepClone(item.state)
305
+ }
306
+
307
+ return validated
308
+ }
309
+
310
+ function validateVector(value, expectedLength, label) {
311
+ if (!Array.isArray(value)) {
312
+ throw new Error(`${label} must be an array of ${expectedLength} numbers`)
313
+ }
314
+ if (value.length !== expectedLength) {
315
+ throw new Error(`${label} must contain exactly ${expectedLength} numbers`)
316
+ }
317
+
318
+ const out = []
319
+ for (let i = 0; i < value.length; i += 1) {
320
+ const num = value[i]
321
+ if (typeof num !== 'number' || !Number.isFinite(num)) {
322
+ throw new Error(`${label} index ${i} must be a finite number`)
323
+ }
324
+ out.push(num)
325
+ }
326
+
327
+ return out
328
+ }
329
+
330
+ function generateUniqueId(existing) {
331
+ let id = ''
332
+ do {
333
+ const bytes = randomBytes(ID_LENGTH)
334
+ let next = ''
335
+ for (let i = 0; i < bytes.length; i += 1) {
336
+ next += ID_ALPHABET[bytes[i] % ID_ALPHABET.length]
337
+ }
338
+ id = next
339
+ } while (existing.has(id))
340
+
341
+ return id
342
+ }
343
+
344
+ async function confirmDangerousAction({ yes, summary }) {
345
+ if (yes) return true
346
+
347
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
348
+ throw new Error('Confirmation required in non-interactive mode. Re-run with --yes.')
349
+ }
350
+
351
+ const rl = readline.createInterface({
352
+ input: process.stdin,
353
+ output: process.stdout,
354
+ })
355
+
356
+ try {
357
+ const response = await rl.question(`${summary}\nType \"yes\" to continue: `)
358
+ return response.trim().toLowerCase() === 'yes'
359
+ } finally {
360
+ rl.close()
361
+ }
362
+ }
363
+
364
+ async function readWorld(worldPath) {
365
+ const raw = await fs.readFile(worldPath, 'utf8')
366
+ let parsed
367
+
368
+ try {
369
+ parsed = JSON.parse(raw)
370
+ } catch (error) {
371
+ throw new Error(`Failed to parse ${worldPath}: ${error.message}`)
372
+ }
373
+
374
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
375
+ throw new Error(`${worldPath} must contain a JSON object`)
376
+ }
377
+
378
+ if (!Array.isArray(parsed.entities)) {
379
+ throw new Error(`${worldPath} must contain an \"entities\" array`)
380
+ }
381
+
382
+ return parsed
383
+ }
384
+
385
+ async function readTransforms(transformsPath) {
386
+ const raw = await fs.readFile(transformsPath, 'utf8')
387
+ let parsed
388
+
389
+ try {
390
+ parsed = JSON.parse(raw)
391
+ } catch (error) {
392
+ throw new Error(`Failed to parse transforms file ${transformsPath}: ${error.message}`)
393
+ }
394
+
395
+ if (!Array.isArray(parsed)) {
396
+ throw new Error(`Transforms file ${transformsPath} must be a JSON array`)
397
+ }
398
+
399
+ return parsed
400
+ }
401
+
402
+ async function readIdArray(idsPath) {
403
+ const raw = await fs.readFile(idsPath, 'utf8')
404
+ let parsed
405
+
406
+ try {
407
+ parsed = JSON.parse(raw)
408
+ } catch (error) {
409
+ throw new Error(`Failed to parse IDs file ${idsPath}: ${error.message}`)
410
+ }
411
+
412
+ if (!Array.isArray(parsed)) {
413
+ throw new Error(`IDs file ${idsPath} must be a JSON array of strings`)
414
+ }
415
+
416
+ for (let i = 0; i < parsed.length; i += 1) {
417
+ if (typeof parsed[i] !== 'string' || parsed[i].length === 0) {
418
+ throw new Error(`IDs file ${idsPath} item #${i} must be a non-empty string`)
419
+ }
420
+ }
421
+
422
+ return parsed
423
+ }
424
+
425
+ async function writeWorld(worldPath, world) {
426
+ const dir = path.dirname(worldPath)
427
+ const tempPath = path.join(dir, `.${path.basename(worldPath)}.${process.pid}.tmp`)
428
+ const content = `${JSON.stringify(world, null, 2)}\n`
429
+
430
+ await fs.writeFile(tempPath, content, 'utf8')
431
+ await fs.rename(tempPath, worldPath)
432
+ }
433
+
434
+ function getRequiredStringOption(options, key) {
435
+ if (typeof options[key] !== 'string' || options[key].length === 0) {
436
+ throw new Error(`Missing required flag: --${key}`)
437
+ }
438
+ return options[key]
439
+ }
440
+
441
+ function getStringOption(options, key, fallback = undefined) {
442
+ const value = options[key]
443
+
444
+ if (value === undefined) return fallback
445
+
446
+ if (typeof value !== 'string' || value.length === 0) {
447
+ throw new Error(`Flag --${key} requires a value`)
448
+ }
449
+
450
+ return value
451
+ }
452
+
453
+ function deepClone(value) {
454
+ return JSON.parse(JSON.stringify(value))
455
+ }
456
+
457
+ function isPlainObject(value) {
458
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
459
+ }
@@ -14,6 +14,22 @@ Apps live in `apps/` and each app folder contains blueprint JSON plus a script e
14
14
  Local-first (project files):
15
15
 
16
16
  - Run `npm run apps:new <AppName>` (creates `apps/<AppName>/` with `index.js` + blueprint)
17
+ - Batch-edit `world.json` entities: `npm run world:entities -- add --template-id <id> --transforms <file>` / `npm run world:entities -- delete --blueprint <name>`
18
+ - To duplicate one app many times (eg, make a forest from one `Tree`), generate many transforms and run `npm run world:entities -- add --template-id <TreeEntityId> --transforms tmp/forest.json --yes`, then delete `tmp/forest.json`
19
+ - See `docs/world-entities-cli.md` for full flags (`--replace`, `--ids`, `--yes`, `--world`)
20
+ - Transform file format for `--transforms` (JSON array):
21
+ ```json
22
+ [
23
+ {
24
+ "position": [0, 0, 0],
25
+ "quaternion": [0, 0, 0, 1],
26
+ "scale": [1, 1, 1],
27
+ "pinned": false,
28
+ "props": {},
29
+ "state": {}
30
+ }
31
+ ]
32
+ ```
17
33
  - Put assets in top-level `assets/` and reference them from blueprint JSON for app.config props
18
34
  - Run `npm run dev` for hot reload
19
35
 
@@ -25,14 +25,14 @@
25
25
  <meta name="apple-mobile-web-app-capable" content="yes" />
26
26
  <meta name="mobile-web-app-capable" content="yes" />
27
27
  <link rel="preload" href="/SpaceMono-regular.woff2" as="font" type="font/woff2" crossorigin />
28
- <link rel="stylesheet" type="text/css" href="/index.css?v=1771711641829" />
28
+ <link rel="stylesheet" type="text/css" href="/index.css?v=1771723069907" />
29
29
  <script>
30
30
  window.PARTICLES_PATH = '/particles-4YQR4CFO.js'
31
31
  </script>
32
32
  </head>
33
33
  <body>
34
34
  <div id="root"></div>
35
- <script src="/env.js?v=1771711641829"></script>
35
+ <script src="/env.js?v=1771723069907"></script>
36
36
  <script src="/admin-VV3XAOYE.js" type="module"></script>
37
37
  </body>
38
38
  </html>
@@ -44,14 +44,14 @@
44
44
  <meta name="theme-color" content="#ffffff"> -->
45
45
 
46
46
  <link rel="preload" href="/SpaceMono-regular.woff2" as="font" type="font/woff2" crossorigin />
47
- <link rel="stylesheet" type="text/css" href="/index.css?v=1771711641826" />
47
+ <link rel="stylesheet" type="text/css" href="/index.css?v=1771723069906" />
48
48
  <script>
49
49
  window.PARTICLES_PATH = '/particles-4YQR4CFO.js'
50
50
  </script>
51
51
  </head>
52
52
  <body>
53
53
  <div id="root"></div>
54
- <script src="/env.js?v=1771711641826"></script>
54
+ <script src="/env.js?v=1771723069906"></script>
55
55
  <script src="/index-FOLIOXLC.js" type="module"></script>
56
56
  </body>
57
57
  </html>
@@ -0,0 +1,92 @@
1
+ # World Entities CLI
2
+
3
+ Batch-create and batch-delete entities in `world.json` for agent-driven workflows.
4
+
5
+ This tool is generic by design: agents decide what to place and where, and provide transform data.
6
+
7
+ ## Command
8
+
9
+ ```bash
10
+ npm run world:entities -- <command> [flags]
11
+ ```
12
+
13
+ ## Add entities from a template
14
+
15
+ Clone an existing entity instance by ID, but apply transform overrides from a JSON file.
16
+
17
+ ```bash
18
+ npm run world:entities -- add \
19
+ --template-id k3sbGG4iq4 \
20
+ --transforms tmp/add-trees.json
21
+ ```
22
+
23
+ Replace mode (delete all current instances of the template blueprint first, then add):
24
+
25
+ ```bash
26
+ npm run world:entities -- add \
27
+ --template-id k3sbGG4iq4 \
28
+ --transforms tmp/add-trees.json \
29
+ --replace \
30
+ --yes
31
+ ```
32
+
33
+ ## Delete entities
34
+
35
+ Delete by blueprint:
36
+
37
+ ```bash
38
+ npm run world:entities -- delete --blueprint Tree --yes
39
+ ```
40
+
41
+ Delete by explicit ID list file:
42
+
43
+ ```bash
44
+ npm run world:entities -- delete --ids tmp/delete-ids.json --yes
45
+ ```
46
+
47
+ ## Flags
48
+
49
+ - `--world <path>`: world file path (default `world.json`)
50
+ - `--yes`: skip confirmation prompts and allow no-op operations
51
+ - `--replace`: add mode only; remove existing entities with template blueprint before add
52
+
53
+ ## Transforms File Format
54
+
55
+ `--transforms` must point to a JSON array of objects:
56
+
57
+ ```json
58
+ [
59
+ {
60
+ "position": [0, 0, 0],
61
+ "quaternion": [0, 0, 0, 1],
62
+ "scale": [1, 1, 1],
63
+ "pinned": false,
64
+ "props": {},
65
+ "state": {}
66
+ }
67
+ ]
68
+ ```
69
+
70
+ Fields:
71
+
72
+ - `position` required (`[x, y, z]`)
73
+ - `quaternion` optional (`[x, y, z, w]`, default `[0,0,0,1]`)
74
+ - `scale` optional (`[x, y, z]`, default `[1,1,1]`)
75
+ - `pinned` optional (default `false`)
76
+ - `props` optional (default cloned from template entity)
77
+ - `state` optional (default cloned from template entity)
78
+ - `id` optional (auto-generated short 10-char ID if omitted)
79
+
80
+ ## Temp file workflow for agents
81
+
82
+ Store generated transform files in `tmp/` (gitignored except `.gitkeep`/`README.md`), run the command, then delete the temp file.
83
+
84
+ Example:
85
+
86
+ ```bash
87
+ # 1) Create temp transforms JSON under tmp/
88
+ # 2) Run CLI
89
+ npm run world:entities -- add --template-id k3sbGG4iq4 --transforms tmp/run-001.json --yes
90
+ # 3) Remove temp file
91
+ rm tmp/run-001.json
92
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamedev",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "index.node.js",
6
6
  "types": "index.d.ts",