@myvillage/cli 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.
@@ -0,0 +1,1301 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ // MyVillage brand colors
5
+ const BRAND = {
6
+ gold: '#FFD700',
7
+ brown: '#8B4513',
8
+ green: '#228B22',
9
+ primary: '#B07C00',
10
+ secondary: '#E4DCCB',
11
+ darkBrown: '#302017',
12
+ deepGreen: '#043922',
13
+ teal: '#799C9F',
14
+ };
15
+
16
+ export function createGameProject(targetDir, options) {
17
+ const { name, description, type, ageGroup } = options;
18
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
19
+
20
+ // Create directory structure
21
+ const dirs = [
22
+ '',
23
+ 'public',
24
+ 'public/assets',
25
+ 'public/assets/models',
26
+ 'public/assets/textures',
27
+ 'public/assets/audio',
28
+ 'src',
29
+ 'src/scenes',
30
+ 'src/components',
31
+ 'src/utils',
32
+ ];
33
+
34
+ for (const dir of dirs) {
35
+ mkdirSync(join(targetDir, dir), { recursive: true });
36
+ }
37
+
38
+ // Write common files
39
+ writeFileSync(join(targetDir, 'package.json'), generatePackageJson(name, description, slug, type));
40
+ writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
41
+ writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
42
+ writeFileSync(join(targetDir, 'README.md'), generateReadme(name, description, type, ageGroup));
43
+ writeFileSync(join(targetDir, 'public/index.html'), generateIndexHtml(name));
44
+
45
+ // Write base source files
46
+ writeFileSync(join(targetDir, 'src/main.js'), generateMainJs(type));
47
+ writeFileSync(join(targetDir, 'src/scenes/MainScene.js'), generateMainScene(type));
48
+ writeFileSync(join(targetDir, 'src/utils/InputManager.js'), generateInputManager());
49
+ writeFileSync(join(targetDir, 'src/utils/AudioManager.js'), generateAudioManager());
50
+ writeFileSync(join(targetDir, 'src/components/UI.js'), generateUI(type));
51
+
52
+ // Write game-type-specific files
53
+ switch (type) {
54
+ case 'quiz':
55
+ writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateQuizGameLogic());
56
+ break;
57
+ case 'exploration':
58
+ writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateExplorationGameLogic());
59
+ break;
60
+ case 'narrative':
61
+ writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateNarrativeGameLogic());
62
+ break;
63
+ default:
64
+ writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateBaseGameLogic());
65
+ break;
66
+ }
67
+ }
68
+
69
+ function generatePackageJson(name, description, slug, type) {
70
+ return JSON.stringify({
71
+ name: slug,
72
+ version: '1.0.0',
73
+ description,
74
+ private: true,
75
+ type: 'module',
76
+ scripts: {
77
+ dev: 'vite',
78
+ build: 'vite build',
79
+ preview: 'vite preview',
80
+ },
81
+ dependencies: {
82
+ three: '^0.161.0',
83
+ },
84
+ devDependencies: {
85
+ vite: '^5.0.0',
86
+ },
87
+ myvillage: {
88
+ gameId: null,
89
+ gameType: type,
90
+ lastDeployed: null,
91
+ },
92
+ }, null, 2) + '\n';
93
+ }
94
+
95
+ function generateViteConfig() {
96
+ return `import { defineConfig } from 'vite';
97
+
98
+ export default defineConfig({
99
+ root: '.',
100
+ publicDir: 'public',
101
+ build: {
102
+ outDir: 'dist',
103
+ assetsDir: 'assets',
104
+ },
105
+ server: {
106
+ port: 3000,
107
+ open: true,
108
+ },
109
+ });
110
+ `;
111
+ }
112
+
113
+ function generateGitignore() {
114
+ return `node_modules/
115
+ dist/
116
+ .DS_Store
117
+ *.log
118
+ `;
119
+ }
120
+
121
+ function generateReadme(name, description, type, ageGroup) {
122
+ return `# ${name}
123
+
124
+ ${description}
125
+
126
+ - **Game Type**: ${type}
127
+ - **Target Age Group**: ${ageGroup}
128
+ - **Engine**: Three.js
129
+ - **Built with**: MyVillageOS CLI
130
+
131
+ ## Getting Started
132
+
133
+ \`\`\`bash
134
+ # Install dependencies
135
+ npm install
136
+
137
+ # Start development server
138
+ npm run dev
139
+
140
+ # Build for production
141
+ npm run build
142
+
143
+ # Preview production build
144
+ npm run preview
145
+ \`\`\`
146
+
147
+ ## Project Structure
148
+
149
+ \`\`\`
150
+ src/
151
+ main.js - Game entry point, sets up Three.js renderer
152
+ scenes/
153
+ MainScene.js - Main Three.js scene with lighting and camera
154
+ components/
155
+ UI.js - UI overlay (HUD, menus, dialogs)
156
+ GameLogic.js - Core game logic and state management
157
+ utils/
158
+ InputManager.js - Keyboard, mouse, and touch input handling
159
+ AudioManager.js - Sound effects and background music
160
+ public/
161
+ index.html - HTML entry point
162
+ assets/ - Static assets (models, textures, audio)
163
+ \`\`\`
164
+
165
+ ## Deploying to MyVillageOS
166
+
167
+ When your game is ready, deploy it to the MyVillageOS platform:
168
+
169
+ \`\`\`bash
170
+ myvillage deploy
171
+ \`\`\`
172
+
173
+ You'll earn MVT tokens for successful deployments!
174
+
175
+ ## Learn More
176
+
177
+ - [Three.js Documentation](https://threejs.org/docs/)
178
+ - [Vite Documentation](https://vitejs.dev/)
179
+ - [MyVillageOS Developer Guide](https://portal.myvillageproject.ai)
180
+ `;
181
+ }
182
+
183
+ function generateIndexHtml(name) {
184
+ return `<!DOCTYPE html>
185
+ <html lang="en">
186
+ <head>
187
+ <meta charset="UTF-8" />
188
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
189
+ <title>${name}</title>
190
+ <style>
191
+ * { margin: 0; padding: 0; box-sizing: border-box; }
192
+ html, body { width: 100%; height: 100%; overflow: hidden; }
193
+ body { background: ${BRAND.darkBrown}; }
194
+ #game-container { width: 100%; height: 100%; position: relative; }
195
+ canvas { display: block; }
196
+
197
+ /* Loading screen */
198
+ #loading-screen {
199
+ position: absolute;
200
+ top: 0; left: 0; width: 100%; height: 100%;
201
+ display: flex; flex-direction: column;
202
+ align-items: center; justify-content: center;
203
+ background: ${BRAND.darkBrown};
204
+ color: ${BRAND.secondary};
205
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
206
+ z-index: 100;
207
+ transition: opacity 0.5s;
208
+ }
209
+ #loading-screen.hidden { opacity: 0; pointer-events: none; }
210
+ #loading-screen h1 { color: ${BRAND.gold}; margin-bottom: 20px; }
211
+ #progress-bar {
212
+ width: 200px; height: 6px;
213
+ background: rgba(255,255,255,0.2);
214
+ border-radius: 3px; overflow: hidden;
215
+ }
216
+ #progress-fill {
217
+ height: 100%; width: 0%;
218
+ background: ${BRAND.gold};
219
+ transition: width 0.3s;
220
+ }
221
+
222
+ /* UI overlay */
223
+ #ui-overlay {
224
+ position: absolute;
225
+ top: 0; left: 0; width: 100%; height: 100%;
226
+ pointer-events: none;
227
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
228
+ }
229
+ #ui-overlay > * { pointer-events: auto; }
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div id="game-container">
234
+ <div id="loading-screen">
235
+ <h1>${name}</h1>
236
+ <div id="progress-bar"><div id="progress-fill"></div></div>
237
+ <p id="loading-text" style="margin-top: 10px; font-size: 14px;">Loading...</p>
238
+ </div>
239
+ <div id="ui-overlay"></div>
240
+ </div>
241
+ <script type="module" src="/src/main.js"></script>
242
+ </body>
243
+ </html>
244
+ `;
245
+ }
246
+
247
+ function generateMainJs(type) {
248
+ return `// Game Entry Point
249
+ // This file initializes the Three.js renderer and starts the game loop.
250
+
251
+ import * as THREE from 'three';
252
+ import { MainScene } from './scenes/MainScene.js';
253
+ import { InputManager } from './utils/InputManager.js';
254
+ import { AudioManager } from './utils/AudioManager.js';
255
+ import { GameLogic } from './components/GameLogic.js';
256
+ import { UI } from './components/UI.js';
257
+
258
+ class Game {
259
+ constructor() {
260
+ this.container = document.getElementById('game-container');
261
+ this.clock = new THREE.Clock();
262
+
263
+ // Game state: 'loading' | 'menu' | 'playing' | 'paused' | 'gameover'
264
+ this.state = 'loading';
265
+
266
+ this.init();
267
+ }
268
+
269
+ init() {
270
+ // Set up renderer with responsive canvas
271
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
272
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
273
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
274
+ this.renderer.shadowMap.enabled = true;
275
+ this.container.appendChild(this.renderer.domElement);
276
+
277
+ // Initialize subsystems
278
+ this.input = new InputManager(this.renderer.domElement);
279
+ this.audio = new AudioManager();
280
+ this.scene = new MainScene();
281
+ this.gameLogic = new GameLogic(this);
282
+ this.ui = new UI(this);
283
+
284
+ // Handle window resize
285
+ window.addEventListener('resize', () => this.onResize());
286
+
287
+ // Simulate asset loading (replace with real asset loading in your game)
288
+ this.loadAssets().then(() => {
289
+ this.state = 'menu';
290
+ this.hideLoadingScreen();
291
+ this.ui.showMenu();
292
+ });
293
+
294
+ // Start game loop
295
+ this.animate();
296
+ }
297
+
298
+ async loadAssets() {
299
+ const progressFill = document.getElementById('progress-fill');
300
+ const loadingText = document.getElementById('loading-text');
301
+
302
+ // Simulate loading progress - replace with actual asset loading
303
+ const steps = ['Preparing scene...', 'Loading assets...', 'Almost ready...'];
304
+ for (let i = 0; i < steps.length; i++) {
305
+ loadingText.textContent = steps[i];
306
+ progressFill.style.width = \`\${((i + 1) / steps.length) * 100}%\`;
307
+ await new Promise((r) => setTimeout(r, 300));
308
+ }
309
+ }
310
+
311
+ hideLoadingScreen() {
312
+ const screen = document.getElementById('loading-screen');
313
+ screen.classList.add('hidden');
314
+ setTimeout(() => screen.remove(), 500);
315
+ }
316
+
317
+ onResize() {
318
+ const width = window.innerWidth;
319
+ const height = window.innerHeight;
320
+
321
+ this.scene.camera.aspect = width / height;
322
+ this.scene.camera.updateProjectionMatrix();
323
+ this.renderer.setSize(width, height);
324
+ }
325
+
326
+ startGame() {
327
+ this.state = 'playing';
328
+ this.gameLogic.start();
329
+ this.ui.showHUD();
330
+ }
331
+
332
+ pauseGame() {
333
+ this.state = 'paused';
334
+ this.ui.showPauseMenu();
335
+ }
336
+
337
+ resumeGame() {
338
+ this.state = 'playing';
339
+ this.ui.showHUD();
340
+ }
341
+
342
+ endGame(result) {
343
+ this.state = 'gameover';
344
+ this.ui.showGameOver(result);
345
+ }
346
+
347
+ animate() {
348
+ requestAnimationFrame(() => this.animate());
349
+
350
+ const delta = this.clock.getDelta();
351
+
352
+ if (this.state === 'playing') {
353
+ this.gameLogic.update(delta);
354
+ this.scene.update(delta);
355
+ }
356
+
357
+ this.renderer.render(this.scene.scene, this.scene.camera);
358
+ }
359
+ }
360
+
361
+ // Start the game
362
+ const game = new Game();
363
+
364
+ // Make game globally accessible for debugging
365
+ window.game = game;
366
+ `;
367
+ }
368
+
369
+ function generateMainScene(type) {
370
+ return `// Main Three.js Scene
371
+ // Sets up camera, lighting, and the 3D environment.
372
+
373
+ import * as THREE from 'three';
374
+
375
+ export class MainScene {
376
+ constructor() {
377
+ // Create scene
378
+ this.scene = new THREE.Scene();
379
+ this.scene.background = new THREE.Color(0x87CEEB); // Sky blue
380
+
381
+ // Camera setup with perspective projection
382
+ this.camera = new THREE.PerspectiveCamera(
383
+ 60, // FOV
384
+ window.innerWidth / window.innerHeight, // Aspect ratio
385
+ 0.1, // Near plane
386
+ 1000 // Far plane
387
+ );
388
+ this.camera.position.set(0, 5, 10);
389
+ this.camera.lookAt(0, 0, 0);
390
+
391
+ // Lighting setup
392
+ this.setupLighting();
393
+
394
+ // Create ground plane
395
+ this.setupEnvironment();
396
+ }
397
+
398
+ setupLighting() {
399
+ // Ambient light for overall illumination
400
+ const ambient = new THREE.AmbientLight(0xffffff, 0.5);
401
+ this.scene.add(ambient);
402
+
403
+ // Directional light (sun) with shadows
404
+ const sun = new THREE.DirectionalLight(0xffffff, 1.0);
405
+ sun.position.set(10, 20, 10);
406
+ sun.castShadow = true;
407
+ sun.shadow.mapSize.width = 2048;
408
+ sun.shadow.mapSize.height = 2048;
409
+ sun.shadow.camera.near = 0.5;
410
+ sun.shadow.camera.far = 50;
411
+ sun.shadow.camera.left = -20;
412
+ sun.shadow.camera.right = 20;
413
+ sun.shadow.camera.top = 20;
414
+ sun.shadow.camera.bottom = -20;
415
+ this.scene.add(sun);
416
+ }
417
+
418
+ setupEnvironment() {
419
+ // Ground plane using MyVillage brand green
420
+ const groundGeometry = new THREE.PlaneGeometry(50, 50);
421
+ const groundMaterial = new THREE.MeshStandardMaterial({
422
+ color: 0x228B22, // MyVillage green
423
+ roughness: 0.8,
424
+ });
425
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
426
+ ground.rotation.x = -Math.PI / 2;
427
+ ground.receiveShadow = true;
428
+ this.scene.add(ground);
429
+
430
+ // Add a sample object - replace with your game objects
431
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
432
+ const material = new THREE.MeshStandardMaterial({
433
+ color: 0xFFD700, // MyVillage gold
434
+ });
435
+ this.sampleObject = new THREE.Mesh(geometry, material);
436
+ this.sampleObject.position.y = 0.5;
437
+ this.sampleObject.castShadow = true;
438
+ this.scene.add(this.sampleObject);
439
+ }
440
+
441
+ update(delta) {
442
+ // Rotate the sample object - replace with your scene updates
443
+ if (this.sampleObject) {
444
+ this.sampleObject.rotation.y += delta * 0.5;
445
+ }
446
+ }
447
+ }
448
+ `;
449
+ }
450
+
451
+ function generateInputManager() {
452
+ return `// Input Manager
453
+ // Handles keyboard, mouse, and touch input for both desktop and mobile.
454
+
455
+ export class InputManager {
456
+ constructor(canvas) {
457
+ this.canvas = canvas;
458
+
459
+ // Keyboard state
460
+ this.keys = {};
461
+ this.keysJustPressed = {};
462
+
463
+ // Mouse state
464
+ this.mouse = { x: 0, y: 0, down: false };
465
+
466
+ // Touch state
467
+ this.touches = [];
468
+ this.isTouchDevice = 'ontouchstart' in window;
469
+
470
+ this.setupEventListeners();
471
+ }
472
+
473
+ setupEventListeners() {
474
+ // Keyboard events
475
+ window.addEventListener('keydown', (e) => {
476
+ if (!this.keys[e.code]) {
477
+ this.keysJustPressed[e.code] = true;
478
+ }
479
+ this.keys[e.code] = true;
480
+ });
481
+
482
+ window.addEventListener('keyup', (e) => {
483
+ this.keys[e.code] = false;
484
+ });
485
+
486
+ // Mouse events
487
+ this.canvas.addEventListener('mousemove', (e) => {
488
+ // Normalized device coordinates (-1 to +1)
489
+ this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
490
+ this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
491
+ });
492
+
493
+ this.canvas.addEventListener('mousedown', () => {
494
+ this.mouse.down = true;
495
+ });
496
+
497
+ this.canvas.addEventListener('mouseup', () => {
498
+ this.mouse.down = false;
499
+ });
500
+
501
+ // Touch events
502
+ this.canvas.addEventListener('touchstart', (e) => {
503
+ this.touches = Array.from(e.touches).map((t) => ({
504
+ x: (t.clientX / window.innerWidth) * 2 - 1,
505
+ y: -(t.clientY / window.innerHeight) * 2 + 1,
506
+ id: t.identifier,
507
+ }));
508
+ });
509
+
510
+ this.canvas.addEventListener('touchmove', (e) => {
511
+ e.preventDefault();
512
+ this.touches = Array.from(e.touches).map((t) => ({
513
+ x: (t.clientX / window.innerWidth) * 2 - 1,
514
+ y: -(t.clientY / window.innerHeight) * 2 + 1,
515
+ id: t.identifier,
516
+ }));
517
+ }, { passive: false });
518
+
519
+ this.canvas.addEventListener('touchend', (e) => {
520
+ this.touches = Array.from(e.touches).map((t) => ({
521
+ x: (t.clientX / window.innerWidth) * 2 - 1,
522
+ y: -(t.clientY / window.innerHeight) * 2 + 1,
523
+ id: t.identifier,
524
+ }));
525
+ });
526
+ }
527
+
528
+ // Check if a key is currently held down
529
+ isKeyDown(code) {
530
+ return !!this.keys[code];
531
+ }
532
+
533
+ // Check if a key was just pressed this frame (call clearFrame() each frame)
534
+ isKeyJustPressed(code) {
535
+ return !!this.keysJustPressed[code];
536
+ }
537
+
538
+ // Get WASD / arrow key movement vector
539
+ getMovementVector() {
540
+ let x = 0;
541
+ let z = 0;
542
+
543
+ if (this.isKeyDown('KeyW') || this.isKeyDown('ArrowUp')) z -= 1;
544
+ if (this.isKeyDown('KeyS') || this.isKeyDown('ArrowDown')) z += 1;
545
+ if (this.isKeyDown('KeyA') || this.isKeyDown('ArrowLeft')) x -= 1;
546
+ if (this.isKeyDown('KeyD') || this.isKeyDown('ArrowRight')) x += 1;
547
+
548
+ // Normalize diagonal movement
549
+ const length = Math.sqrt(x * x + z * z);
550
+ if (length > 0) {
551
+ x /= length;
552
+ z /= length;
553
+ }
554
+
555
+ return { x, z };
556
+ }
557
+
558
+ // Call at the end of each frame to clear one-shot inputs
559
+ clearFrame() {
560
+ this.keysJustPressed = {};
561
+ }
562
+ }
563
+ `;
564
+ }
565
+
566
+ function generateAudioManager() {
567
+ return `// Audio Manager
568
+ // Handles sound effects and background music using the Web Audio API.
569
+
570
+ export class AudioManager {
571
+ constructor() {
572
+ this.context = null;
573
+ this.sounds = new Map();
574
+ this.musicGain = null;
575
+ this.sfxGain = null;
576
+ this.initialized = false;
577
+ }
578
+
579
+ // Initialize audio context on first user interaction
580
+ init() {
581
+ if (this.initialized) return;
582
+
583
+ this.context = new (window.AudioContext || window.webkitAudioContext)();
584
+ this.musicGain = this.context.createGain();
585
+ this.sfxGain = this.context.createGain();
586
+ this.musicGain.connect(this.context.destination);
587
+ this.sfxGain.connect(this.context.destination);
588
+
589
+ this.musicGain.gain.value = 0.5;
590
+ this.sfxGain.gain.value = 0.8;
591
+
592
+ this.initialized = true;
593
+ }
594
+
595
+ // Load an audio file and store it by name
596
+ async loadSound(name, url) {
597
+ this.init();
598
+
599
+ try {
600
+ const response = await fetch(url);
601
+ const arrayBuffer = await response.arrayBuffer();
602
+ const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
603
+ this.sounds.set(name, audioBuffer);
604
+ } catch (err) {
605
+ console.warn(\`Failed to load sound "\${name}" from \${url}:\`, err.message);
606
+ }
607
+ }
608
+
609
+ // Play a loaded sound effect
610
+ playSFX(name) {
611
+ this.init();
612
+
613
+ const buffer = this.sounds.get(name);
614
+ if (!buffer) return;
615
+
616
+ const source = this.context.createBufferSource();
617
+ source.buffer = buffer;
618
+ source.connect(this.sfxGain);
619
+ source.start(0);
620
+ }
621
+
622
+ // Play a loaded sound as looping background music
623
+ playMusic(name) {
624
+ this.init();
625
+ this.stopMusic();
626
+
627
+ const buffer = this.sounds.get(name);
628
+ if (!buffer) return;
629
+
630
+ this.currentMusic = this.context.createBufferSource();
631
+ this.currentMusic.buffer = buffer;
632
+ this.currentMusic.loop = true;
633
+ this.currentMusic.connect(this.musicGain);
634
+ this.currentMusic.start(0);
635
+ }
636
+
637
+ stopMusic() {
638
+ if (this.currentMusic) {
639
+ this.currentMusic.stop();
640
+ this.currentMusic = null;
641
+ }
642
+ }
643
+
644
+ setMusicVolume(value) {
645
+ if (this.musicGain) this.musicGain.gain.value = Math.max(0, Math.min(1, value));
646
+ }
647
+
648
+ setSFXVolume(value) {
649
+ if (this.sfxGain) this.sfxGain.gain.value = Math.max(0, Math.min(1, value));
650
+ }
651
+ }
652
+ `;
653
+ }
654
+
655
+ function generateUI(type) {
656
+ let typeSpecificUI = '';
657
+
658
+ switch (type) {
659
+ case 'quiz':
660
+ typeSpecificUI = `
661
+ showQuestion(question, choices, onAnswer) {
662
+ this.clearOverlay();
663
+
664
+ const panel = this.createElement('div', {
665
+ position: 'absolute', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
666
+ width: '90%', maxWidth: '600px', background: 'rgba(48,32,23,0.95)',
667
+ borderRadius: '16px', padding: '24px', color: '#E4DCCB',
668
+ });
669
+
670
+ const qText = this.createElement('p', {
671
+ fontSize: '18px', fontWeight: 'bold', marginBottom: '16px',
672
+ color: '#FFD700', textAlign: 'center',
673
+ });
674
+ qText.textContent = question;
675
+ panel.appendChild(qText);
676
+
677
+ choices.forEach((choice, index) => {
678
+ const btn = this.createElement('button', {
679
+ display: 'block', width: '100%', padding: '12px',
680
+ margin: '8px 0', background: '#043922', color: '#E4DCCB',
681
+ border: '2px solid #228B22', borderRadius: '8px',
682
+ fontSize: '16px', cursor: 'pointer', textAlign: 'left',
683
+ });
684
+ btn.textContent = choice;
685
+ btn.addEventListener('click', () => onAnswer(index));
686
+ panel.appendChild(btn);
687
+ });
688
+
689
+ this.overlay.appendChild(panel);
690
+ }
691
+
692
+ showScore(score, total) {
693
+ this.clearOverlay();
694
+
695
+ const panel = this.createElement('div', {
696
+ position: 'absolute', top: '10px', right: '10px',
697
+ background: 'rgba(48,32,23,0.85)', borderRadius: '12px',
698
+ padding: '10px 18px', color: '#FFD700', fontSize: '18px', fontWeight: 'bold',
699
+ });
700
+ panel.textContent = \`Score: \${score} / \${total}\`;
701
+ this.overlay.appendChild(panel);
702
+ }`;
703
+ break;
704
+
705
+ case 'exploration':
706
+ typeSpecificUI = `
707
+ showMinimap(playerPos, items) {
708
+ // Minimap rendering in top-right corner
709
+ let minimap = this.overlay.querySelector('#minimap');
710
+ if (!minimap) {
711
+ minimap = this.createElement('canvas', {
712
+ position: 'absolute', top: '10px', right: '10px',
713
+ width: '150px', height: '150px',
714
+ background: 'rgba(48,32,23,0.8)', borderRadius: '8px',
715
+ border: '2px solid #B07C00',
716
+ });
717
+ minimap.id = 'minimap';
718
+ minimap.width = 150;
719
+ minimap.height = 150;
720
+ this.overlay.appendChild(minimap);
721
+ }
722
+
723
+ const ctx = minimap.getContext('2d');
724
+ ctx.clearRect(0, 0, 150, 150);
725
+
726
+ // Draw player
727
+ ctx.fillStyle = '#FFD700';
728
+ ctx.beginPath();
729
+ ctx.arc(75 + playerPos.x * 3, 75 + playerPos.z * 3, 4, 0, Math.PI * 2);
730
+ ctx.fill();
731
+
732
+ // Draw collectible items
733
+ ctx.fillStyle = '#228B22';
734
+ items.forEach((item) => {
735
+ ctx.beginPath();
736
+ ctx.arc(75 + item.x * 3, 75 + item.z * 3, 3, 0, Math.PI * 2);
737
+ ctx.fill();
738
+ });
739
+ }
740
+
741
+ showInventory(items) {
742
+ let inv = this.overlay.querySelector('#inventory');
743
+ if (!inv) {
744
+ inv = this.createElement('div', {
745
+ position: 'absolute', bottom: '10px', left: '50%', transform: 'translateX(-50%)',
746
+ display: 'flex', gap: '8px', background: 'rgba(48,32,23,0.85)',
747
+ padding: '10px', borderRadius: '12px',
748
+ });
749
+ inv.id = 'inventory';
750
+ this.overlay.appendChild(inv);
751
+ }
752
+ inv.innerHTML = '';
753
+
754
+ items.forEach((item) => {
755
+ const slot = this.createElement('div', {
756
+ width: '48px', height: '48px', background: '#043922',
757
+ borderRadius: '8px', border: '2px solid #228B22',
758
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
759
+ color: '#FFD700', fontSize: '12px', textAlign: 'center',
760
+ });
761
+ slot.textContent = item.name;
762
+ inv.appendChild(slot);
763
+ });
764
+ }`;
765
+ break;
766
+
767
+ case 'narrative':
768
+ typeSpecificUI = `
769
+ showDialogue(speaker, text, choices, onChoice) {
770
+ this.clearOverlay();
771
+
772
+ const panel = this.createElement('div', {
773
+ position: 'absolute', bottom: '0', left: '0', width: '100%',
774
+ background: 'rgba(48,32,23,0.95)', padding: '20px 24px',
775
+ borderTop: '3px solid #B07C00', color: '#E4DCCB',
776
+ });
777
+
778
+ if (speaker) {
779
+ const nameTag = this.createElement('span', {
780
+ color: '#FFD700', fontWeight: 'bold', fontSize: '16px',
781
+ marginBottom: '8px', display: 'block',
782
+ });
783
+ nameTag.textContent = speaker;
784
+ panel.appendChild(nameTag);
785
+ }
786
+
787
+ const textEl = this.createElement('p', {
788
+ fontSize: '15px', lineHeight: '1.5', marginBottom: choices ? '16px' : '0',
789
+ });
790
+ textEl.textContent = text;
791
+ panel.appendChild(textEl);
792
+
793
+ if (choices && choices.length > 0) {
794
+ choices.forEach((choice, index) => {
795
+ const btn = this.createElement('button', {
796
+ display: 'block', width: '100%', padding: '10px',
797
+ margin: '6px 0', background: 'transparent', color: '#FFD700',
798
+ border: '1px solid #B07C00', borderRadius: '8px',
799
+ fontSize: '14px', cursor: 'pointer', textAlign: 'left',
800
+ });
801
+ btn.textContent = \`> \${choice}\`;
802
+ btn.addEventListener('click', () => onChoice(index));
803
+ panel.appendChild(btn);
804
+ });
805
+ } else {
806
+ const hint = this.createElement('p', {
807
+ fontSize: '12px', color: '#799C9F', marginTop: '8px', textAlign: 'center',
808
+ });
809
+ hint.textContent = 'Click to continue...';
810
+ panel.appendChild(hint);
811
+ panel.addEventListener('click', () => onChoice(-1));
812
+ }
813
+
814
+ this.overlay.appendChild(panel);
815
+ }`;
816
+ break;
817
+
818
+ default:
819
+ typeSpecificUI = '';
820
+ }
821
+
822
+ return `// UI Overlay Manager
823
+ // Manages HUD elements, menus, and dialogs rendered as HTML over the canvas.
824
+
825
+ export class UI {
826
+ constructor(game) {
827
+ this.game = game;
828
+ this.overlay = document.getElementById('ui-overlay');
829
+ }
830
+
831
+ // Helper to create a styled DOM element
832
+ createElement(tag, styles) {
833
+ const el = document.createElement(tag);
834
+ Object.assign(el.style, styles);
835
+ return el;
836
+ }
837
+
838
+ clearOverlay() {
839
+ this.overlay.innerHTML = '';
840
+ }
841
+
842
+ showMenu() {
843
+ this.clearOverlay();
844
+
845
+ const menu = this.createElement('div', {
846
+ position: 'absolute', top: '50%', left: '50%',
847
+ transform: 'translate(-50%, -50%)', textAlign: 'center',
848
+ color: '#E4DCCB',
849
+ });
850
+
851
+ const title = this.createElement('h1', {
852
+ fontSize: '36px', color: '#FFD700', marginBottom: '30px',
853
+ });
854
+ title.textContent = document.title;
855
+ menu.appendChild(title);
856
+
857
+ const playBtn = this.createElement('button', {
858
+ padding: '14px 48px', fontSize: '18px', fontWeight: 'bold',
859
+ background: '#B07C00', color: 'white', border: 'none',
860
+ borderRadius: '12px', cursor: 'pointer',
861
+ });
862
+ playBtn.textContent = 'Play';
863
+ playBtn.addEventListener('click', () => this.game.startGame());
864
+ menu.appendChild(playBtn);
865
+
866
+ this.overlay.appendChild(menu);
867
+ }
868
+
869
+ showHUD() {
870
+ this.clearOverlay();
871
+
872
+ const pauseBtn = this.createElement('button', {
873
+ position: 'absolute', top: '10px', right: '10px',
874
+ padding: '8px 16px', background: 'rgba(48,32,23,0.7)',
875
+ color: '#E4DCCB', border: '1px solid #B07C00',
876
+ borderRadius: '8px', cursor: 'pointer', fontSize: '14px',
877
+ });
878
+ pauseBtn.textContent = 'Pause';
879
+ pauseBtn.addEventListener('click', () => this.game.pauseGame());
880
+ this.overlay.appendChild(pauseBtn);
881
+ }
882
+
883
+ showPauseMenu() {
884
+ this.clearOverlay();
885
+
886
+ const menu = this.createElement('div', {
887
+ position: 'absolute', top: '50%', left: '50%',
888
+ transform: 'translate(-50%, -50%)', textAlign: 'center',
889
+ background: 'rgba(48,32,23,0.9)', padding: '30px 40px',
890
+ borderRadius: '16px', color: '#E4DCCB',
891
+ });
892
+
893
+ const title = this.createElement('h2', {
894
+ color: '#FFD700', marginBottom: '20px',
895
+ });
896
+ title.textContent = 'Paused';
897
+ menu.appendChild(title);
898
+
899
+ const resumeBtn = this.createElement('button', {
900
+ display: 'block', width: '100%', padding: '12px',
901
+ margin: '8px 0', background: '#B07C00', color: 'white',
902
+ border: 'none', borderRadius: '8px', fontSize: '16px', cursor: 'pointer',
903
+ });
904
+ resumeBtn.textContent = 'Resume';
905
+ resumeBtn.addEventListener('click', () => this.game.resumeGame());
906
+ menu.appendChild(resumeBtn);
907
+
908
+ this.overlay.appendChild(menu);
909
+ }
910
+
911
+ showGameOver(result) {
912
+ this.clearOverlay();
913
+
914
+ const panel = this.createElement('div', {
915
+ position: 'absolute', top: '50%', left: '50%',
916
+ transform: 'translate(-50%, -50%)', textAlign: 'center',
917
+ background: 'rgba(48,32,23,0.95)', padding: '30px 40px',
918
+ borderRadius: '16px', color: '#E4DCCB',
919
+ });
920
+
921
+ const title = this.createElement('h2', {
922
+ color: '#FFD700', marginBottom: '10px',
923
+ });
924
+ title.textContent = result?.won ? 'Congratulations!' : 'Game Over';
925
+ panel.appendChild(title);
926
+
927
+ if (result?.score !== undefined) {
928
+ const score = this.createElement('p', {
929
+ fontSize: '20px', marginBottom: '20px',
930
+ });
931
+ score.textContent = \`Score: \${result.score}\`;
932
+ panel.appendChild(score);
933
+ }
934
+
935
+ const retryBtn = this.createElement('button', {
936
+ padding: '12px 36px', background: '#B07C00', color: 'white',
937
+ border: 'none', borderRadius: '8px', fontSize: '16px', cursor: 'pointer',
938
+ });
939
+ retryBtn.textContent = 'Play Again';
940
+ retryBtn.addEventListener('click', () => {
941
+ this.game.gameLogic.reset();
942
+ this.game.startGame();
943
+ });
944
+ panel.appendChild(retryBtn);
945
+
946
+ this.overlay.appendChild(panel);
947
+ }
948
+ ${typeSpecificUI}
949
+ }
950
+ `;
951
+ }
952
+
953
+ function generateBaseGameLogic() {
954
+ return `// Base Game Logic
955
+ // Manages game state, scoring, and core game mechanics.
956
+ // Customize this file to build your game!
957
+
958
+ export class GameLogic {
959
+ constructor(game) {
960
+ this.game = game;
961
+ this.score = 0;
962
+ this.time = 0;
963
+ }
964
+
965
+ start() {
966
+ this.score = 0;
967
+ this.time = 0;
968
+ }
969
+
970
+ reset() {
971
+ this.score = 0;
972
+ this.time = 0;
973
+ }
974
+
975
+ update(delta) {
976
+ this.time += delta;
977
+
978
+ // Add your game logic here!
979
+ // Access input: this.game.input.isKeyDown('Space')
980
+ // Access scene: this.game.scene
981
+ // Access audio: this.game.audio.playSFX('jump')
982
+ // End game: this.game.endGame({ won: true, score: this.score })
983
+ }
984
+ }
985
+ `;
986
+ }
987
+
988
+ function generateQuizGameLogic() {
989
+ return `// Quiz Game Logic
990
+ // Manages questions, answers, scoring, and progress tracking.
991
+
992
+ export class GameLogic {
993
+ constructor(game) {
994
+ this.game = game;
995
+ this.score = 0;
996
+ this.currentQuestion = 0;
997
+
998
+ // Define your quiz questions here
999
+ this.questions = [
1000
+ {
1001
+ question: 'What is 2 + 2?',
1002
+ choices: ['3', '4', '5', '6'],
1003
+ correct: 1,
1004
+ },
1005
+ {
1006
+ question: 'What is the capital of France?',
1007
+ choices: ['London', 'Berlin', 'Paris', 'Madrid'],
1008
+ correct: 2,
1009
+ },
1010
+ {
1011
+ question: 'Which planet is closest to the Sun?',
1012
+ choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
1013
+ correct: 1,
1014
+ },
1015
+ ];
1016
+ }
1017
+
1018
+ start() {
1019
+ this.score = 0;
1020
+ this.currentQuestion = 0;
1021
+ this.showCurrentQuestion();
1022
+ }
1023
+
1024
+ reset() {
1025
+ this.score = 0;
1026
+ this.currentQuestion = 0;
1027
+ }
1028
+
1029
+ showCurrentQuestion() {
1030
+ if (this.currentQuestion >= this.questions.length) {
1031
+ // Quiz complete
1032
+ this.game.endGame({
1033
+ won: this.score > this.questions.length / 2,
1034
+ score: this.score,
1035
+ });
1036
+ return;
1037
+ }
1038
+
1039
+ const q = this.questions[this.currentQuestion];
1040
+ this.game.ui.showQuestion(q.question, q.choices, (answerIndex) => {
1041
+ this.handleAnswer(answerIndex);
1042
+ });
1043
+ this.game.ui.showScore(this.score, this.questions.length);
1044
+ }
1045
+
1046
+ handleAnswer(answerIndex) {
1047
+ const q = this.questions[this.currentQuestion];
1048
+
1049
+ if (answerIndex === q.correct) {
1050
+ this.score++;
1051
+ // Play celebration animation - rotate the sample object faster
1052
+ if (this.game.scene.sampleObject) {
1053
+ this.game.scene.sampleObject.scale.set(1.5, 1.5, 1.5);
1054
+ setTimeout(() => {
1055
+ this.game.scene.sampleObject.scale.set(1, 1, 1);
1056
+ }, 500);
1057
+ }
1058
+ }
1059
+
1060
+ this.currentQuestion++;
1061
+ this.showCurrentQuestion();
1062
+ }
1063
+
1064
+ update(delta) {
1065
+ // Quiz games are event-driven, so update is minimal
1066
+ }
1067
+ }
1068
+ `;
1069
+ }
1070
+
1071
+ function generateExplorationGameLogic() {
1072
+ return `// Exploration Game Logic
1073
+ // Manages character movement, collision detection, and collectible items.
1074
+
1075
+ import * as THREE from 'three';
1076
+
1077
+ export class GameLogic {
1078
+ constructor(game) {
1079
+ this.game = game;
1080
+ this.score = 0;
1081
+ this.moveSpeed = 5;
1082
+ this.player = null;
1083
+ this.collectibles = [];
1084
+ this.collectedItems = [];
1085
+ }
1086
+
1087
+ start() {
1088
+ this.score = 0;
1089
+ this.collectedItems = [];
1090
+ this.setupPlayer();
1091
+ this.spawnCollectibles();
1092
+ }
1093
+
1094
+ reset() {
1095
+ // Remove existing objects
1096
+ if (this.player) this.game.scene.scene.remove(this.player);
1097
+ this.collectibles.forEach((c) => this.game.scene.scene.remove(c.mesh));
1098
+ this.collectibles = [];
1099
+ this.collectedItems = [];
1100
+ this.score = 0;
1101
+ }
1102
+
1103
+ setupPlayer() {
1104
+ // Create a simple player character (gold cube)
1105
+ const geometry = new THREE.BoxGeometry(0.8, 1.2, 0.8);
1106
+ const material = new THREE.MeshStandardMaterial({ color: 0xFFD700 });
1107
+ this.player = new THREE.Mesh(geometry, material);
1108
+ this.player.position.set(0, 0.6, 0);
1109
+ this.player.castShadow = true;
1110
+ this.game.scene.scene.add(this.player);
1111
+
1112
+ // Update camera to follow player
1113
+ this.game.scene.camera.position.set(0, 8, 12);
1114
+ }
1115
+
1116
+ spawnCollectibles() {
1117
+ // Spawn collectible items in random positions
1118
+ const items = [
1119
+ { name: 'Star', color: 0xFFD700 },
1120
+ { name: 'Gem', color: 0x228B22 },
1121
+ { name: 'Coin', color: 0xB07C00 },
1122
+ ];
1123
+
1124
+ for (let i = 0; i < 10; i++) {
1125
+ const item = items[i % items.length];
1126
+ const geometry = new THREE.SphereGeometry(0.3, 8, 8);
1127
+ const material = new THREE.MeshStandardMaterial({ color: item.color, emissive: item.color, emissiveIntensity: 0.3 });
1128
+ const mesh = new THREE.Mesh(geometry, material);
1129
+
1130
+ mesh.position.set(
1131
+ (Math.random() - 0.5) * 20,
1132
+ 0.5,
1133
+ (Math.random() - 0.5) * 20
1134
+ );
1135
+ mesh.castShadow = true;
1136
+
1137
+ this.game.scene.scene.add(mesh);
1138
+ this.collectibles.push({ mesh, name: item.name, collected: false });
1139
+ }
1140
+ }
1141
+
1142
+ update(delta) {
1143
+ if (!this.player) return;
1144
+
1145
+ // WASD movement
1146
+ const movement = this.game.input.getMovementVector();
1147
+ this.player.position.x += movement.x * this.moveSpeed * delta;
1148
+ this.player.position.z += movement.z * this.moveSpeed * delta;
1149
+
1150
+ // Keep player within bounds
1151
+ this.player.position.x = Math.max(-24, Math.min(24, this.player.position.x));
1152
+ this.player.position.z = Math.max(-24, Math.min(24, this.player.position.z));
1153
+
1154
+ // Camera follows player
1155
+ this.game.scene.camera.position.x = this.player.position.x;
1156
+ this.game.scene.camera.position.z = this.player.position.z + 12;
1157
+ this.game.scene.camera.lookAt(this.player.position);
1158
+
1159
+ // Check collisions with collectibles
1160
+ this.collectibles.forEach((item) => {
1161
+ if (item.collected) return;
1162
+
1163
+ const dist = this.player.position.distanceTo(item.mesh.position);
1164
+ if (dist < 1.0) {
1165
+ item.collected = true;
1166
+ this.game.scene.scene.remove(item.mesh);
1167
+ this.collectedItems.push({ name: item.name });
1168
+ this.score++;
1169
+ }
1170
+ });
1171
+
1172
+ // Rotate uncollected items
1173
+ this.collectibles.forEach((item) => {
1174
+ if (!item.collected) {
1175
+ item.mesh.rotation.y += delta * 2;
1176
+ item.mesh.position.y = 0.5 + Math.sin(Date.now() * 0.003) * 0.15;
1177
+ }
1178
+ });
1179
+
1180
+ // Update UI
1181
+ this.game.ui.showInventory(this.collectedItems);
1182
+ this.game.ui.showMinimap(
1183
+ this.player.position,
1184
+ this.collectibles.filter((c) => !c.collected).map((c) => c.mesh.position)
1185
+ );
1186
+
1187
+ // Check win condition
1188
+ const remaining = this.collectibles.filter((c) => !c.collected).length;
1189
+ if (remaining === 0) {
1190
+ this.game.endGame({ won: true, score: this.score });
1191
+ }
1192
+
1193
+ this.game.input.clearFrame();
1194
+ }
1195
+ }
1196
+ `;
1197
+ }
1198
+
1199
+ function generateNarrativeGameLogic() {
1200
+ return `// Narrative Game Logic
1201
+ // Manages dialogue, choices, scene transitions, and story branching.
1202
+
1203
+ export class GameLogic {
1204
+ constructor(game) {
1205
+ this.game = game;
1206
+ this.currentSceneIndex = 0;
1207
+ this.score = 0;
1208
+
1209
+ // Define your story scenes here.
1210
+ // Each scene has speaker, text, and optional choices.
1211
+ // Choices have 'text' and 'next' (index of the next scene to go to).
1212
+ this.storyScenes = [
1213
+ {
1214
+ speaker: 'Narrator',
1215
+ text: 'You find yourself at the entrance to a mysterious village. The golden gates shine in the sunlight.',
1216
+ choices: [
1217
+ { text: 'Enter the village', next: 1 },
1218
+ { text: 'Look around first', next: 2 },
1219
+ ],
1220
+ },
1221
+ {
1222
+ speaker: 'Village Elder',
1223
+ text: 'Welcome, young traveler! We have been expecting you. Our village needs your help.',
1224
+ choices: [
1225
+ { text: 'How can I help?', next: 3 },
1226
+ { text: 'What happened here?', next: 3 },
1227
+ ],
1228
+ },
1229
+ {
1230
+ speaker: 'Narrator',
1231
+ text: 'You notice a weathered signpost and colorful flowers lining a cobblestone path. The village looks peaceful but there is an air of anticipation.',
1232
+ choices: [
1233
+ { text: 'Follow the path inside', next: 1 },
1234
+ ],
1235
+ },
1236
+ {
1237
+ speaker: 'Village Elder',
1238
+ text: 'A great challenge lies ahead. Only someone with courage and wisdom can solve it. Will you accept the quest?',
1239
+ choices: [
1240
+ { text: 'I accept!', next: 4 },
1241
+ { text: 'Tell me more about the quest', next: 4 },
1242
+ ],
1243
+ },
1244
+ {
1245
+ speaker: 'Narrator',
1246
+ text: 'And so your adventure begins... This is where your story continues! Edit the storyScenes array in GameLogic.js to create your own narrative.',
1247
+ choices: null, // null choices = end of story
1248
+ },
1249
+ ];
1250
+ }
1251
+
1252
+ start() {
1253
+ this.currentSceneIndex = 0;
1254
+ this.score = 0;
1255
+ this.showCurrentScene();
1256
+ }
1257
+
1258
+ reset() {
1259
+ this.currentSceneIndex = 0;
1260
+ this.score = 0;
1261
+ }
1262
+
1263
+ showCurrentScene() {
1264
+ const scene = this.storyScenes[this.currentSceneIndex];
1265
+ if (!scene) {
1266
+ this.game.endGame({ won: true, score: this.score });
1267
+ return;
1268
+ }
1269
+
1270
+ const choiceTexts = scene.choices ? scene.choices.map((c) => c.text) : null;
1271
+
1272
+ this.game.ui.showDialogue(scene.speaker, scene.text, choiceTexts, (choiceIndex) => {
1273
+ this.handleChoice(choiceIndex);
1274
+ });
1275
+ }
1276
+
1277
+ handleChoice(choiceIndex) {
1278
+ const scene = this.storyScenes[this.currentSceneIndex];
1279
+
1280
+ if (!scene.choices || choiceIndex === -1) {
1281
+ // End of story
1282
+ this.game.endGame({ won: true, score: this.score });
1283
+ return;
1284
+ }
1285
+
1286
+ const choice = scene.choices[choiceIndex];
1287
+ if (choice && choice.next !== undefined) {
1288
+ this.currentSceneIndex = choice.next;
1289
+ this.score++;
1290
+ this.showCurrentScene();
1291
+ } else {
1292
+ this.game.endGame({ won: true, score: this.score });
1293
+ }
1294
+ }
1295
+
1296
+ update(delta) {
1297
+ // Narrative games are event-driven via UI choices
1298
+ }
1299
+ }
1300
+ `;
1301
+ }