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.
- package/CREDITS.csv +22985 -0
- package/LICENSE +674 -0
- package/README.md +281 -0
- package/assets/music/.gitkeep +0 -0
- package/dist/character/batch.d.ts +17 -0
- package/dist/character/batch.js +48 -0
- package/dist/character/batch.js.map +1 -0
- package/dist/character/composer.d.ts +3 -0
- package/dist/character/composer.js +164 -0
- package/dist/character/composer.js.map +1 -0
- package/dist/character/definitions.d.ts +16 -0
- package/dist/character/definitions.js +116 -0
- package/dist/character/definitions.js.map +1 -0
- package/dist/character/presets.d.ts +6 -0
- package/dist/character/presets.js +246 -0
- package/dist/character/presets.js.map +1 -0
- package/dist/character/slicer.d.ts +8 -0
- package/dist/character/slicer.js +66 -0
- package/dist/character/slicer.js.map +1 -0
- package/dist/character/types.d.ts +48 -0
- package/dist/character/types.js +32 -0
- package/dist/character/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +938 -0
- package/dist/export/frames.d.ts +5 -0
- package/dist/export/frames.js +15 -0
- package/dist/export/frames.js.map +1 -0
- package/dist/export/godot.d.ts +17 -0
- package/dist/export/godot.js +464 -0
- package/dist/export/godot.js.map +1 -0
- package/dist/export/types.d.ts +11 -0
- package/dist/export/types.js +2 -0
- package/dist/export/types.js.map +1 -0
- package/dist/license.d.ts +49 -0
- package/dist/license.js +271 -0
- package/dist/map/cellular.d.ts +3 -0
- package/dist/map/cellular.js +191 -0
- package/dist/map/cellular.js.map +1 -0
- package/dist/map/dungeon.d.ts +3 -0
- package/dist/map/dungeon.js +238 -0
- package/dist/map/dungeon.js.map +1 -0
- package/dist/map/multifloor.d.ts +20 -0
- package/dist/map/multifloor.js +57 -0
- package/dist/map/multifloor.js.map +1 -0
- package/dist/map/overworld.d.ts +3 -0
- package/dist/map/overworld.js +205 -0
- package/dist/map/overworld.js.map +1 -0
- package/dist/map/town.d.ts +7 -0
- package/dist/map/town.js +181 -0
- package/dist/map/town.js.map +1 -0
- package/dist/map/types.d.ts +65 -0
- package/dist/map/types.js +16 -0
- package/dist/map/types.js.map +1 -0
- package/dist/map/wfc.d.ts +18 -0
- package/dist/map/wfc.js +192 -0
- package/dist/map/wfc.js.map +1 -0
- package/dist/tileset/atlas.d.ts +15 -0
- package/dist/tileset/atlas.js +55 -0
- package/dist/tileset/atlas.js.map +1 -0
- package/dist/tileset/registry.d.ts +12 -0
- package/dist/tileset/registry.js +71 -0
- package/dist/tileset/registry.js.map +1 -0
- package/dist/tileset/terrain.d.ts +3 -0
- package/dist/tileset/terrain.js +110 -0
- package/dist/tileset/terrain.js.map +1 -0
- package/dist/utils/credits.d.ts +11 -0
- package/dist/utils/credits.js +74 -0
- package/dist/utils/credits.js.map +1 -0
- package/dist/utils/image.d.ts +17 -0
- package/dist/utils/image.js +94 -0
- package/dist/utils/image.js.map +1 -0
- package/dist/utils/rng.d.ts +18 -0
- package/dist/utils/rng.js +48 -0
- package/dist/utils/rng.js.map +1 -0
- 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
|