lpc-forge 1.0.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 (75) hide show
  1. package/CREDITS.csv +22985 -0
  2. package/LICENSE +674 -0
  3. package/README.md +281 -0
  4. package/assets/music/.gitkeep +0 -0
  5. package/dist/character/batch.d.ts +17 -0
  6. package/dist/character/batch.js +48 -0
  7. package/dist/character/batch.js.map +1 -0
  8. package/dist/character/composer.d.ts +3 -0
  9. package/dist/character/composer.js +164 -0
  10. package/dist/character/composer.js.map +1 -0
  11. package/dist/character/definitions.d.ts +16 -0
  12. package/dist/character/definitions.js +116 -0
  13. package/dist/character/definitions.js.map +1 -0
  14. package/dist/character/presets.d.ts +6 -0
  15. package/dist/character/presets.js +246 -0
  16. package/dist/character/presets.js.map +1 -0
  17. package/dist/character/slicer.d.ts +8 -0
  18. package/dist/character/slicer.js +66 -0
  19. package/dist/character/slicer.js.map +1 -0
  20. package/dist/character/types.d.ts +48 -0
  21. package/dist/character/types.js +32 -0
  22. package/dist/character/types.js.map +1 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +938 -0
  25. package/dist/export/frames.d.ts +5 -0
  26. package/dist/export/frames.js +15 -0
  27. package/dist/export/frames.js.map +1 -0
  28. package/dist/export/godot.d.ts +17 -0
  29. package/dist/export/godot.js +464 -0
  30. package/dist/export/godot.js.map +1 -0
  31. package/dist/export/types.d.ts +11 -0
  32. package/dist/export/types.js +2 -0
  33. package/dist/export/types.js.map +1 -0
  34. package/dist/license.d.ts +49 -0
  35. package/dist/license.js +271 -0
  36. package/dist/map/cellular.d.ts +3 -0
  37. package/dist/map/cellular.js +191 -0
  38. package/dist/map/cellular.js.map +1 -0
  39. package/dist/map/dungeon.d.ts +3 -0
  40. package/dist/map/dungeon.js +238 -0
  41. package/dist/map/dungeon.js.map +1 -0
  42. package/dist/map/multifloor.d.ts +20 -0
  43. package/dist/map/multifloor.js +57 -0
  44. package/dist/map/multifloor.js.map +1 -0
  45. package/dist/map/overworld.d.ts +3 -0
  46. package/dist/map/overworld.js +205 -0
  47. package/dist/map/overworld.js.map +1 -0
  48. package/dist/map/town.d.ts +7 -0
  49. package/dist/map/town.js +181 -0
  50. package/dist/map/town.js.map +1 -0
  51. package/dist/map/types.d.ts +65 -0
  52. package/dist/map/types.js +16 -0
  53. package/dist/map/types.js.map +1 -0
  54. package/dist/map/wfc.d.ts +18 -0
  55. package/dist/map/wfc.js +192 -0
  56. package/dist/map/wfc.js.map +1 -0
  57. package/dist/tileset/atlas.d.ts +15 -0
  58. package/dist/tileset/atlas.js +55 -0
  59. package/dist/tileset/atlas.js.map +1 -0
  60. package/dist/tileset/registry.d.ts +12 -0
  61. package/dist/tileset/registry.js +71 -0
  62. package/dist/tileset/registry.js.map +1 -0
  63. package/dist/tileset/terrain.d.ts +3 -0
  64. package/dist/tileset/terrain.js +110 -0
  65. package/dist/tileset/terrain.js.map +1 -0
  66. package/dist/utils/credits.d.ts +11 -0
  67. package/dist/utils/credits.js +74 -0
  68. package/dist/utils/credits.js.map +1 -0
  69. package/dist/utils/image.d.ts +17 -0
  70. package/dist/utils/image.js +94 -0
  71. package/dist/utils/image.js.map +1 -0
  72. package/dist/utils/rng.d.ts +18 -0
  73. package/dist/utils/rng.js +48 -0
  74. package/dist/utils/rng.js.map +1 -0
  75. package/package.json +77 -0
package/dist/cli.js ADDED
@@ -0,0 +1,938 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { resolve, join } from 'node:path';
4
+ import { mkdir, writeFile } from 'node:fs/promises';
5
+ import { fileURLToPath } from 'node:url';
6
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
7
+ const REPO_ROOT = resolve(__dirname, '..');
8
+ const program = new Command();
9
+ program
10
+ .name('lpc-forge')
11
+ .description('Complete 2D game asset pipeline — character compositor, map generator, Godot 4 exporter')
12
+ .version('0.1.0');
13
+ // === CHARACTER COMMAND ===
14
+ program
15
+ .command('character')
16
+ .description('Generate a character spritesheet')
17
+ .option('-p, --preset <name>', 'Use a preset (warrior, mage, rogue, ranger, villager)')
18
+ .option('-b, --body <type>', 'Body type (male, female, muscular, teen, child, pregnant)', 'male')
19
+ .option('--skin <variant>', 'Skin color variant', 'light')
20
+ .option('--hair <style:color>', 'Hair style and color (e.g., "plain:brown")')
21
+ .option('--armor <type:variant>', 'Armor type and variant')
22
+ .option('--weapon <type:variant>', 'Weapon type and variant')
23
+ .option('-o, --output <path>', 'Output path', './output/character')
24
+ .option('--slice', 'Also slice into individual frames', false)
25
+ .option('--godot', 'Export as Godot 4 resources', false)
26
+ .option('--list-layers', 'List all available layers and variants')
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ .action(async (opts) => {
29
+ const chalk = (await import('chalk')).default;
30
+ const ora = (await import('ora')).default;
31
+ if (opts.listLayers) {
32
+ const { loadDefinitions, listLayers } = await import('./character/definitions.js');
33
+ const registry = await loadDefinitions(REPO_ROOT);
34
+ const layers = listLayers(registry);
35
+ for (const [category, items] of Object.entries(layers)) {
36
+ console.log(chalk.bold.cyan(`\n${category}`));
37
+ for (const item of items) {
38
+ console.log(` ${chalk.green(item.name)}: ${item.variants.slice(0, 8).join(', ')}${item.variants.length > 8 ? '...' : ''}`);
39
+ }
40
+ }
41
+ return;
42
+ }
43
+ const spinner = ora('Composing character...').start();
44
+ try {
45
+ const { composeCharacter } = await import('./character/composer.js');
46
+ const { PRESETS } = await import('./character/presets.js');
47
+ let spec;
48
+ if (opts.preset) {
49
+ const preset = PRESETS[opts.preset];
50
+ if (!preset) {
51
+ spinner.fail(`Unknown preset: ${opts.preset}. Available: ${Object.keys(PRESETS).join(', ')}`);
52
+ process.exit(1);
53
+ }
54
+ spec = preset.spec;
55
+ spinner.text = `Composing ${preset.name}...`;
56
+ }
57
+ else {
58
+ // Build spec from CLI options
59
+ const layers = [
60
+ { category: 'body', subcategory: 'body', variant: opts.skin },
61
+ ];
62
+ if (opts.hair) {
63
+ const [style, color] = opts.hair.split(':');
64
+ layers.push({ category: 'hair', subcategory: `hair_${style}`, variant: color || 'brown' });
65
+ }
66
+ if (opts.armor) {
67
+ const [type, variant] = opts.armor.split(':');
68
+ layers.push({ category: 'torso', subcategory: `torso_armour_${type}`, variant: variant || 'steel' });
69
+ }
70
+ if (opts.weapon) {
71
+ const [type, variant] = opts.weapon.split(':');
72
+ layers.push({ category: 'weapons', subcategory: `weapon_${type}`, variant: variant || 'default' });
73
+ }
74
+ spec = {
75
+ bodyType: opts.body,
76
+ layers,
77
+ };
78
+ }
79
+ const buffer = await composeCharacter(spec, REPO_ROOT);
80
+ const outputDir = resolve(opts.output);
81
+ await mkdir(outputDir, { recursive: true });
82
+ await writeFile(join(outputDir, 'spritesheet.png'), buffer);
83
+ spinner.succeed(`Character spritesheet saved to ${outputDir}/spritesheet.png`);
84
+ if (opts.slice) {
85
+ const sliceSpinner = ora('Slicing into frames...').start();
86
+ const { sliceCharacter } = await import('./character/slicer.js');
87
+ const result = await sliceCharacter(buffer, join(outputDir, 'frames'));
88
+ sliceSpinner.succeed(`Sliced into ${result.totalFrames} frames`);
89
+ }
90
+ if (opts.godot) {
91
+ const godotSpinner = ora('Exporting Godot resources...').start();
92
+ const { exportCharacterToGodot } = await import('./export/godot.js');
93
+ const name = opts.preset || 'character';
94
+ await exportCharacterToGodot(buffer, outputDir, name);
95
+ godotSpinner.succeed('Godot resources exported');
96
+ }
97
+ }
98
+ catch (err) {
99
+ spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
100
+ process.exit(1);
101
+ }
102
+ });
103
+ // === BATCH COMMAND ===
104
+ program
105
+ .command('batch')
106
+ .description('Generate multiple characters from a JSON config')
107
+ .argument('<config>', 'Path to batch config JSON')
108
+ .option('-o, --output <path>', 'Output directory', './output/batch')
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
+ .action(async (configPath, opts) => {
111
+ const chalk = (await import('chalk')).default;
112
+ const ora = (await import('ora')).default;
113
+ const { runBatch } = await import('./character/batch.js');
114
+ const spinner = ora('Running batch generation...').start();
115
+ const results = await runBatch(resolve(configPath), REPO_ROOT, resolve(opts.output));
116
+ const succeeded = results.filter(r => r.success).length;
117
+ const failed = results.filter(r => !r.success).length;
118
+ spinner.succeed(`Batch complete: ${succeeded} succeeded, ${failed} failed`);
119
+ for (const r of results) {
120
+ if (r.success) {
121
+ console.log(chalk.green(` ✓ ${r.name}`));
122
+ }
123
+ else {
124
+ console.log(chalk.red(` ✗ ${r.name}: ${r.error}`));
125
+ }
126
+ }
127
+ });
128
+ // === LIST COMMAND ===
129
+ program
130
+ .command('list')
131
+ .description('List available assets')
132
+ .option('-c, --category <name>', 'Filter by category')
133
+ .option('--body-type <type>', 'Filter by body type')
134
+ .option('--json', 'Output as JSON')
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ .action(async (opts) => {
137
+ const { loadDefinitions, listLayers } = await import('./character/definitions.js');
138
+ const registry = await loadDefinitions(REPO_ROOT);
139
+ const layers = listLayers(registry);
140
+ if (opts.category) {
141
+ const filtered = {};
142
+ for (const [key, val] of Object.entries(layers)) {
143
+ if (key.includes(opts.category)) {
144
+ filtered[key] = val;
145
+ }
146
+ }
147
+ if (opts.json) {
148
+ console.log(JSON.stringify(filtered, null, 2));
149
+ }
150
+ else {
151
+ for (const [cat, items] of Object.entries(filtered)) {
152
+ console.log(`\n${cat}:`);
153
+ for (const item of items) {
154
+ console.log(` ${item.name}: ${item.variants.join(', ')}`);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ else {
160
+ if (opts.json) {
161
+ console.log(JSON.stringify(layers, null, 2));
162
+ }
163
+ else {
164
+ for (const [cat, items] of Object.entries(layers)) {
165
+ console.log(`\n${cat}:`);
166
+ for (const item of items) {
167
+ console.log(` ${item.name}: ${item.variants.join(', ')}`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ });
173
+ // === MAP COMMAND ===
174
+ program
175
+ .command('map')
176
+ .description('Generate a procedural map')
177
+ .argument('<type>', 'Map type (dungeon, cave, overworld, wfc, town, multifloor)')
178
+ .option('-W, --width <n>', 'Map width in tiles', '50')
179
+ .option('-H, --height <n>', 'Map height in tiles', '50')
180
+ .option('-s, --seed <seed>', 'Random seed')
181
+ .option('--rooms <n>', 'Number of rooms (dungeon only)', '12')
182
+ .option('--room-min <n>', 'Minimum room size', '5')
183
+ .option('--room-max <n>', 'Maximum room size', '15')
184
+ .option('--buildings <n>', 'Number of buildings (town only)', '6')
185
+ .option('--floors <n>', 'Number of floors (multifloor only)', '3')
186
+ .option('-o, --output <path>', 'Output path', './output/map')
187
+ .option('--render', 'Render visual PNG preview', true)
188
+ .option('--godot', 'Export as Godot 4 TileMap', false)
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
190
+ .action(async (type, opts) => {
191
+ const chalk = (await import('chalk')).default;
192
+ const ora = (await import('ora')).default;
193
+ const spinner = ora(`Generating ${type} map...`).start();
194
+ try {
195
+ const width = parseInt(opts.width);
196
+ const height = parseInt(opts.height);
197
+ const seed = opts.seed || `map-${Date.now()}`;
198
+ let map;
199
+ switch (type) {
200
+ case 'dungeon': {
201
+ const { generateDungeon } = await import('./map/dungeon.js');
202
+ map = generateDungeon({
203
+ width,
204
+ height,
205
+ seed,
206
+ maxRooms: parseInt(opts.rooms),
207
+ roomMinSize: parseInt(opts.roomMin),
208
+ roomMaxSize: parseInt(opts.roomMax),
209
+ });
210
+ break;
211
+ }
212
+ case 'cave': {
213
+ const { generateCave } = await import('./map/cellular.js');
214
+ map = generateCave({ width, height, seed });
215
+ break;
216
+ }
217
+ case 'overworld': {
218
+ const { generateOverworld } = await import('./map/overworld.js');
219
+ map = generateOverworld({ width, height, seed });
220
+ break;
221
+ }
222
+ case 'wfc': {
223
+ const { generateWFC } = await import('./map/wfc.js');
224
+ map = generateWFC({ width, height, seed });
225
+ break;
226
+ }
227
+ case 'town': {
228
+ const { generateTown } = await import('./map/town.js');
229
+ map = generateTown({
230
+ width,
231
+ height,
232
+ seed,
233
+ buildings: parseInt(opts.buildings),
234
+ });
235
+ break;
236
+ }
237
+ case 'multifloor': {
238
+ const { generateMultiFloor } = await import('./map/multifloor.js');
239
+ const result = await generateMultiFloor({
240
+ floors: parseInt(opts.floors),
241
+ width,
242
+ height,
243
+ seed,
244
+ });
245
+ const outputDir = resolve(opts.output);
246
+ await mkdir(outputDir, { recursive: true });
247
+ // Save all floors data
248
+ await writeFile(join(outputDir, 'floors.json'), JSON.stringify(result, null, 2));
249
+ // Render each floor
250
+ if (opts.render) {
251
+ const { generateDefaultTileset } = await import('./tileset/registry.js');
252
+ const { renderMap } = await import('./tileset/terrain.js');
253
+ const tilesetDir = join(outputDir, 'tileset');
254
+ await generateDefaultTileset(tilesetDir);
255
+ for (let i = 0; i < result.floors.length; i++) {
256
+ await renderMap(result.floors[i], tilesetDir, join(outputDir, `floor_${i + 1}.png`));
257
+ }
258
+ }
259
+ spinner.succeed(`multifloor dungeon generated (${result.floors.length} floors, seed: ${seed})`);
260
+ console.log((await import('chalk')).default.green(`\nOutput: ${outputDir}`));
261
+ return;
262
+ }
263
+ default:
264
+ spinner.fail(`Unknown map type: ${type}. Use: dungeon, cave, overworld, wfc, town, multifloor`);
265
+ process.exit(1);
266
+ }
267
+ const outputDir = resolve(opts.output);
268
+ await mkdir(outputDir, { recursive: true });
269
+ // Save map data as JSON
270
+ await writeFile(join(outputDir, 'map.json'), JSON.stringify(map, null, 2));
271
+ spinner.succeed(`${type} map generated (${width}×${height}, ${map.rooms.length} rooms, seed: ${seed})`);
272
+ // Render visual preview
273
+ if (opts.render) {
274
+ const renderSpinner = ora('Rendering map preview...').start();
275
+ const { generateDefaultTileset } = await import('./tileset/registry.js');
276
+ const { renderMap } = await import('./tileset/terrain.js');
277
+ const tilesetDir = join(outputDir, 'tileset');
278
+ await generateDefaultTileset(tilesetDir);
279
+ await renderMap(map, tilesetDir, join(outputDir, 'map.png'));
280
+ renderSpinner.succeed('Map preview rendered');
281
+ }
282
+ // Godot export
283
+ if (opts.godot) {
284
+ const godotSpinner = ora('Exporting Godot TileMap...').start();
285
+ const { exportMapToGodot } = await import('./export/godot.js');
286
+ await exportMapToGodot(map, outputDir, type);
287
+ godotSpinner.succeed('Godot TileMap exported');
288
+ }
289
+ console.log(chalk.green(`\nOutput: ${outputDir}`));
290
+ }
291
+ catch (err) {
292
+ spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
293
+ process.exit(1);
294
+ }
295
+ });
296
+ // === INIT COMMAND ===
297
+ program
298
+ .command('init')
299
+ .description('Scaffold a Godot project with generated assets (use --full for premium complete game)')
300
+ .argument('<name>', 'Project name')
301
+ .option('--character <preset>', 'Character preset', 'warrior')
302
+ .option('--map <type>', 'Map type (dungeon, cave, overworld)', 'dungeon')
303
+ .option('--full', 'Generate FULL premium game (all systems, SFX, UI, lighting, particles)')
304
+ .option('--lighting <preset>', 'Lighting preset (dungeon_dark, overworld_night, cave, etc.)')
305
+ .option('--particles <names...>', 'Particle effects to include (rain, fireflies, torch_fire, etc.)')
306
+ .option('--systems <names...>', 'Specific game systems to include')
307
+ .option('--no-sfx', 'Skip SFX generation')
308
+ .option('-o, --output <path>', 'Output directory', '.')
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ .action(async (name, opts) => {
311
+ const chalk = (await import('chalk')).default;
312
+ const ora = (await import('ora')).default;
313
+ const outputDir = resolve(opts.output, name);
314
+ const isFull = opts.full === true;
315
+ // Premium gate check
316
+ if (isFull) {
317
+ const { requireLicense } = await import('./license.js');
318
+ await requireLicense('init --full');
319
+ }
320
+ try {
321
+ // 1. Scaffold Godot project
322
+ const scaffoldSpinner = ora('Scaffolding Godot project...').start();
323
+ const { scaffoldGodotProject, exportCharacterToGodot, exportMapToGodot } = await import('./export/godot.js');
324
+ await scaffoldGodotProject(outputDir, name, {
325
+ characterName: opts.character,
326
+ mapName: opts.map,
327
+ });
328
+ scaffoldSpinner.succeed('Godot project scaffolded');
329
+ // 2. Generate character
330
+ const charSpinner = ora(`Generating ${opts.character} character...`).start();
331
+ const { composeCharacter } = await import('./character/composer.js');
332
+ const { PRESETS } = await import('./character/presets.js');
333
+ const preset = PRESETS[opts.character];
334
+ if (!preset) {
335
+ charSpinner.fail(`Unknown preset: ${opts.character}`);
336
+ process.exit(1);
337
+ }
338
+ const charBuffer = await composeCharacter(preset.spec, REPO_ROOT);
339
+ await exportCharacterToGodot(charBuffer, outputDir, opts.character);
340
+ charSpinner.succeed(`${preset.name} character generated`);
341
+ // 3. Generate map
342
+ const mapSpinner = ora(`Generating ${opts.map} map...`).start();
343
+ let map;
344
+ const seed = `${name}-${Date.now()}`;
345
+ switch (opts.map) {
346
+ case 'dungeon': {
347
+ const { generateDungeon } = await import('./map/dungeon.js');
348
+ map = generateDungeon({ width: 50, height: 50, seed });
349
+ break;
350
+ }
351
+ case 'cave': {
352
+ const { generateCave } = await import('./map/cellular.js');
353
+ map = generateCave({ width: 50, height: 50, seed });
354
+ break;
355
+ }
356
+ case 'overworld': {
357
+ const { generateOverworld } = await import('./map/overworld.js');
358
+ map = generateOverworld({ width: 60, height: 60, seed });
359
+ break;
360
+ }
361
+ default: {
362
+ const { generateDungeon } = await import('./map/dungeon.js');
363
+ map = generateDungeon({ width: 50, height: 50, seed });
364
+ }
365
+ }
366
+ await exportMapToGodot(map, outputDir, opts.map);
367
+ const { generateDefaultTileset } = await import('./tileset/registry.js');
368
+ const { renderMap } = await import('./tileset/terrain.js');
369
+ const tilesetDir = join(outputDir, 'tileset');
370
+ await generateDefaultTileset(tilesetDir);
371
+ await renderMap(map, tilesetDir, join(outputDir, 'map_preview.png'));
372
+ mapSpinner.succeed(`${opts.map} map generated`);
373
+ // === PREMIUM: Full game generation ===
374
+ if (isFull) {
375
+ // 4. Game Systems
376
+ const sysSpinner = ora('Generating game systems...').start();
377
+ const { writeGameSystems } = await import('./systems/writer.js');
378
+ const sysResult = await writeGameSystems(outputDir, opts.systems);
379
+ sysSpinner.succeed(`${sysResult.systems.length} game systems generated (${sysResult.filesWritten} files)`);
380
+ // 5. SFX
381
+ if (opts.sfx !== false) {
382
+ const sfxSpinner = ora('Generating sound effects...').start();
383
+ const { generateAllSfx } = await import('./audio/sfx-generator.js');
384
+ const sfxResults = await generateAllSfx(outputDir);
385
+ sfxSpinner.succeed(`${sfxResults.length} sound effects generated`);
386
+ }
387
+ // 6. UI Kit
388
+ const uiSpinner = ora('Generating UI kit...').start();
389
+ const { generateUIKit } = await import('./ui/generator.js');
390
+ await generateUIKit(join(outputDir, 'ui'), 'medieval');
391
+ uiSpinner.succeed('UI kit generated');
392
+ // 7. Item Icons
393
+ const iconsSpinner = ora('Generating item icons...').start();
394
+ const { generateAllIcons } = await import('./ui/icons.js');
395
+ await generateAllIcons(join(outputDir, 'icons'));
396
+ iconsSpinner.succeed('Item icons generated');
397
+ // 8. Props
398
+ const propsSpinner = ora('Generating props...').start();
399
+ const { generateAllProps } = await import('./ui/props.js');
400
+ await generateAllProps(join(outputDir, 'props'));
401
+ propsSpinner.succeed('Props generated');
402
+ // 9. Portrait
403
+ const portraitSpinner = ora('Generating character portrait...').start();
404
+ const { extractPortrait } = await import('./ui/portrait.js');
405
+ await extractPortrait(charBuffer, outputDir, opts.character);
406
+ portraitSpinner.succeed('Character portrait generated');
407
+ // 10. Lighting
408
+ const lightingSpinner = ora('Generating lighting presets...').start();
409
+ const { writeAllLightingPresets } = await import('./lighting/index.js');
410
+ const lightingFiles = await writeAllLightingPresets(outputDir);
411
+ lightingSpinner.succeed(`${lightingFiles.length} lighting presets generated`);
412
+ // 11. Particles
413
+ const particleSpinner = ora('Generating particle effects...').start();
414
+ const { writeAllParticlePresets } = await import('./lighting/index.js');
415
+ const particleFiles = await writeAllParticlePresets(outputDir);
416
+ particleSpinner.succeed(`${particleFiles.length} particle effects generated`);
417
+ // 12. Generate enemy characters
418
+ const enemySpinner = ora('Generating enemy characters...').start();
419
+ const enemyPresets = ['skeleton', 'guard', 'thief'];
420
+ let enemyCount = 0;
421
+ for (const enemyPreset of enemyPresets) {
422
+ if (PRESETS[enemyPreset]) {
423
+ const enemyBuffer = await composeCharacter(PRESETS[enemyPreset].spec, REPO_ROOT);
424
+ await exportCharacterToGodot(enemyBuffer, outputDir, enemyPreset, { isPlayer: false });
425
+ enemyCount++;
426
+ }
427
+ }
428
+ enemySpinner.succeed(`${enemyCount} enemy characters generated`);
429
+ // 13. Generate NPC characters
430
+ const npcSpinner = ora('Generating NPC characters...').start();
431
+ const npcPresets = ['merchant', 'healer', 'guard', 'peasant'];
432
+ let npcCount = 0;
433
+ for (const npcPreset of npcPresets) {
434
+ if (PRESETS[npcPreset] && npcPreset !== 'guard') {
435
+ const npcBuffer = await composeCharacter(PRESETS[npcPreset].spec, REPO_ROOT);
436
+ await exportCharacterToGodot(npcBuffer, outputDir, `npc_${npcPreset}`, { isPlayer: false });
437
+ npcCount++;
438
+ }
439
+ }
440
+ npcSpinner.succeed(`${npcCount} NPC characters generated`);
441
+ // 14. Update project.godot with autoloads and input actions
442
+ const projectGodotPath = join(outputDir, 'project.godot');
443
+ let projectContent = await (await import('node:fs/promises')).readFile(projectGodotPath, 'utf-8');
444
+ // Add autoloads section
445
+ if (sysResult.autoloads.length > 0) {
446
+ let autoloadSection = '\n[autoload]\n\n';
447
+ for (const al of sysResult.autoloads) {
448
+ autoloadSection += `${al.name}="${al.path}"\n`;
449
+ }
450
+ // Insert before [rendering]
451
+ projectContent = projectContent.replace('[rendering]', autoloadSection + '[rendering]');
452
+ }
453
+ // Add custom input actions
454
+ const customActions = [...new Set([...sysResult.inputActions])];
455
+ const keyMap = {
456
+ inventory: 73, // I
457
+ interact: 69, // E
458
+ pause: 4194305, // Escape
459
+ quest_log: 74, // J
460
+ };
461
+ for (const action of customActions) {
462
+ const keycode = keyMap[action] ?? 0;
463
+ if (keycode && !projectContent.includes(`${action}=`)) {
464
+ const inputEntry = `${action}={
465
+ "deadzone": 0.5,
466
+ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":${keycode},"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
467
+ }\n`;
468
+ // Insert before [rendering]
469
+ projectContent = projectContent.replace('[rendering]', inputEntry + '\n[rendering]');
470
+ }
471
+ }
472
+ await (await import('node:fs/promises')).writeFile(projectGodotPath, projectContent);
473
+ // Print summary
474
+ console.log(chalk.green('\n✅ Full premium project generated!\n'));
475
+ console.log(chalk.cyan(' 📂 Project: ') + outputDir);
476
+ console.log(chalk.cyan(' 🎮 Game Systems: ') + sysResult.systems.join(', '));
477
+ console.log(chalk.cyan(' 🔊 Sound Effects: ') + (opts.sfx !== false ? 'All categories' : 'Skipped'));
478
+ console.log(chalk.cyan(' 🎨 UI Kit: ') + 'Medieval theme');
479
+ console.log(chalk.cyan(' 🖼️ Icons: ') + 'All item icons');
480
+ console.log(chalk.cyan(' 🏗️ Props: ') + 'All prop sprites');
481
+ console.log(chalk.cyan(' 💡 Lighting: ') + `${lightingFiles.length} presets`);
482
+ console.log(chalk.cyan(' ✨ Particles: ') + `${particleFiles.length} effects`);
483
+ console.log(chalk.cyan(' ⚔️ Enemies: ') + `${enemyCount} characters`);
484
+ console.log(chalk.cyan(' 🧑 NPCs: ') + `${npcCount} characters`);
485
+ console.log(chalk.cyan(' 📸 Portrait: ') + '3 sizes');
486
+ console.log('');
487
+ console.log(chalk.yellow(' Autoloads added to project.godot:'));
488
+ for (const al of sysResult.autoloads) {
489
+ console.log(chalk.gray(` ${al.name} → ${al.path}`));
490
+ }
491
+ console.log('');
492
+ console.log(chalk.yellow(' Input Actions:'));
493
+ for (const action of customActions) {
494
+ const keyName = Object.entries(keyMap).find(([k]) => k === action)?.[0] ?? action;
495
+ console.log(chalk.gray(` ${keyName} → configured`));
496
+ }
497
+ }
498
+ else {
499
+ // Free tier output
500
+ console.log(chalk.green(`\n✓ Godot project created at: ${outputDir}`));
501
+ console.log(chalk.gray(`\nGenerated assets:`));
502
+ console.log(chalk.gray(` • Character: sprites/${opts.character}/`));
503
+ console.log(chalk.gray(` • Map: ${opts.map}.tscn`));
504
+ console.log(chalk.gray(` • Player script: scripts/player.gd`));
505
+ console.log('');
506
+ console.log(chalk.yellow(' 💎 Want the full game? Run:'));
507
+ console.log(chalk.cyan(' lpc-forge init my-game --full'));
508
+ console.log(chalk.gray(' Includes: inventory, dialog, AI, menus, SFX, lighting, particles, and more'));
509
+ }
510
+ console.log(chalk.gray(`\nTo use with Godot 4.6:`));
511
+ console.log(chalk.gray(` 1. Open Godot → Import → Select ${outputDir}/project.godot`));
512
+ console.log(chalk.gray(` 2. Run the project (F5)`));
513
+ }
514
+ catch (err) {
515
+ console.error(chalk.red(`Failed: ${err instanceof Error ? err.message : String(err)}`));
516
+ process.exit(1);
517
+ }
518
+ });
519
+ // === ACTIVATE COMMAND ===
520
+ program
521
+ .command('activate')
522
+ .description('Activate a premium license key')
523
+ .argument('[key]', 'License key from your purchase')
524
+ .option('--status', 'Check current license status')
525
+ .option('--deactivate', 'Remove stored license')
526
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
527
+ .action(async (key, opts) => {
528
+ const chalk = (await import('chalk')).default;
529
+ const { getLicenseInfo, activateLicense, deactivateLicense } = await import('./license.js');
530
+ if (opts.deactivate) {
531
+ await deactivateLicense();
532
+ console.log(chalk.green('License deactivated.'));
533
+ return;
534
+ }
535
+ if (opts.status) {
536
+ const info = await getLicenseInfo();
537
+ if (info && info.valid) {
538
+ console.log(chalk.green('✅ Premium license active'));
539
+ console.log(chalk.gray(` Key: ${info.key}`));
540
+ console.log(chalk.gray(` Email: ${info.email}`));
541
+ console.log(chalk.gray(` Activated: ${info.activatedAt}`));
542
+ console.log(chalk.gray(` Last Verified: ${info.lastVerifiedAt}`));
543
+ }
544
+ else if (info && !info.valid) {
545
+ console.log(chalk.red('❌ License invalid or tampered'));
546
+ console.log(chalk.gray(' Re-activate: lpc-forge activate <key>'));
547
+ }
548
+ else {
549
+ console.log(chalk.yellow('No active license.'));
550
+ console.log(chalk.gray(' Purchase at: https://blueth.online/plugins/lpc-forge'));
551
+ console.log(chalk.gray(' Activate: lpc-forge activate <key>'));
552
+ }
553
+ return;
554
+ }
555
+ if (!key) {
556
+ console.log(chalk.yellow('Usage: lpc-forge activate <license-key>'));
557
+ console.log(chalk.gray(' Purchase at: https://blueth.online/plugins/lpc-forge'));
558
+ return;
559
+ }
560
+ const ora = (await import('ora')).default;
561
+ const spinner = ora('Activating license...').start();
562
+ const result = await activateLicense(key);
563
+ if (result.success) {
564
+ spinner.succeed(chalk.green(result.message));
565
+ console.log(chalk.cyan('\n You now have access to premium features!'));
566
+ console.log(chalk.gray(' Try: lpc-forge init my-game --full'));
567
+ }
568
+ else {
569
+ spinner.fail(chalk.red(result.message));
570
+ }
571
+ });
572
+ // === LIGHTING COMMAND ===
573
+ program
574
+ .command('lighting')
575
+ .description('Generate lighting presets for Godot scenes')
576
+ .option('-o, --output <path>', 'Output directory', './output')
577
+ .option('-p, --preset <name>', 'Specific lighting preset')
578
+ .option('--list', 'List available lighting presets')
579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
580
+ .action(async (opts) => {
581
+ const chalk = (await import('chalk')).default;
582
+ const ora = (await import('ora')).default;
583
+ if (opts.list) {
584
+ const { listLightingPresets, getLightingPreset } = await import('./lighting/index.js');
585
+ console.log(chalk.bold.cyan('\nAvailable Lighting Presets:\n'));
586
+ for (const name of listLightingPresets()) {
587
+ const preset = getLightingPreset(name);
588
+ const lights = preset.lights.length > 0 ? ` (${preset.lights.length} light${preset.lights.length > 1 ? 's' : ''})` : '';
589
+ console.log(` ${chalk.green(name)}: ${preset.description}${lights}`);
590
+ }
591
+ return;
592
+ }
593
+ // Premium gate
594
+ const { requireLicense } = await import('./license.js');
595
+ await requireLicense('lighting');
596
+ const spinner = ora('Generating lighting presets...').start();
597
+ try {
598
+ if (opts.preset) {
599
+ const { getLightingPreset, generateLightingScene } = await import('./lighting/index.js');
600
+ const preset = getLightingPreset(opts.preset);
601
+ if (!preset) {
602
+ spinner.fail(chalk.red(`Unknown preset: ${opts.preset}`));
603
+ process.exit(1);
604
+ }
605
+ const { mkdir, writeFile } = await import('node:fs/promises');
606
+ const outDir = resolve(opts.output, 'lighting');
607
+ await mkdir(outDir, { recursive: true });
608
+ const scene = generateLightingScene(preset);
609
+ await writeFile(join(outDir, `lighting_${opts.preset}.tscn`), scene);
610
+ spinner.succeed(chalk.green(`Lighting preset "${opts.preset}" → ${opts.output}/lighting/`));
611
+ }
612
+ else {
613
+ const { writeAllLightingPresets } = await import('./lighting/index.js');
614
+ const files = await writeAllLightingPresets(resolve(opts.output));
615
+ spinner.succeed(chalk.green(`${files.length} lighting presets → ${opts.output}/lighting/`));
616
+ }
617
+ }
618
+ catch (err) {
619
+ spinner.fail(chalk.red('Failed'));
620
+ console.error(err);
621
+ process.exit(1);
622
+ }
623
+ });
624
+ // === PARTICLES COMMAND ===
625
+ program
626
+ .command('particles')
627
+ .description('Generate particle effect scenes for Godot')
628
+ .option('-o, --output <path>', 'Output directory', './output')
629
+ .option('-p, --preset <name>', 'Specific particle preset')
630
+ .option('--list', 'List available particle effects')
631
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
632
+ .action(async (opts) => {
633
+ const chalk = (await import('chalk')).default;
634
+ const ora = (await import('ora')).default;
635
+ if (opts.list) {
636
+ const { listParticlePresets, getParticlePreset } = await import('./lighting/index.js');
637
+ console.log(chalk.bold.cyan('\nAvailable Particle Effects:\n'));
638
+ for (const name of listParticlePresets()) {
639
+ const preset = getParticlePreset(name);
640
+ console.log(` ${chalk.green(name)}: ${preset.description}`);
641
+ }
642
+ return;
643
+ }
644
+ // Premium gate
645
+ const { requireLicense } = await import('./license.js');
646
+ await requireLicense('particles');
647
+ const spinner = ora('Generating particle effects...').start();
648
+ try {
649
+ if (opts.preset) {
650
+ const { getParticlePreset } = await import('./lighting/index.js');
651
+ const preset = getParticlePreset(opts.preset);
652
+ if (!preset) {
653
+ spinner.fail(chalk.red(`Unknown preset: ${opts.preset}`));
654
+ process.exit(1);
655
+ }
656
+ const { mkdir, writeFile } = await import('node:fs/promises');
657
+ const outDir = resolve(opts.output, 'particles');
658
+ await mkdir(outDir, { recursive: true });
659
+ await writeFile(join(outDir, `${opts.preset}.tscn`), preset.scene);
660
+ spinner.succeed(chalk.green(`Particle "${opts.preset}" → ${opts.output}/particles/`));
661
+ }
662
+ else {
663
+ const { writeAllParticlePresets } = await import('./lighting/index.js');
664
+ const files = await writeAllParticlePresets(resolve(opts.output));
665
+ spinner.succeed(chalk.green(`${files.length} particle effects → ${opts.output}/particles/`));
666
+ }
667
+ }
668
+ catch (err) {
669
+ spinner.fail(chalk.red('Failed'));
670
+ console.error(err);
671
+ process.exit(1);
672
+ }
673
+ });
674
+ // === SFX COMMAND ===
675
+ program
676
+ .command('sfx')
677
+ .description('Generate 8-bit sound effects for your game')
678
+ .option('-o, --output <path>', 'Output directory', './output/audio')
679
+ .option('-c, --category <category>', 'Only generate sounds from this category')
680
+ .option('--list', 'List all available SFX presets')
681
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
682
+ .action(async (opts) => {
683
+ const chalk = (await import('chalk')).default;
684
+ const ora = (await import('ora')).default;
685
+ if (opts.list) {
686
+ const { getPresetsByCategory } = await import('./audio/index.js');
687
+ const grouped = getPresetsByCategory();
688
+ for (const [category, presets] of Object.entries(grouped)) {
689
+ console.log(chalk.bold.cyan(`\n${category.toUpperCase()} (${presets.length})`));
690
+ for (const preset of presets) {
691
+ console.log(` ${chalk.green(preset.name.toLowerCase().replaceAll(/\s+/g, '_'))}: ${preset.description}`);
692
+ }
693
+ }
694
+ const total = Object.values(grouped).reduce((sum, p) => sum + p.length, 0);
695
+ console.log(chalk.bold(`\n${total} sound effects available`));
696
+ return;
697
+ }
698
+ // Premium gate
699
+ const { requireLicense } = await import('./license.js');
700
+ await requireLicense('sfx');
701
+ const spinner = ora('Generating sound effects...').start();
702
+ try {
703
+ const { generateAllSfx } = await import('./audio/index.js');
704
+ const results = await generateAllSfx(opts.output, {
705
+ category: opts.category,
706
+ });
707
+ spinner.succeed(chalk.green(`Generated ${results.length} sound effects → ${opts.output}/sfx/`));
708
+ // Group results by category for summary
709
+ const byCategory = {};
710
+ for (const r of results) {
711
+ byCategory[r.category] = (byCategory[r.category] || 0) + 1;
712
+ }
713
+ for (const [cat, count] of Object.entries(byCategory)) {
714
+ console.log(` ${chalk.cyan(cat)}: ${count} sounds`);
715
+ }
716
+ }
717
+ catch (err) {
718
+ spinner.fail(chalk.red('SFX generation failed'));
719
+ console.error(err);
720
+ process.exit(1);
721
+ }
722
+ });
723
+ // === UI KIT COMMAND ===
724
+ program
725
+ .command('ui')
726
+ .description('Generate pixel-art UI kit (panels, buttons, bars, slots, cursors)')
727
+ .option('-o, --output <path>', 'Output directory', './output/ui')
728
+ .option('-t, --theme <name>', 'Theme name (medieval, dark, nature, ice, fire, royal)', 'medieval')
729
+ .option('--all-themes', 'Generate for ALL themes')
730
+ .option('--list-themes', 'List available themes')
731
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
732
+ .action(async (opts) => {
733
+ const chalk = (await import('chalk')).default;
734
+ const ora = (await import('ora')).default;
735
+ if (opts.listThemes) {
736
+ const { listThemes, UI_THEMES } = await import('./ui/index.js');
737
+ console.log(chalk.bold.cyan('\nAvailable UI Themes:\n'));
738
+ for (const name of listThemes()) {
739
+ const t = UI_THEMES[name];
740
+ console.log(` ${chalk.green(name)}: ${t.name}`);
741
+ }
742
+ return;
743
+ }
744
+ // Premium gate
745
+ const { requireLicense } = await import('./license.js');
746
+ await requireLicense('ui');
747
+ const spinner = ora('Generating UI kit...').start();
748
+ try {
749
+ const { generateUIKit, listThemes } = await import('./ui/index.js');
750
+ if (opts.allThemes) {
751
+ const themes = listThemes();
752
+ let total = 0;
753
+ for (const theme of themes) {
754
+ const result = await generateUIKit(opts.output, theme);
755
+ total += result.totalAssets;
756
+ spinner.text = `Generated ${theme} theme (${result.totalAssets} assets)...`;
757
+ }
758
+ spinner.succeed(chalk.green(`Generated ${total} UI assets across ${themes.length} themes → ${opts.output}/ui/`));
759
+ }
760
+ else {
761
+ const result = await generateUIKit(opts.output, opts.theme);
762
+ spinner.succeed(chalk.green(`Generated ${result.totalAssets} UI assets (${opts.theme}) → ${opts.output}/ui/`));
763
+ console.log(` ${chalk.cyan('Panels')}: ${result.panels.length}`);
764
+ console.log(` ${chalk.cyan('Buttons')}: ${result.buttons.length}`);
765
+ console.log(` ${chalk.cyan('Frames')}: ${result.frames.length}`);
766
+ console.log(` ${chalk.cyan('Bars')}: ${result.bars.length}`);
767
+ console.log(` ${chalk.cyan('Slots')}: ${result.slots.length}`);
768
+ console.log(` ${chalk.cyan('Cursors')}: ${result.cursors.length}`);
769
+ }
770
+ }
771
+ catch (err) {
772
+ spinner.fail(chalk.red('UI generation failed'));
773
+ console.error(err);
774
+ process.exit(1);
775
+ }
776
+ });
777
+ // === PORTRAIT COMMAND ===
778
+ program
779
+ .command('portrait')
780
+ .description('Extract character portrait from a composited spritesheet')
781
+ .requiredOption('-i, --input <path>', 'Path to character spritesheet PNG')
782
+ .option('-n, --name <name>', 'Character name for output files', 'character')
783
+ .option('-o, --output <path>', 'Output directory', './output')
784
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
785
+ .action(async (opts) => {
786
+ const chalk = (await import('chalk')).default;
787
+ const ora = (await import('ora')).default;
788
+ // Premium gate
789
+ const { requireLicense } = await import('./license.js');
790
+ await requireLicense('portrait');
791
+ const { readFile } = await import('node:fs/promises');
792
+ const spinner = ora('Extracting portrait...').start();
793
+ try {
794
+ const { extractPortrait } = await import('./ui/portrait.js');
795
+ const sheetBuffer = await readFile(resolve(opts.input));
796
+ const results = await extractPortrait(sheetBuffer, opts.output, opts.name);
797
+ spinner.succeed(chalk.green(`Extracted ${results.length} portrait sizes → ${opts.output}/portraits/`));
798
+ for (const r of results) {
799
+ console.log(` ${chalk.cyan(r.size + 'px')}: ${r.path}`);
800
+ }
801
+ }
802
+ catch (err) {
803
+ spinner.fail(chalk.red('Portrait extraction failed'));
804
+ console.error(err);
805
+ process.exit(1);
806
+ }
807
+ });
808
+ // === PROPS COMMAND ===
809
+ program
810
+ .command('props')
811
+ .description('Generate pixel-art prop sprites (chests, barrels, torches, etc.)')
812
+ .option('-o, --output <path>', 'Output directory', './output')
813
+ .option('--list', 'List available props')
814
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
815
+ .action(async (opts) => {
816
+ const chalk = (await import('chalk')).default;
817
+ const ora = (await import('ora')).default;
818
+ if (opts.list) {
819
+ const { listProps } = await import('./ui/props.js');
820
+ const props = listProps();
821
+ console.log(chalk.bold.cyan(`\nAvailable Props (${props.length}):\n`));
822
+ for (const p of props) {
823
+ console.log(` ${chalk.green(p.name)} (${p.width}x${p.height}) — ${p.description}`);
824
+ }
825
+ return;
826
+ }
827
+ // Premium gate
828
+ const { requireLicense } = await import('./license.js');
829
+ await requireLicense('props');
830
+ const spinner = ora('Generating props...').start();
831
+ try {
832
+ const { generateAllProps } = await import('./ui/props.js');
833
+ const paths = await generateAllProps(opts.output);
834
+ spinner.succeed(chalk.green(`Generated ${paths.length} props → ${opts.output}/props/`));
835
+ }
836
+ catch (err) {
837
+ spinner.fail(chalk.red('Props generation failed'));
838
+ console.error(err);
839
+ process.exit(1);
840
+ }
841
+ });
842
+ // === ICONS COMMAND ===
843
+ program
844
+ .command('icons')
845
+ .description('Generate pixel-art item icons + atlas')
846
+ .option('-o, --output <path>', 'Output directory', './output')
847
+ .option('--list', 'List available icons')
848
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
849
+ .action(async (opts) => {
850
+ const chalk = (await import('chalk')).default;
851
+ const ora = (await import('ora')).default;
852
+ if (opts.list) {
853
+ const { listIcons } = await import('./ui/icons.js');
854
+ const icons = listIcons();
855
+ const byCategory = {};
856
+ for (const i of icons) {
857
+ if (!byCategory[i.category])
858
+ byCategory[i.category] = [];
859
+ byCategory[i.category].push(i);
860
+ }
861
+ for (const [cat, items] of Object.entries(byCategory)) {
862
+ console.log(chalk.bold.cyan(`\n${cat.toUpperCase()} (${items.length})`));
863
+ for (const i of items) {
864
+ console.log(` ${chalk.green(i.name)}: ${i.description}`);
865
+ }
866
+ }
867
+ console.log(chalk.bold(`\n${icons.length} icons available`));
868
+ return;
869
+ }
870
+ // Premium gate
871
+ const { requireLicense } = await import('./license.js');
872
+ await requireLicense('icons');
873
+ const spinner = ora('Generating icons...').start();
874
+ try {
875
+ const { generateAllIcons } = await import('./ui/icons.js');
876
+ const { icons, atlas } = await generateAllIcons(opts.output);
877
+ spinner.succeed(chalk.green(`Generated ${icons.length} icons + atlas → ${opts.output}/icons/`));
878
+ console.log(` ${chalk.cyan('Atlas')}: ${atlas}`);
879
+ }
880
+ catch (err) {
881
+ spinner.fail(chalk.red('Icon generation failed'));
882
+ console.error(err);
883
+ process.exit(1);
884
+ }
885
+ });
886
+ // === SYSTEMS COMMAND ===
887
+ program
888
+ .command('systems')
889
+ .description('Generate GDScript game systems (enemy AI, inventory, dialog, etc.)')
890
+ .option('-o, --output <path>', 'Output directory', './output')
891
+ .option('-s, --system <names...>', 'Specific systems to generate (default: all)')
892
+ .option('--list', 'List available game systems')
893
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
894
+ .action(async (opts) => {
895
+ const chalk = (await import('chalk')).default;
896
+ const ora = (await import('ora')).default;
897
+ if (opts.list) {
898
+ const { getAllSystems } = await import('./systems/index.js');
899
+ const systems = getAllSystems();
900
+ console.log(chalk.bold.cyan(`\nAvailable Game Systems (${systems.length}):\n`));
901
+ for (const sys of systems) {
902
+ const deps = sys.dependencies.length > 0 ? ` (needs: ${sys.dependencies.join(', ')})` : '';
903
+ const autoloads = sys.autoloads.length > 0 ? ` [autoload: ${sys.autoloads.map(a => a.name).join(', ')}]` : '';
904
+ console.log(` ${chalk.green(sys.name)}: ${sys.description}${deps}${autoloads}`);
905
+ console.log(` Files: ${sys.files.map(f => f.path).join(', ')}`);
906
+ }
907
+ return;
908
+ }
909
+ // Premium gate
910
+ const { requireLicense } = await import('./license.js');
911
+ await requireLicense('systems');
912
+ const spinner = ora('Generating game systems...').start();
913
+ try {
914
+ const { writeGameSystems } = await import('./systems/writer.js');
915
+ const result = await writeGameSystems(resolve(opts.output), opts.system);
916
+ spinner.succeed(chalk.green(`Generated ${result.filesWritten} files across ${result.systems.length} systems → ${opts.output}/`));
917
+ console.log(` ${chalk.cyan('Systems')}: ${result.systems.join(', ')}`);
918
+ if (result.autoloads.length > 0) {
919
+ console.log(` ${chalk.yellow('Autoloads (add to project.godot):')}`);
920
+ for (const al of result.autoloads) {
921
+ console.log(` ${al.name} = "${al.path}"`);
922
+ }
923
+ }
924
+ if (result.inputActions.length > 0) {
925
+ console.log(` ${chalk.yellow('Input Actions (add to project settings):')}`);
926
+ for (const action of result.inputActions) {
927
+ console.log(` ${action}`);
928
+ }
929
+ }
930
+ }
931
+ catch (err) {
932
+ spinner.fail(chalk.red('System generation failed'));
933
+ console.error(err);
934
+ process.exit(1);
935
+ }
936
+ });
937
+ program.parse();
938
+ //# sourceMappingURL=cli.js.map