create-threejs-game 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,15 +7,28 @@ Scaffold a Three.js game project with AI-assisted design documents.
7
7
  Before running the CLI, have these ready:
8
8
 
9
9
  ### 1. 3D Assets (Required)
10
- Download a GLTF asset pack from:
10
+ Download GLTF asset packs from:
11
11
  - [itch.io](https://itch.io/game-assets/tag-3d)
12
12
  - [Kenney.nl](https://kenney.nl/assets)
13
13
  - [Quaternius](https://quaternius.com/)
14
14
 
15
15
  The CLI will ask for the path to your downloaded assets folder and validate it contains `.gltf` or `.glb` files.
16
16
 
17
- ### 2. Preview Image (Recommended)
18
- Most asset packs include a `Preview.jpg`. If not, take a screenshot of your assets. This is used by the AI to generate concept mockups.
17
+ **Multi-Pack Support:** You can combine multiple asset packs by organizing them in subdirectories (nested to any depth):
18
+ ```
19
+ my-assets/
20
+ ├── characters/
21
+ │ ├── humans/ # Has Preview.jpg
22
+ │ └── monsters/ # Has Preview.jpg
23
+ ├── buildings/ # Has Preview.jpg
24
+ └── environment/
25
+ ├── trees/ # Has Preview.jpg
26
+ └── rocks/ # Has Preview.jpg
27
+ ```
28
+ The CLI recursively finds all directories with previews and combines them into a single grid.
29
+
30
+ ### 2. Preview Image (Auto-generated for multi-packs)
31
+ Most asset packs include a `Preview.jpg`. For multi-pack directories, the CLI automatically combines all subdirectory previews into one image. For single packs without a preview, take a screenshot of your assets.
19
32
 
20
33
  ### 3. API Keys (Required for automation)
21
34
  Set these as environment variables for the smoothest experience:
package/bin/cli.js CHANGED
@@ -16,6 +16,14 @@ const path = require('path');
16
16
  const readline = require('readline');
17
17
  const { execSync, spawn } = require('child_process');
18
18
 
19
+ // Sharp for image processing (optional, for combining previews)
20
+ let sharp;
21
+ try {
22
+ sharp = require('sharp');
23
+ } catch (e) {
24
+ // Sharp not available - preview combining will be skipped
25
+ }
26
+
19
27
  // Colors for terminal output
20
28
  const colors = {
21
29
  reset: '\x1b[0m',
@@ -91,6 +99,131 @@ function copyDir(src, dest, exclude = []) {
91
99
  }
92
100
  }
93
101
 
102
+ // Find preview image in a directory
103
+ function findPreview(dir) {
104
+ const previewNames = ['Preview.jpg', 'Preview.png', 'preview.jpg', 'preview.png',
105
+ 'Preview.jpeg', 'preview.jpeg'];
106
+ for (const name of previewNames) {
107
+ const previewPath = path.join(dir, name);
108
+ if (fs.existsSync(previewPath)) {
109
+ return previewPath;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ // Recursively find all directories with preview images
116
+ function findPacksWithPreviews(dir, basePath = dir) {
117
+ const packs = [];
118
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
119
+
120
+ // Check if this directory has a preview
121
+ const preview = findPreview(dir);
122
+ if (preview && dir !== basePath) {
123
+ // Use relative path from base as the name
124
+ const relativePath = path.relative(basePath, dir);
125
+ packs.push({
126
+ name: relativePath,
127
+ path: dir,
128
+ preview: preview
129
+ });
130
+ }
131
+
132
+ // Recursively check subdirectories
133
+ for (const entry of entries) {
134
+ if (!entry.isDirectory()) continue;
135
+ if (entry.name.startsWith('.')) continue; // Skip hidden dirs
136
+
137
+ const subdirPath = path.join(dir, entry.name);
138
+ const subPacks = findPacksWithPreviews(subdirPath, basePath);
139
+ packs.push(...subPacks);
140
+ }
141
+
142
+ return packs;
143
+ }
144
+
145
+ // Detect if directory is a multi-pack (has subdirs with previews)
146
+ function detectMultiPack(sourceDir) {
147
+ const packsWithPreviews = findPacksWithPreviews(sourceDir, sourceDir);
148
+
149
+ // Consider it a multi-pack if at least 2 directories have previews
150
+ if (packsWithPreviews.length >= 2) {
151
+ return packsWithPreviews;
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ // Combine preview images into a grid
158
+ async function combinePreviews(packs, outputPath) {
159
+ if (!sharp) {
160
+ console.log(c('yellow', ' ⚠ sharp not available - skipping preview combination'));
161
+ return null;
162
+ }
163
+
164
+ const cellWidth = 512;
165
+ const cellHeight = 512;
166
+ const padding = 10;
167
+ const backgroundColor = { r: 30, g: 30, b: 30, alpha: 1 };
168
+
169
+ const cols = Math.ceil(Math.sqrt(packs.length));
170
+ const rows = Math.ceil(packs.length / cols);
171
+ const totalWidth = cols * cellWidth + (cols + 1) * padding;
172
+ const totalHeight = rows * cellHeight + (rows + 1) * padding;
173
+
174
+ console.log(c('dim', ` Creating ${cols}x${rows} combined preview...`));
175
+
176
+ const composites = [];
177
+
178
+ for (let i = 0; i < packs.length; i++) {
179
+ const pack = packs[i];
180
+ const col = i % cols;
181
+ const row = Math.floor(i / cols);
182
+
183
+ const x = padding + col * (cellWidth + padding);
184
+ const y = padding + row * (cellHeight + padding);
185
+
186
+ try {
187
+ const resized = await sharp(pack.preview)
188
+ .resize(cellWidth, cellHeight, {
189
+ fit: 'contain',
190
+ background: backgroundColor
191
+ })
192
+ .toBuffer();
193
+
194
+ composites.push({
195
+ input: resized,
196
+ left: x,
197
+ top: y
198
+ });
199
+ } catch (err) {
200
+ console.log(c('yellow', ` ⚠ Failed to process ${pack.name}: ${err.message}`));
201
+ }
202
+ }
203
+
204
+ if (composites.length === 0) return null;
205
+
206
+ // Ensure output directory exists
207
+ const outputDir = path.dirname(outputPath);
208
+ if (!fs.existsSync(outputDir)) {
209
+ fs.mkdirSync(outputDir, { recursive: true });
210
+ }
211
+
212
+ await sharp({
213
+ create: {
214
+ width: totalWidth,
215
+ height: totalHeight,
216
+ channels: 4,
217
+ background: backgroundColor
218
+ }
219
+ })
220
+ .composite(composites)
221
+ .jpeg({ quality: 90 })
222
+ .toFile(outputPath);
223
+
224
+ return outputPath;
225
+ }
226
+
94
227
  // Run a script
95
228
  function runScript(scriptPath, args = [], cwd) {
96
229
  return new Promise((resolve, reject) => {
@@ -307,7 +440,21 @@ async function main() {
307
440
  fs.mkdirSync(assetsDir, { recursive: true });
308
441
 
309
442
  // Copy assets if path was provided
443
+ let isMultiPack = false;
444
+ let multiPackInfo = null;
445
+
310
446
  if (assetsSourcePath) {
447
+ // Check if this is a multi-pack directory
448
+ multiPackInfo = detectMultiPack(assetsSourcePath);
449
+ isMultiPack = multiPackInfo !== null;
450
+
451
+ if (isMultiPack) {
452
+ console.log(c('cyan', ` Detected multi-pack with ${multiPackInfo.length} asset packs:`));
453
+ for (const pack of multiPackInfo) {
454
+ console.log(c('dim', ` - ${pack.name}`));
455
+ }
456
+ }
457
+
311
458
  console.log(c('dim', ' Copying assets...'));
312
459
  copyDir(assetsSourcePath, assetsDir, ['node_modules', '.git', '.DS_Store']);
313
460
 
@@ -326,6 +473,17 @@ async function main() {
326
473
  };
327
474
  const fileCount = countFiles(assetsDir);
328
475
  console.log(c('green', ' ✓ ') + `public/assets/${gameName}/ (${fileCount} files copied)`);
476
+
477
+ // Combine previews if multi-pack
478
+ if (isMultiPack && sharp) {
479
+ try {
480
+ const combinedPreviewPath = path.join(assetsDir, 'Preview.jpg');
481
+ await combinePreviews(multiPackInfo, combinedPreviewPath);
482
+ console.log(c('green', ' ✓ ') + 'Combined preview image generated');
483
+ } catch (err) {
484
+ console.log(c('yellow', ` ⚠ Could not combine previews: ${err.message}`));
485
+ }
486
+ }
329
487
  } else {
330
488
  fs.writeFileSync(path.join(assetsDir, '.gitkeep'), '');
331
489
  console.log(c('green', ' ✓ ') + `public/assets/${gameName}/`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-threejs-game",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
4
4
  "description": "Scaffold a Three.js game project with AI-assisted design documents",
5
5
  "bin": {
6
6
  "create-threejs-game": "./bin/cli.js"
@@ -25,5 +25,8 @@
25
25
  },
26
26
  "engines": {
27
27
  "node": ">=18.0.0"
28
+ },
29
+ "dependencies": {
30
+ "sharp": "^0.33.0"
28
31
  }
29
32
  }
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Combine Preview Images
5
+ *
6
+ * Scans a directory for subdirectories containing Preview.jpg/png files
7
+ * and combines them into a single grid image.
8
+ *
9
+ * Usage: node combine-previews.js <source-dir> <output-path>
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // We'll use sharp for image processing - it's fast and handles this well
16
+ let sharp;
17
+ try {
18
+ sharp = require('sharp');
19
+ } catch (e) {
20
+ console.error('Error: sharp module not found.');
21
+ console.error('Install it with: npm install sharp');
22
+ process.exit(1);
23
+ }
24
+
25
+ /**
26
+ * Find preview image in a directory
27
+ */
28
+ function findPreview(dir) {
29
+ const previewNames = ['Preview.jpg', 'Preview.png', 'preview.jpg', 'preview.png',
30
+ 'Preview.jpeg', 'preview.jpeg'];
31
+ for (const name of previewNames) {
32
+ const previewPath = path.join(dir, name);
33
+ if (fs.existsSync(previewPath)) {
34
+ return previewPath;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Recursively find all preview images in subdirectories (any depth)
42
+ */
43
+ function findPreviews(sourceDir, basePath = sourceDir) {
44
+ const previews = [];
45
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
46
+
47
+ // Check if this directory has a preview (skip the root)
48
+ if (sourceDir !== basePath) {
49
+ const preview = findPreview(sourceDir);
50
+ if (preview) {
51
+ const relativePath = path.relative(basePath, sourceDir);
52
+ previews.push({
53
+ path: preview,
54
+ name: relativePath
55
+ });
56
+ }
57
+ }
58
+
59
+ // Recursively check subdirectories
60
+ for (const entry of entries) {
61
+ if (!entry.isDirectory()) continue;
62
+ if (entry.name.startsWith('.')) continue; // Skip hidden dirs
63
+
64
+ const subdirPath = path.join(sourceDir, entry.name);
65
+ const subPreviews = findPreviews(subdirPath, basePath);
66
+ previews.push(...subPreviews);
67
+ }
68
+
69
+ return previews;
70
+ }
71
+
72
+ /**
73
+ * Calculate grid dimensions for N images
74
+ */
75
+ function calculateGrid(count) {
76
+ const cols = Math.ceil(Math.sqrt(count));
77
+ const rows = Math.ceil(count / cols);
78
+ return { cols, rows };
79
+ }
80
+
81
+ /**
82
+ * Combine images into a grid
83
+ */
84
+ async function combineImages(previews, outputPath, options = {}) {
85
+ const {
86
+ cellWidth = 512,
87
+ cellHeight = 512,
88
+ padding = 10,
89
+ backgroundColor = { r: 30, g: 30, b: 30, alpha: 1 }
90
+ } = options;
91
+
92
+ if (previews.length === 0) {
93
+ console.log('No preview images found.');
94
+ return null;
95
+ }
96
+
97
+ if (previews.length === 1) {
98
+ // Just copy the single preview
99
+ fs.copyFileSync(previews[0].path, outputPath);
100
+ console.log(`Copied single preview: ${previews[0].name}`);
101
+ return outputPath;
102
+ }
103
+
104
+ const { cols, rows } = calculateGrid(previews.length);
105
+ const totalWidth = cols * cellWidth + (cols + 1) * padding;
106
+ const totalHeight = rows * cellHeight + (rows + 1) * padding;
107
+
108
+ console.log(`Creating ${cols}x${rows} grid (${totalWidth}x${totalHeight}px) for ${previews.length} previews...`);
109
+
110
+ // Prepare each image
111
+ const composites = [];
112
+
113
+ for (let i = 0; i < previews.length; i++) {
114
+ const preview = previews[i];
115
+ const col = i % cols;
116
+ const row = Math.floor(i / cols);
117
+
118
+ const x = padding + col * (cellWidth + padding);
119
+ const y = padding + row * (cellHeight + padding);
120
+
121
+ try {
122
+ // Resize image to fit cell while maintaining aspect ratio
123
+ const resized = await sharp(preview.path)
124
+ .resize(cellWidth, cellHeight, {
125
+ fit: 'contain',
126
+ background: backgroundColor
127
+ })
128
+ .toBuffer();
129
+
130
+ composites.push({
131
+ input: resized,
132
+ left: x,
133
+ top: y
134
+ });
135
+
136
+ console.log(` [${i + 1}/${previews.length}] ${preview.name}`);
137
+ } catch (err) {
138
+ console.error(` Failed to process ${preview.name}: ${err.message}`);
139
+ }
140
+ }
141
+
142
+ // Create the combined image
143
+ const combined = sharp({
144
+ create: {
145
+ width: totalWidth,
146
+ height: totalHeight,
147
+ channels: 4,
148
+ background: backgroundColor
149
+ }
150
+ });
151
+
152
+ await combined
153
+ .composite(composites)
154
+ .jpeg({ quality: 90 })
155
+ .toFile(outputPath);
156
+
157
+ console.log(`\nCombined preview saved to: ${outputPath}`);
158
+ return outputPath;
159
+ }
160
+
161
+ /**
162
+ * Main function
163
+ */
164
+ async function main() {
165
+ const args = process.argv.slice(2);
166
+
167
+ if (args.length < 2) {
168
+ console.log('Usage: node combine-previews.js <source-dir> <output-path>');
169
+ console.log('');
170
+ console.log('Scans source-dir for subdirectories with Preview.jpg/png');
171
+ console.log('and combines them into a single grid image.');
172
+ process.exit(1);
173
+ }
174
+
175
+ const sourceDir = args[0];
176
+ const outputPath = args[1];
177
+
178
+ if (!fs.existsSync(sourceDir)) {
179
+ console.error(`Error: Source directory not found: ${sourceDir}`);
180
+ process.exit(1);
181
+ }
182
+
183
+ const previews = findPreviews(sourceDir);
184
+
185
+ if (previews.length === 0) {
186
+ console.log('No subdirectories with preview images found.');
187
+ console.log('Looking for: Preview.jpg, Preview.png, preview.jpg, preview.png');
188
+ process.exit(0);
189
+ }
190
+
191
+ console.log(`Found ${previews.length} preview images:\n`);
192
+
193
+ // Ensure output directory exists
194
+ const outputDir = path.dirname(outputPath);
195
+ if (!fs.existsSync(outputDir)) {
196
+ fs.mkdirSync(outputDir, { recursive: true });
197
+ }
198
+
199
+ await combineImages(previews, outputPath);
200
+ }
201
+
202
+ // Export for use as module
203
+ module.exports = { findPreview, findPreviews, combineImages, calculateGrid };
204
+
205
+ // Run if called directly
206
+ if (require.main === module) {
207
+ main().catch(err => {
208
+ console.error('Error:', err.message);
209
+ process.exit(1);
210
+ });
211
+ }