@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.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/bin/myvillage.js +5 -0
- package/package.json +37 -0
- package/src/commands/create-game.js +106 -0
- package/src/commands/deploy.js +120 -0
- package/src/commands/login.js +200 -0
- package/src/commands/logout.js +12 -0
- package/src/commands/status.js +126 -0
- package/src/index.js +50 -0
- package/src/utils/api.js +121 -0
- package/src/utils/auth.js +104 -0
- package/src/utils/config.js +48 -0
- package/src/utils/templates.js +1301 -0
|
@@ -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
|
+
}
|