create-threejs-game 1.0.5 → 1.1.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/README.md CHANGED
@@ -7,15 +7,24 @@ 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:
18
+ ```
19
+ my-assets/
20
+ ├── characters/ # Asset pack 1 (with Preview.jpg)
21
+ ├── buildings/ # Asset pack 2 (with Preview.jpg)
22
+ └── environment/ # Asset pack 3 (with Preview.jpg)
23
+ ```
24
+ The CLI auto-detects this structure and combines all preview images into a single grid.
25
+
26
+ ### 2. Preview Image (Auto-generated for multi-packs)
27
+ 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
28
 
20
29
  ### 3. API Keys (Required for automation)
21
30
  Set these as environment variables for the smoothest experience:
@@ -74,6 +83,8 @@ my-game/
74
83
  ├── plans/ # Generated implementation plans
75
84
  ├── prompts/ # Prompt templates (fallback/reference)
76
85
  ├── public/
86
+ │ ├── {game}/
87
+ │ │ └── concept.jpg # Generated mockup
77
88
  │ └── assets/{game}/ # Your 3D assets (copied by CLI)
78
89
  ├── scripts/
79
90
  │ ├── config.json # API keys and game config
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,118 @@ 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
+ // Detect if directory is a multi-pack (has subdirs with previews)
116
+ function detectMultiPack(sourceDir) {
117
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
118
+ const subdirs = entries.filter(e => e.isDirectory());
119
+
120
+ if (subdirs.length === 0) return null;
121
+
122
+ const packsWithPreviews = [];
123
+
124
+ for (const subdir of subdirs) {
125
+ const subdirPath = path.join(sourceDir, subdir.name);
126
+ const preview = findPreview(subdirPath);
127
+ if (preview) {
128
+ packsWithPreviews.push({
129
+ name: subdir.name,
130
+ path: subdirPath,
131
+ preview: preview
132
+ });
133
+ }
134
+ }
135
+
136
+ // Consider it a multi-pack if at least 2 subdirs have previews
137
+ if (packsWithPreviews.length >= 2) {
138
+ return packsWithPreviews;
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ // Combine preview images into a grid
145
+ async function combinePreviews(packs, outputPath) {
146
+ if (!sharp) {
147
+ console.log(c('yellow', ' ⚠ sharp not available - skipping preview combination'));
148
+ return null;
149
+ }
150
+
151
+ const cellWidth = 512;
152
+ const cellHeight = 512;
153
+ const padding = 10;
154
+ const backgroundColor = { r: 30, g: 30, b: 30, alpha: 1 };
155
+
156
+ const cols = Math.ceil(Math.sqrt(packs.length));
157
+ const rows = Math.ceil(packs.length / cols);
158
+ const totalWidth = cols * cellWidth + (cols + 1) * padding;
159
+ const totalHeight = rows * cellHeight + (rows + 1) * padding;
160
+
161
+ console.log(c('dim', ` Creating ${cols}x${rows} combined preview...`));
162
+
163
+ const composites = [];
164
+
165
+ for (let i = 0; i < packs.length; i++) {
166
+ const pack = packs[i];
167
+ const col = i % cols;
168
+ const row = Math.floor(i / cols);
169
+
170
+ const x = padding + col * (cellWidth + padding);
171
+ const y = padding + row * (cellHeight + padding);
172
+
173
+ try {
174
+ const resized = await sharp(pack.preview)
175
+ .resize(cellWidth, cellHeight, {
176
+ fit: 'contain',
177
+ background: backgroundColor
178
+ })
179
+ .toBuffer();
180
+
181
+ composites.push({
182
+ input: resized,
183
+ left: x,
184
+ top: y
185
+ });
186
+ } catch (err) {
187
+ console.log(c('yellow', ` ⚠ Failed to process ${pack.name}: ${err.message}`));
188
+ }
189
+ }
190
+
191
+ if (composites.length === 0) return null;
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 sharp({
200
+ create: {
201
+ width: totalWidth,
202
+ height: totalHeight,
203
+ channels: 4,
204
+ background: backgroundColor
205
+ }
206
+ })
207
+ .composite(composites)
208
+ .jpeg({ quality: 90 })
209
+ .toFile(outputPath);
210
+
211
+ return outputPath;
212
+ }
213
+
94
214
  // Run a script
95
215
  function runScript(scriptPath, args = [], cwd) {
96
216
  return new Promise((resolve, reject) => {
@@ -307,7 +427,21 @@ async function main() {
307
427
  fs.mkdirSync(assetsDir, { recursive: true });
308
428
 
309
429
  // Copy assets if path was provided
430
+ let isMultiPack = false;
431
+ let multiPackInfo = null;
432
+
310
433
  if (assetsSourcePath) {
434
+ // Check if this is a multi-pack directory
435
+ multiPackInfo = detectMultiPack(assetsSourcePath);
436
+ isMultiPack = multiPackInfo !== null;
437
+
438
+ if (isMultiPack) {
439
+ console.log(c('cyan', ` Detected multi-pack with ${multiPackInfo.length} asset packs:`));
440
+ for (const pack of multiPackInfo) {
441
+ console.log(c('dim', ` - ${pack.name}`));
442
+ }
443
+ }
444
+
311
445
  console.log(c('dim', ' Copying assets...'));
312
446
  copyDir(assetsSourcePath, assetsDir, ['node_modules', '.git', '.DS_Store']);
313
447
 
@@ -326,6 +460,17 @@ async function main() {
326
460
  };
327
461
  const fileCount = countFiles(assetsDir);
328
462
  console.log(c('green', ' ✓ ') + `public/assets/${gameName}/ (${fileCount} files copied)`);
463
+
464
+ // Combine previews if multi-pack
465
+ if (isMultiPack && sharp) {
466
+ try {
467
+ const combinedPreviewPath = path.join(assetsDir, 'Preview.jpg');
468
+ await combinePreviews(multiPackInfo, combinedPreviewPath);
469
+ console.log(c('green', ' ✓ ') + 'Combined preview image generated');
470
+ } catch (err) {
471
+ console.log(c('yellow', ` ⚠ Could not combine previews: ${err.message}`));
472
+ }
473
+ }
329
474
  } else {
330
475
  fs.writeFileSync(path.join(assetsDir, '.gitkeep'), '');
331
476
  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.5",
3
+ "version": "1.1.0",
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,193 @@
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 all preview images in subdirectories
27
+ */
28
+ function findPreviews(sourceDir) {
29
+ const previews = [];
30
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
31
+
32
+ for (const entry of entries) {
33
+ if (!entry.isDirectory()) continue;
34
+
35
+ const subdir = path.join(sourceDir, entry.name);
36
+ const previewNames = ['Preview.jpg', 'Preview.png', 'preview.jpg', 'preview.png',
37
+ 'Preview.jpeg', 'preview.jpeg'];
38
+
39
+ for (const name of previewNames) {
40
+ const previewPath = path.join(subdir, name);
41
+ if (fs.existsSync(previewPath)) {
42
+ previews.push({
43
+ path: previewPath,
44
+ name: entry.name
45
+ });
46
+ break;
47
+ }
48
+ }
49
+ }
50
+
51
+ return previews;
52
+ }
53
+
54
+ /**
55
+ * Calculate grid dimensions for N images
56
+ */
57
+ function calculateGrid(count) {
58
+ const cols = Math.ceil(Math.sqrt(count));
59
+ const rows = Math.ceil(count / cols);
60
+ return { cols, rows };
61
+ }
62
+
63
+ /**
64
+ * Combine images into a grid
65
+ */
66
+ async function combineImages(previews, outputPath, options = {}) {
67
+ const {
68
+ cellWidth = 512,
69
+ cellHeight = 512,
70
+ padding = 10,
71
+ backgroundColor = { r: 30, g: 30, b: 30, alpha: 1 }
72
+ } = options;
73
+
74
+ if (previews.length === 0) {
75
+ console.log('No preview images found.');
76
+ return null;
77
+ }
78
+
79
+ if (previews.length === 1) {
80
+ // Just copy the single preview
81
+ fs.copyFileSync(previews[0].path, outputPath);
82
+ console.log(`Copied single preview: ${previews[0].name}`);
83
+ return outputPath;
84
+ }
85
+
86
+ const { cols, rows } = calculateGrid(previews.length);
87
+ const totalWidth = cols * cellWidth + (cols + 1) * padding;
88
+ const totalHeight = rows * cellHeight + (rows + 1) * padding;
89
+
90
+ console.log(`Creating ${cols}x${rows} grid (${totalWidth}x${totalHeight}px) for ${previews.length} previews...`);
91
+
92
+ // Prepare each image
93
+ const composites = [];
94
+
95
+ for (let i = 0; i < previews.length; i++) {
96
+ const preview = previews[i];
97
+ const col = i % cols;
98
+ const row = Math.floor(i / cols);
99
+
100
+ const x = padding + col * (cellWidth + padding);
101
+ const y = padding + row * (cellHeight + padding);
102
+
103
+ try {
104
+ // Resize image to fit cell while maintaining aspect ratio
105
+ const resized = await sharp(preview.path)
106
+ .resize(cellWidth, cellHeight, {
107
+ fit: 'contain',
108
+ background: backgroundColor
109
+ })
110
+ .toBuffer();
111
+
112
+ composites.push({
113
+ input: resized,
114
+ left: x,
115
+ top: y
116
+ });
117
+
118
+ console.log(` [${i + 1}/${previews.length}] ${preview.name}`);
119
+ } catch (err) {
120
+ console.error(` Failed to process ${preview.name}: ${err.message}`);
121
+ }
122
+ }
123
+
124
+ // Create the combined image
125
+ const combined = sharp({
126
+ create: {
127
+ width: totalWidth,
128
+ height: totalHeight,
129
+ channels: 4,
130
+ background: backgroundColor
131
+ }
132
+ });
133
+
134
+ await combined
135
+ .composite(composites)
136
+ .jpeg({ quality: 90 })
137
+ .toFile(outputPath);
138
+
139
+ console.log(`\nCombined preview saved to: ${outputPath}`);
140
+ return outputPath;
141
+ }
142
+
143
+ /**
144
+ * Main function
145
+ */
146
+ async function main() {
147
+ const args = process.argv.slice(2);
148
+
149
+ if (args.length < 2) {
150
+ console.log('Usage: node combine-previews.js <source-dir> <output-path>');
151
+ console.log('');
152
+ console.log('Scans source-dir for subdirectories with Preview.jpg/png');
153
+ console.log('and combines them into a single grid image.');
154
+ process.exit(1);
155
+ }
156
+
157
+ const sourceDir = args[0];
158
+ const outputPath = args[1];
159
+
160
+ if (!fs.existsSync(sourceDir)) {
161
+ console.error(`Error: Source directory not found: ${sourceDir}`);
162
+ process.exit(1);
163
+ }
164
+
165
+ const previews = findPreviews(sourceDir);
166
+
167
+ if (previews.length === 0) {
168
+ console.log('No subdirectories with preview images found.');
169
+ console.log('Looking for: Preview.jpg, Preview.png, preview.jpg, preview.png');
170
+ process.exit(0);
171
+ }
172
+
173
+ console.log(`Found ${previews.length} preview images:\n`);
174
+
175
+ // Ensure output directory exists
176
+ const outputDir = path.dirname(outputPath);
177
+ if (!fs.existsSync(outputDir)) {
178
+ fs.mkdirSync(outputDir, { recursive: true });
179
+ }
180
+
181
+ await combineImages(previews, outputPath);
182
+ }
183
+
184
+ // Export for use as module
185
+ module.exports = { findPreviews, combineImages, calculateGrid };
186
+
187
+ // Run if called directly
188
+ if (require.main === module) {
189
+ main().catch(err => {
190
+ console.error('Error:', err.message);
191
+ process.exit(1);
192
+ });
193
+ }