@myvillage/cli 1.63.0 → 1.64.3
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/package.json +1 -1
- package/src/utils/templates.js +1085 -291
package/src/utils/templates.js
CHANGED
|
@@ -55,6 +55,19 @@ const BRAND = {
|
|
|
55
55
|
|
|
56
56
|
export function createGameProject(targetDir, options) {
|
|
57
57
|
const { name, description, type, ageGroup, orientation = 'landscape' } = options;
|
|
58
|
+
|
|
59
|
+
// Quiz and narrative are 2D UI experiences, not 3D worlds — they get dedicated
|
|
60
|
+
// DOM/CSS scaffolds (no Three.js, no renderer, no game loop) instead of the
|
|
61
|
+
// shared 3D shell the exploration/custom types use.
|
|
62
|
+
if (type === 'quiz') {
|
|
63
|
+
createQuizProject(targetDir, options);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (type === 'narrative') {
|
|
67
|
+
createNarrativeProject(targetDir, options);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
59
72
|
|
|
60
73
|
// Create directory structure
|
|
@@ -81,6 +94,7 @@ export function createGameProject(targetDir, options) {
|
|
|
81
94
|
writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
|
|
82
95
|
writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
|
|
83
96
|
writeFileSync(join(targetDir, 'README.md'), generateReadme(name, description, type, ageGroup));
|
|
97
|
+
writeFileSync(join(targetDir, 'AGENTS.md'), generate3DGameAgents(name, type, orientation));
|
|
84
98
|
writeFileSync(join(targetDir, 'index.html'), generateIndexHtml(name, orientation));
|
|
85
99
|
writeFileSync(join(targetDir, 'assets/README.md'), generateAssetsReadme(orientation));
|
|
86
100
|
copyTemplateArt(targetDir, type, orientation);
|
|
@@ -93,23 +107,1086 @@ export function createGameProject(targetDir, options) {
|
|
|
93
107
|
writeFileSync(join(targetDir, 'src/utils/AudioManager.js'), generateAudioManager());
|
|
94
108
|
writeFileSync(join(targetDir, 'src/components/UI.js'), generateUI(type));
|
|
95
109
|
|
|
96
|
-
// Write game-type-specific
|
|
110
|
+
// Write the game-type-specific game logic. Quiz and narrative are handled
|
|
111
|
+
// earlier by their own 2D scaffolds, so only exploration and custom (default)
|
|
112
|
+
// reach this 3D path.
|
|
97
113
|
switch (type) {
|
|
98
|
-
case 'quiz':
|
|
99
|
-
writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateQuizGameLogic());
|
|
100
|
-
break;
|
|
101
114
|
case 'exploration':
|
|
102
115
|
writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateExplorationGameLogic());
|
|
103
116
|
break;
|
|
104
|
-
case 'narrative':
|
|
105
|
-
writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateNarrativeGameLogic());
|
|
106
|
-
break;
|
|
107
117
|
default:
|
|
108
118
|
writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateBaseGameLogic());
|
|
109
119
|
break;
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
// ── Quiz Game Project (2D) ─────────────────────────────────────────
|
|
124
|
+
// A quiz is a UI experience, so this scaffold is plain DOM/CSS — no Three.js,
|
|
125
|
+
// no renderer, no game loop. Two files hold everything a developer edits:
|
|
126
|
+
// src/questions.js - the questions + quiz settings (content)
|
|
127
|
+
// src/styles.css - the look
|
|
128
|
+
// src/main.js runs the quiz lifecycle (start → questions → results) and reports
|
|
129
|
+
// the result through the shared MyVillage game contract.
|
|
130
|
+
function createQuizProject(targetDir, options) {
|
|
131
|
+
const { name, description, ageGroup, orientation = 'portrait' } = options;
|
|
132
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
133
|
+
|
|
134
|
+
const dirs = ['', 'src', 'assets', 'public'];
|
|
135
|
+
for (const dir of dirs) {
|
|
136
|
+
mkdirSync(join(targetDir, dir), { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
writeFileSync(join(targetDir, 'package.json'), generate2DGamePackageJson(name, description, slug, ageGroup, orientation, 'quiz'));
|
|
140
|
+
writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
|
|
141
|
+
writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
|
|
142
|
+
writeFileSync(join(targetDir, 'index.html'), generate2DGameIndexHtml(name, orientation));
|
|
143
|
+
writeFileSync(join(targetDir, 'README.md'), generateQuizReadme(name, description, ageGroup));
|
|
144
|
+
writeFileSync(join(targetDir, 'AGENTS.md'), generate2DGameAgents(name, 'quiz', orientation));
|
|
145
|
+
|
|
146
|
+
writeFileSync(join(targetDir, 'src/main.js'), generateQuizApp());
|
|
147
|
+
writeFileSync(join(targetDir, 'src/questions.js'), generateQuizQuestions(name));
|
|
148
|
+
writeFileSync(join(targetDir, 'src/styles.css'), generateQuizStyles());
|
|
149
|
+
writeFileSync(join(targetDir, 'src/myvillage-game.js'), generateMyVillageGameSdk());
|
|
150
|
+
|
|
151
|
+
writeFileSync(join(targetDir, 'assets/README.md'), generateAssetsReadme(orientation));
|
|
152
|
+
copyTemplateArt(targetDir, 'quiz', orientation);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Shared by the 2D game types (quiz, narrative) — a plain Vite project with no
|
|
156
|
+
// Three.js. `gameType` goes into the myvillage block the deploy command reads.
|
|
157
|
+
function generate2DGamePackageJson(name, description, slug, ageGroup, orientation, gameType) {
|
|
158
|
+
return JSON.stringify({
|
|
159
|
+
name: slug,
|
|
160
|
+
version: '1.0.0',
|
|
161
|
+
description,
|
|
162
|
+
private: true,
|
|
163
|
+
type: 'module',
|
|
164
|
+
scripts: {
|
|
165
|
+
dev: 'vite',
|
|
166
|
+
build: 'vite build',
|
|
167
|
+
preview: 'vite preview',
|
|
168
|
+
},
|
|
169
|
+
// No Three.js — these are pure DOM/CSS games.
|
|
170
|
+
devDependencies: {
|
|
171
|
+
vite: '^5.0.0',
|
|
172
|
+
},
|
|
173
|
+
myvillage: {
|
|
174
|
+
name: name,
|
|
175
|
+
gameId: null,
|
|
176
|
+
gameType,
|
|
177
|
+
targetAge: ageGroup,
|
|
178
|
+
orientation,
|
|
179
|
+
aspectRatio: orientation === 'portrait' ? '9:16' : '16:9',
|
|
180
|
+
assets: {
|
|
181
|
+
thumbnail: 'assets/thumbnail.png',
|
|
182
|
+
banner: 'assets/banner.png',
|
|
183
|
+
},
|
|
184
|
+
villages: [],
|
|
185
|
+
lastDeployed: null,
|
|
186
|
+
},
|
|
187
|
+
}, null, 2) + '\n';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Shared by the 2D game types — links styles.css + main.js into a #app root.
|
|
191
|
+
function generate2DGameIndexHtml(name, orientation) {
|
|
192
|
+
return `<!DOCTYPE html>
|
|
193
|
+
<html lang="en">
|
|
194
|
+
<head>
|
|
195
|
+
<meta charset="UTF-8" />
|
|
196
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
197
|
+
<!-- orientation: ${orientation} -->
|
|
198
|
+
<title>${name}</title>
|
|
199
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<div id="app"></div>
|
|
203
|
+
<script type="module" src="/src/main.js"></script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function generateQuizQuestions(name) {
|
|
210
|
+
return `// Quiz content — edit this file to make the quiz your own.
|
|
211
|
+
//
|
|
212
|
+
// Each question has the prompt, a list of choices, and \`correct\`: the index of
|
|
213
|
+
// the right choice, starting at 0. Add or remove questions freely.
|
|
214
|
+
|
|
215
|
+
export const quizConfig = {
|
|
216
|
+
// Shown on the start screen.
|
|
217
|
+
title: ${JSON.stringify(name)},
|
|
218
|
+
intro: 'Answer the questions and see how many you get right!',
|
|
219
|
+
// Fraction of questions you must get right to "win" (0.6 = 60%).
|
|
220
|
+
passThreshold: 0.6,
|
|
221
|
+
// Briefly highlight the correct / incorrect answer after each pick.
|
|
222
|
+
showFeedback: true,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const questions = [
|
|
226
|
+
{
|
|
227
|
+
question: 'What is 2 + 2?',
|
|
228
|
+
choices: ['3', '4', '5', '6'],
|
|
229
|
+
correct: 1,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
question: 'What is the capital of France?',
|
|
233
|
+
choices: ['London', 'Berlin', 'Paris', 'Madrid'],
|
|
234
|
+
correct: 2,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
question: 'Which planet is closest to the Sun?',
|
|
238
|
+
choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
|
|
239
|
+
correct: 1,
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function generateQuizApp() {
|
|
246
|
+
return `// Quiz Game — a 2D question/answer game.
|
|
247
|
+
//
|
|
248
|
+
// Edit your questions and settings in src/questions.js, and the look in
|
|
249
|
+
// src/styles.css. This file runs the quiz: it renders the start screen, each
|
|
250
|
+
// question (with a progress bar + score), instant right/wrong feedback, and a
|
|
251
|
+
// results screen — then reports the result to the MyVillage host.
|
|
252
|
+
|
|
253
|
+
import { MyVillageGame } from './myvillage-game.js';
|
|
254
|
+
import { quizConfig, questions } from './questions.js';
|
|
255
|
+
|
|
256
|
+
class QuizGame {
|
|
257
|
+
constructor(root) {
|
|
258
|
+
this.root = root;
|
|
259
|
+
this.index = 0;
|
|
260
|
+
this.score = 0;
|
|
261
|
+
this.answered = false;
|
|
262
|
+
|
|
263
|
+
this.renderStart();
|
|
264
|
+
|
|
265
|
+
// Tell the MyVillage host the game is loaded and interactive.
|
|
266
|
+
MyVillageGame.ready();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
renderStart() {
|
|
270
|
+
this.root.innerHTML = \`
|
|
271
|
+
<main class="screen">
|
|
272
|
+
<div class="card start-card">
|
|
273
|
+
<h1 class="title">\${quizConfig.title}</h1>
|
|
274
|
+
<p class="intro">\${quizConfig.intro}</p>
|
|
275
|
+
<button class="btn primary" id="start-btn">Start Quiz</button>
|
|
276
|
+
</div>
|
|
277
|
+
</main>
|
|
278
|
+
\`;
|
|
279
|
+
this.root.querySelector('#start-btn').addEventListener('click', () => this.begin());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
begin() {
|
|
283
|
+
this.index = 0;
|
|
284
|
+
this.score = 0;
|
|
285
|
+
this.renderQuestion();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
renderQuestion() {
|
|
289
|
+
if (this.index >= questions.length) {
|
|
290
|
+
this.renderResults();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.answered = false;
|
|
295
|
+
const q = questions[this.index];
|
|
296
|
+
const total = questions.length;
|
|
297
|
+
const progress = (this.index / total) * 100;
|
|
298
|
+
|
|
299
|
+
this.root.innerHTML = \`
|
|
300
|
+
<main class="screen">
|
|
301
|
+
<header class="quiz-header">
|
|
302
|
+
<div class="progress"><div class="progress-fill" style="width: \${progress}%"></div></div>
|
|
303
|
+
<div class="meta">
|
|
304
|
+
<span>Question \${this.index + 1} of \${total}</span>
|
|
305
|
+
<span>Score: \${this.score}</span>
|
|
306
|
+
</div>
|
|
307
|
+
</header>
|
|
308
|
+
<div class="card question-card">
|
|
309
|
+
<p class="question">\${q.question}</p>
|
|
310
|
+
<div class="choices">
|
|
311
|
+
\${q.choices.map((choice, i) => \`<button class="btn choice" data-index="\${i}">\${choice}</button>\`).join('')}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</main>
|
|
315
|
+
\`;
|
|
316
|
+
|
|
317
|
+
this.root.querySelectorAll('.choice').forEach((btn) => {
|
|
318
|
+
btn.addEventListener('click', () => this.answer(Number(btn.dataset.index)));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
answer(choiceIndex) {
|
|
323
|
+
if (this.answered) return; // ignore extra taps while showing feedback
|
|
324
|
+
this.answered = true;
|
|
325
|
+
|
|
326
|
+
const q = questions[this.index];
|
|
327
|
+
if (choiceIndex === q.correct) this.score++;
|
|
328
|
+
|
|
329
|
+
// Reveal the right answer (and the wrong pick, if any).
|
|
330
|
+
const buttons = this.root.querySelectorAll('.choice');
|
|
331
|
+
buttons.forEach((btn, i) => {
|
|
332
|
+
btn.disabled = true;
|
|
333
|
+
if (quizConfig.showFeedback) {
|
|
334
|
+
if (i === q.correct) btn.classList.add('correct');
|
|
335
|
+
else if (i === choiceIndex) btn.classList.add('incorrect');
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const delay = quizConfig.showFeedback ? 900 : 150;
|
|
340
|
+
window.setTimeout(() => {
|
|
341
|
+
this.index++;
|
|
342
|
+
this.renderQuestion();
|
|
343
|
+
}, delay);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
renderResults() {
|
|
347
|
+
const total = questions.length;
|
|
348
|
+
const won = total > 0 && this.score / total >= quizConfig.passThreshold;
|
|
349
|
+
const pct = total > 0 ? Math.round((this.score / total) * 100) : 0;
|
|
350
|
+
|
|
351
|
+
this.root.innerHTML = \`
|
|
352
|
+
<main class="screen">
|
|
353
|
+
<div class="card results-card">
|
|
354
|
+
<h2 class="title">\${won ? 'Great job!' : 'Quiz complete'}</h2>
|
|
355
|
+
<p class="result-score">\${this.score} / \${total}</p>
|
|
356
|
+
<p class="result-pct">\${pct}% correct</p>
|
|
357
|
+
<button class="btn primary" id="again-btn">Play Again</button>
|
|
358
|
+
</div>
|
|
359
|
+
</main>
|
|
360
|
+
\`;
|
|
361
|
+
this.root.querySelector('#again-btn').addEventListener('click', () => this.begin());
|
|
362
|
+
|
|
363
|
+
// REQUIRED: tell the MyVillage host the game is finished. The host records
|
|
364
|
+
// the session, awards this game's MVT reward, and returns the player to the
|
|
365
|
+
// catalog. During \`npm run dev\` there's no host, so this just logs.
|
|
366
|
+
MyVillageGame.complete({ score: this.score, won, custom: { total } });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
new QuizGame(document.getElementById('app'));
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function generateQuizStyles() {
|
|
375
|
+
return `/* Quiz styling — tweak freely. Colors use the MyVillage palette. */
|
|
376
|
+
|
|
377
|
+
:root {
|
|
378
|
+
--gold: #FFD700;
|
|
379
|
+
--primary: #B07C00;
|
|
380
|
+
--cream: #E4DCCB;
|
|
381
|
+
--dark: #302017;
|
|
382
|
+
--deep-green: #043922;
|
|
383
|
+
--green: #228B22;
|
|
384
|
+
--teal: #799C9F;
|
|
385
|
+
--correct: #2e9e4f;
|
|
386
|
+
--incorrect: #c0392b;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
390
|
+
|
|
391
|
+
html, body {
|
|
392
|
+
height: 100%;
|
|
393
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
body {
|
|
397
|
+
background: linear-gradient(160deg, var(--deep-green), var(--dark));
|
|
398
|
+
color: var(--cream);
|
|
399
|
+
display: flex;
|
|
400
|
+
-webkit-font-smoothing: antialiased;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#app { flex: 1; display: flex; }
|
|
404
|
+
|
|
405
|
+
.screen {
|
|
406
|
+
flex: 1;
|
|
407
|
+
display: flex;
|
|
408
|
+
flex-direction: column;
|
|
409
|
+
justify-content: center;
|
|
410
|
+
align-items: center;
|
|
411
|
+
gap: 16px;
|
|
412
|
+
padding: 24px;
|
|
413
|
+
width: 100%;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.card {
|
|
417
|
+
width: 100%;
|
|
418
|
+
max-width: 480px;
|
|
419
|
+
background: rgba(48, 32, 23, 0.92);
|
|
420
|
+
border: 2px solid var(--primary);
|
|
421
|
+
border-radius: 20px;
|
|
422
|
+
padding: 28px 24px;
|
|
423
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.start-card, .results-card { text-align: center; }
|
|
427
|
+
|
|
428
|
+
.title {
|
|
429
|
+
color: var(--gold);
|
|
430
|
+
font-size: 30px;
|
|
431
|
+
margin-bottom: 12px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.intro {
|
|
435
|
+
font-size: 16px;
|
|
436
|
+
line-height: 1.5;
|
|
437
|
+
margin-bottom: 24px;
|
|
438
|
+
color: var(--cream);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Header: progress bar + meta row */
|
|
442
|
+
.quiz-header { width: 100%; max-width: 480px; }
|
|
443
|
+
|
|
444
|
+
.progress {
|
|
445
|
+
height: 8px;
|
|
446
|
+
background: rgba(255, 255, 255, 0.15);
|
|
447
|
+
border-radius: 99px;
|
|
448
|
+
overflow: hidden;
|
|
449
|
+
margin-bottom: 10px;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.progress-fill {
|
|
453
|
+
height: 100%;
|
|
454
|
+
background: var(--gold);
|
|
455
|
+
border-radius: 99px;
|
|
456
|
+
transition: width 0.3s ease;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.meta {
|
|
460
|
+
display: flex;
|
|
461
|
+
justify-content: space-between;
|
|
462
|
+
font-size: 14px;
|
|
463
|
+
color: var(--teal);
|
|
464
|
+
font-weight: 600;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.question {
|
|
468
|
+
font-size: 20px;
|
|
469
|
+
font-weight: 700;
|
|
470
|
+
color: var(--gold);
|
|
471
|
+
margin-bottom: 20px;
|
|
472
|
+
text-align: center;
|
|
473
|
+
line-height: 1.4;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.choices { display: flex; flex-direction: column; gap: 12px; }
|
|
477
|
+
|
|
478
|
+
/* Buttons */
|
|
479
|
+
.btn {
|
|
480
|
+
font-family: inherit;
|
|
481
|
+
font-size: 16px;
|
|
482
|
+
font-weight: 600;
|
|
483
|
+
border-radius: 12px;
|
|
484
|
+
border: none;
|
|
485
|
+
cursor: pointer;
|
|
486
|
+
min-height: 52px;
|
|
487
|
+
padding: 14px 18px;
|
|
488
|
+
transition: transform 0.05s ease, background 0.2s ease, border-color 0.2s ease;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.btn:active { transform: scale(0.98); }
|
|
492
|
+
|
|
493
|
+
.btn.primary {
|
|
494
|
+
background: var(--primary);
|
|
495
|
+
color: #fff;
|
|
496
|
+
width: 100%;
|
|
497
|
+
font-size: 18px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.btn.primary:hover { background: #c98f0a; }
|
|
501
|
+
|
|
502
|
+
.choice {
|
|
503
|
+
background: var(--deep-green);
|
|
504
|
+
color: var(--cream);
|
|
505
|
+
border: 2px solid var(--green);
|
|
506
|
+
text-align: left;
|
|
507
|
+
width: 100%;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.choice:hover:not(:disabled) { border-color: var(--gold); }
|
|
511
|
+
.choice:disabled { cursor: default; opacity: 0.95; }
|
|
512
|
+
|
|
513
|
+
.choice.correct {
|
|
514
|
+
background: var(--correct);
|
|
515
|
+
border-color: var(--correct);
|
|
516
|
+
color: #fff;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.choice.incorrect {
|
|
520
|
+
background: var(--incorrect);
|
|
521
|
+
border-color: var(--incorrect);
|
|
522
|
+
color: #fff;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.result-score {
|
|
526
|
+
font-size: 48px;
|
|
527
|
+
font-weight: 800;
|
|
528
|
+
color: var(--gold);
|
|
529
|
+
margin: 12px 0 4px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.result-pct {
|
|
533
|
+
font-size: 18px;
|
|
534
|
+
color: var(--teal);
|
|
535
|
+
margin-bottom: 24px;
|
|
536
|
+
}
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function generateQuizReadme(name, description, ageGroup) {
|
|
541
|
+
return `# ${name}
|
|
542
|
+
|
|
543
|
+
${description}
|
|
544
|
+
|
|
545
|
+
- **Game Type**: Quiz
|
|
546
|
+
- **Target Age Group**: ${ageGroup}
|
|
547
|
+
- **Built with**: MyVillageOS CLI
|
|
548
|
+
|
|
549
|
+
A 2D quiz game — no 3D engine. Edit two files to make it yours:
|
|
550
|
+
|
|
551
|
+
- **src/questions.js** — your questions, answers, and quiz settings (title, pass mark, feedback)
|
|
552
|
+
- **src/styles.css** — colors, fonts, and layout
|
|
553
|
+
|
|
554
|
+
## Getting Started
|
|
555
|
+
|
|
556
|
+
\`\`\`bash
|
|
557
|
+
npm install
|
|
558
|
+
npm run dev # play it locally with live reload
|
|
559
|
+
npm run build # build for production (outputs to dist/)
|
|
560
|
+
\`\`\`
|
|
561
|
+
|
|
562
|
+
## Editing questions
|
|
563
|
+
|
|
564
|
+
Open \`src/questions.js\`. Each question is an object with the prompt, the list of
|
|
565
|
+
\`choices\`, and \`correct\` — the index of the right choice (0 = the first):
|
|
566
|
+
|
|
567
|
+
\`\`\`js
|
|
568
|
+
{
|
|
569
|
+
question: 'Which planet is closest to the Sun?',
|
|
570
|
+
choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
|
|
571
|
+
correct: 1, // 'Mercury'
|
|
572
|
+
}
|
|
573
|
+
\`\`\`
|
|
574
|
+
|
|
575
|
+
\`quizConfig\` at the top of the file controls the title, intro text, the pass
|
|
576
|
+
threshold (fraction correct needed to "win"), and whether to flash right/wrong
|
|
577
|
+
feedback after each answer.
|
|
578
|
+
|
|
579
|
+
## The game contract
|
|
580
|
+
|
|
581
|
+
When the quiz finishes it calls \`MyVillageGame.complete({ score, won })\`, so the
|
|
582
|
+
MyVillage host records the session and awards the player this game's MVT reward.
|
|
583
|
+
\`MyVillageGame.ready()\` is called on load. During \`npm run dev\` there's no host
|
|
584
|
+
attached, so these just log to the console — that's expected.
|
|
585
|
+
|
|
586
|
+
## Deploying
|
|
587
|
+
|
|
588
|
+
Starter cover art ships in \`assets/\` — swap in your own thumbnail.png + banner.png, then:
|
|
589
|
+
|
|
590
|
+
\`\`\`bash
|
|
591
|
+
myvillage deploy
|
|
592
|
+
\`\`\`
|
|
593
|
+
|
|
594
|
+
## Learn More
|
|
595
|
+
|
|
596
|
+
- [Vite Documentation](https://vitejs.dev/)
|
|
597
|
+
- [MyVillageOS Developer Guide](https://portal.myvillageproject.ai)
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── Narrative Game Project (2D) ────────────────────────────────────
|
|
602
|
+
// A story game is text + choices, so this scaffold is plain DOM/CSS — no
|
|
603
|
+
// Three.js. Devs edit src/story.js (the branching scenes) and src/styles.css
|
|
604
|
+
// (the look). src/main.js runs the story and reports the result through the
|
|
605
|
+
// shared MyVillage game contract.
|
|
606
|
+
function createNarrativeProject(targetDir, options) {
|
|
607
|
+
const { name, description, ageGroup, orientation = 'portrait' } = options;
|
|
608
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
609
|
+
|
|
610
|
+
const dirs = ['', 'src', 'assets', 'public'];
|
|
611
|
+
for (const dir of dirs) {
|
|
612
|
+
mkdirSync(join(targetDir, dir), { recursive: true });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
writeFileSync(join(targetDir, 'package.json'), generate2DGamePackageJson(name, description, slug, ageGroup, orientation, 'narrative'));
|
|
616
|
+
writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
|
|
617
|
+
writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
|
|
618
|
+
writeFileSync(join(targetDir, 'index.html'), generate2DGameIndexHtml(name, orientation));
|
|
619
|
+
writeFileSync(join(targetDir, 'README.md'), generateNarrativeReadme(name, description, ageGroup));
|
|
620
|
+
writeFileSync(join(targetDir, 'AGENTS.md'), generate2DGameAgents(name, 'narrative', orientation));
|
|
621
|
+
|
|
622
|
+
writeFileSync(join(targetDir, 'src/main.js'), generateNarrativeApp());
|
|
623
|
+
writeFileSync(join(targetDir, 'src/story.js'), generateNarrativeStory(name));
|
|
624
|
+
writeFileSync(join(targetDir, 'src/styles.css'), generateNarrativeStyles());
|
|
625
|
+
writeFileSync(join(targetDir, 'src/myvillage-game.js'), generateMyVillageGameSdk());
|
|
626
|
+
|
|
627
|
+
writeFileSync(join(targetDir, 'assets/README.md'), generateAssetsReadme(orientation));
|
|
628
|
+
copyTemplateArt(targetDir, 'narrative', orientation);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function generateNarrativeApp() {
|
|
632
|
+
return `// Narrative Game — an interactive, choice-driven story.
|
|
633
|
+
//
|
|
634
|
+
// Edit your story in src/story.js (scenes, speakers, choices) and the look in
|
|
635
|
+
// src/styles.css. This file runs the story: it shows the start screen, each
|
|
636
|
+
// scene with its choices, branches on what the player picks, and an ending —
|
|
637
|
+
// then reports the result to the MyVillage host.
|
|
638
|
+
|
|
639
|
+
import { MyVillageGame } from './myvillage-game.js';
|
|
640
|
+
import { storyConfig, scenes } from './story.js';
|
|
641
|
+
|
|
642
|
+
class NarrativeGame {
|
|
643
|
+
constructor(root) {
|
|
644
|
+
this.root = root;
|
|
645
|
+
this.index = 0;
|
|
646
|
+
this.choicesMade = 0;
|
|
647
|
+
|
|
648
|
+
this.renderStart();
|
|
649
|
+
|
|
650
|
+
// Tell the MyVillage host the game is loaded and interactive.
|
|
651
|
+
MyVillageGame.ready();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
renderStart() {
|
|
655
|
+
this.root.innerHTML = \`
|
|
656
|
+
<main class="screen">
|
|
657
|
+
<div class="card start-card">
|
|
658
|
+
<h1 class="title">\${storyConfig.title}</h1>
|
|
659
|
+
<p class="intro">\${storyConfig.intro}</p>
|
|
660
|
+
<button class="btn primary" id="begin-btn">Begin</button>
|
|
661
|
+
</div>
|
|
662
|
+
</main>
|
|
663
|
+
\`;
|
|
664
|
+
this.root.querySelector('#begin-btn').addEventListener('click', () => this.begin());
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
begin() {
|
|
668
|
+
this.index = storyConfig.startScene || 0;
|
|
669
|
+
this.choicesMade = 0;
|
|
670
|
+
this.renderScene();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
renderScene() {
|
|
674
|
+
const scene = scenes[this.index];
|
|
675
|
+
if (!scene) {
|
|
676
|
+
this.renderEnding();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const hasChoices = Array.isArray(scene.choices) && scene.choices.length > 0;
|
|
681
|
+
const buttons = hasChoices
|
|
682
|
+
? scene.choices.map((c, i) => \`<button class="btn choice" data-index="\${i}">\${c.text}</button>\`).join('')
|
|
683
|
+
: \`<button class="btn primary" id="end-btn">The End</button>\`;
|
|
684
|
+
|
|
685
|
+
this.root.innerHTML = \`
|
|
686
|
+
<main class="screen">
|
|
687
|
+
<div class="card scene-card">
|
|
688
|
+
\${scene.speaker ? \`<p class="speaker">\${scene.speaker}</p>\` : ''}
|
|
689
|
+
<p class="scene-text">\${scene.text}</p>
|
|
690
|
+
<div class="choices">\${buttons}</div>
|
|
691
|
+
</div>
|
|
692
|
+
</main>
|
|
693
|
+
\`;
|
|
694
|
+
|
|
695
|
+
if (hasChoices) {
|
|
696
|
+
this.root.querySelectorAll('.choice').forEach((btn) => {
|
|
697
|
+
btn.addEventListener('click', () => this.choose(Number(btn.dataset.index)));
|
|
698
|
+
});
|
|
699
|
+
} else {
|
|
700
|
+
this.root.querySelector('#end-btn').addEventListener('click', () => this.renderEnding());
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
choose(choiceIndex) {
|
|
705
|
+
const scene = scenes[this.index];
|
|
706
|
+
const choice = scene.choices[choiceIndex];
|
|
707
|
+
this.choicesMade++;
|
|
708
|
+
// Jump to the linked scene. An undefined / out-of-range next ends the story.
|
|
709
|
+
this.index = choice && choice.next !== undefined ? choice.next : -1;
|
|
710
|
+
this.renderScene();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
renderEnding() {
|
|
714
|
+
this.root.innerHTML = \`
|
|
715
|
+
<main class="screen">
|
|
716
|
+
<div class="card results-card">
|
|
717
|
+
<h2 class="title">The End</h2>
|
|
718
|
+
<p class="intro">You made \${this.choicesMade} choice\${this.choicesMade === 1 ? '' : 's'} along the way.</p>
|
|
719
|
+
<button class="btn primary" id="again-btn">Play Again</button>
|
|
720
|
+
</div>
|
|
721
|
+
</main>
|
|
722
|
+
\`;
|
|
723
|
+
this.root.querySelector('#again-btn').addEventListener('click', () => this.begin());
|
|
724
|
+
|
|
725
|
+
// REQUIRED: tell the MyVillage host the story is finished. The host records
|
|
726
|
+
// the session, awards this game's MVT reward, and returns the player to the
|
|
727
|
+
// catalog. During \`npm run dev\` there's no host, so this just logs.
|
|
728
|
+
MyVillageGame.complete({ won: true, score: this.choicesMade, custom: { choices: this.choicesMade } });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
new NarrativeGame(document.getElementById('app'));
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function generateNarrativeStory(name) {
|
|
737
|
+
return `// Story content — edit this file to write your own branching narrative.
|
|
738
|
+
//
|
|
739
|
+
// Each scene has an optional \`speaker\`, the \`text\` to show, and \`choices\`.
|
|
740
|
+
// Each choice has \`text\` and \`next\`: the index of the scene to jump to.
|
|
741
|
+
// Set \`choices: null\` (or omit it) to mark a scene as the end of the story.
|
|
742
|
+
|
|
743
|
+
export const storyConfig = {
|
|
744
|
+
// Shown on the start screen.
|
|
745
|
+
title: ${JSON.stringify(name)},
|
|
746
|
+
intro: 'An interactive story — your choices shape what happens next.',
|
|
747
|
+
// Index of the scene the story opens on.
|
|
748
|
+
startScene: 0,
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
export const scenes = [
|
|
752
|
+
{
|
|
753
|
+
speaker: 'Narrator',
|
|
754
|
+
text: 'You find yourself at the entrance to a mysterious village. The golden gates shine in the sunlight.',
|
|
755
|
+
choices: [
|
|
756
|
+
{ text: 'Enter the village', next: 1 },
|
|
757
|
+
{ text: 'Look around first', next: 2 },
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
speaker: 'Village Elder',
|
|
762
|
+
text: 'Welcome, young traveler! We have been expecting you. Our village needs your help.',
|
|
763
|
+
choices: [
|
|
764
|
+
{ text: 'How can I help?', next: 3 },
|
|
765
|
+
{ text: 'What happened here?', next: 3 },
|
|
766
|
+
],
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
speaker: 'Narrator',
|
|
770
|
+
text: 'You notice a weathered signpost and colorful flowers lining a cobblestone path. The village looks peaceful, but there is an air of anticipation.',
|
|
771
|
+
choices: [
|
|
772
|
+
{ text: 'Follow the path inside', next: 1 },
|
|
773
|
+
],
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
speaker: 'Village Elder',
|
|
777
|
+
text: 'A great challenge lies ahead. Only someone with courage and wisdom can solve it. Will you accept the quest?',
|
|
778
|
+
choices: [
|
|
779
|
+
{ text: 'I accept!', next: 4 },
|
|
780
|
+
{ text: 'Tell me more first', next: 4 },
|
|
781
|
+
],
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
speaker: 'Narrator',
|
|
785
|
+
text: 'And so your adventure begins. This is where your story continues — edit src/story.js to write your own!',
|
|
786
|
+
choices: null, // null (or omitted) = the end of the story
|
|
787
|
+
},
|
|
788
|
+
];
|
|
789
|
+
`;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function generateNarrativeStyles() {
|
|
793
|
+
return `/* Narrative styling — tweak freely. Colors use the MyVillage palette. */
|
|
794
|
+
|
|
795
|
+
:root {
|
|
796
|
+
--gold: #FFD700;
|
|
797
|
+
--primary: #B07C00;
|
|
798
|
+
--cream: #E4DCCB;
|
|
799
|
+
--dark: #302017;
|
|
800
|
+
--deep-green: #043922;
|
|
801
|
+
--green: #228B22;
|
|
802
|
+
--teal: #799C9F;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
806
|
+
|
|
807
|
+
html, body {
|
|
808
|
+
height: 100%;
|
|
809
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
body {
|
|
813
|
+
background: linear-gradient(160deg, var(--deep-green), var(--dark));
|
|
814
|
+
color: var(--cream);
|
|
815
|
+
display: flex;
|
|
816
|
+
-webkit-font-smoothing: antialiased;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
#app { flex: 1; display: flex; }
|
|
820
|
+
|
|
821
|
+
.screen {
|
|
822
|
+
flex: 1;
|
|
823
|
+
display: flex;
|
|
824
|
+
flex-direction: column;
|
|
825
|
+
justify-content: center;
|
|
826
|
+
align-items: center;
|
|
827
|
+
padding: 24px;
|
|
828
|
+
width: 100%;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.card {
|
|
832
|
+
width: 100%;
|
|
833
|
+
max-width: 540px;
|
|
834
|
+
background: rgba(48, 32, 23, 0.92);
|
|
835
|
+
border: 2px solid var(--primary);
|
|
836
|
+
border-radius: 20px;
|
|
837
|
+
padding: 28px 24px;
|
|
838
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.start-card, .results-card { text-align: center; }
|
|
842
|
+
.scene-card { text-align: left; }
|
|
843
|
+
|
|
844
|
+
.title { color: var(--gold); font-size: 30px; margin-bottom: 12px; }
|
|
845
|
+
.intro { font-size: 16px; line-height: 1.5; margin-bottom: 24px; }
|
|
846
|
+
|
|
847
|
+
.speaker {
|
|
848
|
+
color: var(--gold);
|
|
849
|
+
font-weight: 700;
|
|
850
|
+
font-size: 13px;
|
|
851
|
+
letter-spacing: 0.08em;
|
|
852
|
+
text-transform: uppercase;
|
|
853
|
+
margin-bottom: 10px;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.scene-text {
|
|
857
|
+
font-size: 18px;
|
|
858
|
+
line-height: 1.6;
|
|
859
|
+
margin-bottom: 24px;
|
|
860
|
+
color: var(--cream);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.choices { display: flex; flex-direction: column; gap: 12px; }
|
|
864
|
+
|
|
865
|
+
.btn {
|
|
866
|
+
font-family: inherit;
|
|
867
|
+
font-size: 16px;
|
|
868
|
+
font-weight: 600;
|
|
869
|
+
border-radius: 12px;
|
|
870
|
+
border: none;
|
|
871
|
+
cursor: pointer;
|
|
872
|
+
min-height: 52px;
|
|
873
|
+
padding: 14px 18px;
|
|
874
|
+
transition: transform 0.05s ease, background 0.2s ease, border-color 0.2s ease;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.btn:active { transform: scale(0.98); }
|
|
878
|
+
|
|
879
|
+
.btn.primary { background: var(--primary); color: #fff; width: 100%; font-size: 18px; }
|
|
880
|
+
.btn.primary:hover { background: #c98f0a; }
|
|
881
|
+
|
|
882
|
+
.choice {
|
|
883
|
+
background: var(--deep-green);
|
|
884
|
+
color: var(--cream);
|
|
885
|
+
border: 2px solid var(--green);
|
|
886
|
+
text-align: left;
|
|
887
|
+
width: 100%;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.choice:hover { border-color: var(--gold); }
|
|
891
|
+
`;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function generateNarrativeReadme(name, description, ageGroup) {
|
|
895
|
+
return `# ${name}
|
|
896
|
+
|
|
897
|
+
${description}
|
|
898
|
+
|
|
899
|
+
- **Game Type**: Narrative
|
|
900
|
+
- **Target Age Group**: ${ageGroup}
|
|
901
|
+
- **Built with**: MyVillageOS CLI
|
|
902
|
+
|
|
903
|
+
An interactive, choice-driven story — no 3D engine. Edit two files to make it yours:
|
|
904
|
+
|
|
905
|
+
- **src/story.js** — your scenes, speakers, and branching choices
|
|
906
|
+
- **src/styles.css** — colors, fonts, and layout
|
|
907
|
+
|
|
908
|
+
## Getting Started
|
|
909
|
+
|
|
910
|
+
\`\`\`bash
|
|
911
|
+
npm install
|
|
912
|
+
npm run dev # play it locally with live reload
|
|
913
|
+
npm run build # build for production (outputs to dist/)
|
|
914
|
+
\`\`\`
|
|
915
|
+
|
|
916
|
+
## Writing your story
|
|
917
|
+
|
|
918
|
+
Open \`src/story.js\`. Each scene has an optional \`speaker\`, the \`text\` to show,
|
|
919
|
+
and \`choices\`. Each choice has \`text\` and \`next\` — the index of the scene to
|
|
920
|
+
jump to. Branch the story by pointing choices at different scenes:
|
|
921
|
+
|
|
922
|
+
\`\`\`js
|
|
923
|
+
{
|
|
924
|
+
speaker: 'Village Elder',
|
|
925
|
+
text: 'Will you accept the quest?',
|
|
926
|
+
choices: [
|
|
927
|
+
{ text: 'I accept!', next: 4 },
|
|
928
|
+
{ text: 'Not yet', next: 2 },
|
|
929
|
+
],
|
|
930
|
+
}
|
|
931
|
+
\`\`\`
|
|
932
|
+
|
|
933
|
+
Set \`choices: null\` (or omit it) to make a scene the end of the story.
|
|
934
|
+
|
|
935
|
+
## The game contract
|
|
936
|
+
|
|
937
|
+
When the story ends it calls \`MyVillageGame.complete({ won, score })\`, so the
|
|
938
|
+
MyVillage host records the session and awards the player this game's MVT reward.
|
|
939
|
+
\`MyVillageGame.ready()\` is called on load. During \`npm run dev\` there's no host
|
|
940
|
+
attached, so these just log to the console — that's expected.
|
|
941
|
+
|
|
942
|
+
## Deploying
|
|
943
|
+
|
|
944
|
+
Starter cover art ships in \`assets/\` — swap in your own thumbnail.png + banner.png, then:
|
|
945
|
+
|
|
946
|
+
\`\`\`bash
|
|
947
|
+
myvillage deploy
|
|
948
|
+
\`\`\`
|
|
949
|
+
|
|
950
|
+
## Learn More
|
|
951
|
+
|
|
952
|
+
- [Vite Documentation](https://vitejs.dev/)
|
|
953
|
+
- [MyVillageOS Developer Guide](https://portal.myvillageproject.ai)
|
|
954
|
+
`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// ── AGENTS.md generators ───────────────────────────────────────────
|
|
958
|
+
// Each scaffold ships an AGENTS.md — a guide written for AI coding agents
|
|
959
|
+
// (Claude Code, Cursor, Codex, etc. read it automatically). It points the agent
|
|
960
|
+
// at the right files, documents the available APIs + the MyVillage game
|
|
961
|
+
// contract, and flags what must not break. Built as string arrays (not template
|
|
962
|
+
// literals) so the heavy inline-code backticks need no escaping.
|
|
963
|
+
|
|
964
|
+
function generate2DGameAgents(name, gameType, orientation) {
|
|
965
|
+
const isQuiz = gameType === 'quiz';
|
|
966
|
+
const kind = isQuiz ? 'quiz' : 'narrative';
|
|
967
|
+
const engine = isQuiz ? 'QuizGame' : 'NarrativeGame';
|
|
968
|
+
const contentFile = isQuiz ? 'src/questions.js' : 'src/story.js';
|
|
969
|
+
const endFn = isQuiz ? 'renderResults()' : 'renderEnding()';
|
|
970
|
+
const banner = orientation === 'portrait'
|
|
971
|
+
? '9:16 (e.g. 1080x1920) for this portrait game'
|
|
972
|
+
: '16:9 (e.g. 1920x1080) for this landscape game';
|
|
973
|
+
const contentLine = isQuiz
|
|
974
|
+
? '**Content — edit `src/questions.js` first.** It exports `quizConfig` (title, intro, `passThreshold`, `showFeedback`) and a `questions` array. Each question is `{ question, choices, correct }`, where `correct` is the 0-based index of the right choice.'
|
|
975
|
+
: '**Content — edit `src/story.js` first.** It exports `storyConfig` (title, intro, `startScene`) and a `scenes` array. Each scene is `{ speaker, text, choices }`; each choice is `{ text, next }`, where `next` is the index of the next scene. Set `choices: null` to end the story. Branch by pointing choices at different scenes.';
|
|
976
|
+
const flowLine = isQuiz
|
|
977
|
+
? '**Flow & rules — `src/main.js`.** The `QuizGame` class renders each screen (start -> question -> results) and wires the buttons. Edit here for scoring, timing, or new screens.'
|
|
978
|
+
: '**Flow — `src/main.js`.** The `NarrativeGame` class renders each screen (start -> scene -> ending), follows each choice\'s `next`, and wires the buttons.';
|
|
979
|
+
|
|
980
|
+
return [
|
|
981
|
+
'# AGENTS.md — working on this MyVillage game with an AI agent',
|
|
982
|
+
'',
|
|
983
|
+
'**' + name + '** is a MyVillage ' + kind + ' game — a 2D web game (plain DOM + CSS, no 3D engine, no Three.js) built with Vite. It runs inside the MyVillage host (the muni mobile app or the web player) and standalone in a browser.',
|
|
984
|
+
'',
|
|
985
|
+
'When editing: make the requested change, keep the **MyVillage game contract** intact (see below), and keep `npm run build` working.',
|
|
986
|
+
'',
|
|
987
|
+
'## Where common changes go',
|
|
988
|
+
'',
|
|
989
|
+
'- ' + contentLine,
|
|
990
|
+
'- **Look & feel — `src/styles.css`.** Colors are CSS variables in `:root` (the MyVillage palette). Cards, buttons, layout, fonts.',
|
|
991
|
+
'- ' + flowLine,
|
|
992
|
+
'',
|
|
993
|
+
'## File map',
|
|
994
|
+
'',
|
|
995
|
+
'| File | Purpose |',
|
|
996
|
+
'|------|---------|',
|
|
997
|
+
'| `index.html` | Entry point; mounts `#app`, loads `main.js` + `styles.css`. |',
|
|
998
|
+
'| `src/main.js` | The `' + engine + '` engine — lifecycle + rendering. |',
|
|
999
|
+
'| `' + contentFile + '` | **The content** — edit this first for content changes. |',
|
|
1000
|
+
'| `src/styles.css` | Styling (CSS variables in `:root`). |',
|
|
1001
|
+
'| `src/myvillage-game.js` | Host bridge — **do not edit.** |',
|
|
1002
|
+
'| `assets/` | Cover art shipped on deploy. |',
|
|
1003
|
+
'| `package.json` | The `myvillage` block holds metadata `myvillage deploy` reads. |',
|
|
1004
|
+
'',
|
|
1005
|
+
'## The MyVillage game contract — DO NOT BREAK',
|
|
1006
|
+
'',
|
|
1007
|
+
'The game talks to its host through `src/myvillage-game.js`:',
|
|
1008
|
+
'',
|
|
1009
|
+
'- `MyVillageGame.ready()` — called on load. Leave it.',
|
|
1010
|
+
'- `MyVillageGame.complete({ score, won })` — **required.** Called when the game ends (in `' + endFn + '`). It is how the host records the session and awards the player\'s reward. If you change the end flow, keep this call wired to the real end condition.',
|
|
1011
|
+
'',
|
|
1012
|
+
'Do **not** add an Exit button — the host always provides its own.',
|
|
1013
|
+
'',
|
|
1014
|
+
'During `npm run dev` there is no host attached, so `ready()`/`complete()` just log to the console. That is expected — do not try to "fix" it.',
|
|
1015
|
+
'',
|
|
1016
|
+
'## Run / build / deploy',
|
|
1017
|
+
'',
|
|
1018
|
+
' npm install',
|
|
1019
|
+
' npm run dev # local dev server, live reload',
|
|
1020
|
+
' npm run build # production build -> dist/ (must pass before deploy)',
|
|
1021
|
+
' myvillage deploy # build, then upload + submit to MyVillage',
|
|
1022
|
+
'',
|
|
1023
|
+
'Keep asset URLs relative (Vite `base: \'./\'`). The host serves the game from a sub-path, so absolute `/...` URLs will break.',
|
|
1024
|
+
'',
|
|
1025
|
+
'## Cover art (`assets/`)',
|
|
1026
|
+
'',
|
|
1027
|
+
'Starter art ships here — replace with your own, keeping the same filenames and sizes:',
|
|
1028
|
+
'',
|
|
1029
|
+
'- `thumbnail.png` — square 1:1 (e.g. 512x512). The game card; required.',
|
|
1030
|
+
'- `banner.png` — ' + banner + '. Featured hero; optional.',
|
|
1031
|
+
'',
|
|
1032
|
+
'## Conventions',
|
|
1033
|
+
'',
|
|
1034
|
+
'- Orientation is **' + orientation + '** (set in `package.json` -> `myvillage.orientation`). Portrait runs on muni mobile **and** web; landscape is web-only.',
|
|
1035
|
+
'- Vanilla JS, no framework — keep it dependency-light.',
|
|
1036
|
+
'',
|
|
1037
|
+
].join('\n');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function generate3DGameAgents(name, type, orientation) {
|
|
1041
|
+
const isExploration = type === 'exploration';
|
|
1042
|
+
const banner = orientation === 'portrait'
|
|
1043
|
+
? '9:16 (e.g. 1080x1920) for this portrait game'
|
|
1044
|
+
: '16:9 (e.g. 1920x1080) for this landscape game';
|
|
1045
|
+
const logicLine = isExploration
|
|
1046
|
+
? 'Holds the gameplay — player movement (via `input.getMovementVector()`), collectible spawning + collision, the follow-camera, score, and win condition. It calls `input.enableJoystick()` in `start()` so mobile gets on-screen movement.'
|
|
1047
|
+
: 'A blank starter — `update(delta)` is empty with hints. Build your mechanics here; the 3D scene, input, audio, UI, and game loop are already wired.';
|
|
1048
|
+
const mobileLine = isExploration
|
|
1049
|
+
? 'This game already calls `this.game.input.enableJoystick()` in `GameLogic.start()`, so touch devices get an on-screen movement thumbstick (no-op on desktop). Read movement with `this.game.input.getMovementVector()`.'
|
|
1050
|
+
: 'If your game uses movement, call `this.game.input.enableJoystick()` in `GameLogic.start()` to show an on-screen thumbstick on touch devices (no-op on desktop), then read it with `this.game.input.getMovementVector()`.';
|
|
1051
|
+
const uiExtra = isExploration
|
|
1052
|
+
? ' This game also adds `showMinimap(pos, items)` and `showInventory(items)`.'
|
|
1053
|
+
: '';
|
|
1054
|
+
|
|
1055
|
+
return [
|
|
1056
|
+
'# AGENTS.md — working on this MyVillage game with an AI agent',
|
|
1057
|
+
'',
|
|
1058
|
+
'**' + name + '** is a MyVillage ' + (isExploration ? 'exploration' : 'custom') + ' game — a 3D web game built with **Three.js** and Vite. It runs inside the MyVillage host (the muni mobile app or the web player) and standalone in a browser.',
|
|
1059
|
+
'',
|
|
1060
|
+
'When editing: make the requested change, keep the **MyVillage game contract** intact (see below), and keep `npm run build` working.',
|
|
1061
|
+
'',
|
|
1062
|
+
'## Where common changes go',
|
|
1063
|
+
'',
|
|
1064
|
+
'- **Gameplay — `src/components/GameLogic.js`.** ' + logicLine,
|
|
1065
|
+
'- **3D scene — `src/scenes/MainScene.js`.** Camera, lighting, ground, objects. The starter has a sky, a green ground plane, and a sample rotating cube (`this.sampleObject`). Add your meshes here.',
|
|
1066
|
+
'- **On-screen UI — `src/components/UI.js`.** HTML overlay (start menu, HUD, pause menu, game-over) rendered over the 3D canvas.',
|
|
1067
|
+
'- **Page/overlay styles are inline in `index.html`.**',
|
|
1068
|
+
'',
|
|
1069
|
+
'## File map',
|
|
1070
|
+
'',
|
|
1071
|
+
'| File | Purpose |',
|
|
1072
|
+
'|------|---------|',
|
|
1073
|
+
'| `src/main.js` | The `Game` class — Three.js renderer, state machine (`loading`/`menu`/`playing`/`paused`/`gameover`), the `animate()` loop, host wiring. |',
|
|
1074
|
+
'| `src/scenes/MainScene.js` | The 3D scene (camera, lights, ground, objects). |',
|
|
1075
|
+
'| `src/components/GameLogic.js` | **Your gameplay** — `start()`, `update(delta)`, `reset()`. |',
|
|
1076
|
+
'| `src/components/UI.js` | HTML overlay (menus, HUD). |',
|
|
1077
|
+
'| `src/utils/InputManager.js` | Keyboard / mouse / touch + mobile joystick. |',
|
|
1078
|
+
'| `src/utils/AudioManager.js` | Sound effects + music (Web Audio). |',
|
|
1079
|
+
'| `src/myvillage-game.js` | Host bridge — **do not edit.** |',
|
|
1080
|
+
'| `assets/` | Cover art shipped on deploy. |',
|
|
1081
|
+
'| `public/assets/` | Drop 3D models / textures / audio here. |',
|
|
1082
|
+
'',
|
|
1083
|
+
'## Engine API (reach it as `this.game` inside GameLogic)',
|
|
1084
|
+
'',
|
|
1085
|
+
'- `this.game.scene` — `.scene` (THREE.Scene), `.camera` (THREE.PerspectiveCamera), `.sampleObject`, `.update(delta)`.',
|
|
1086
|
+
'- `this.game.input` — `getMovementVector()` (returns `{ x, z }` from WASD/arrows or the joystick), `isKeyDown(code)`, `isKeyJustPressed(code)`, `enableJoystick()`, `clearFrame()`, `mouse`, `touches`.',
|
|
1087
|
+
'- `this.game.audio` — `playSFX(name)`, `playMusic(name)`, `stopMusic()`, `setMusicVolume(v)`, `setSFXVolume(v)`.',
|
|
1088
|
+
'- `this.game.ui` — `showMenu()`, `showHUD()`, `showPauseMenu()`, `showGameOver(result)`, `clearOverlay()`, `createElement(tag, styles)`.' + uiExtra,
|
|
1089
|
+
'- `this.game` — `startGame()`, `pauseGame()`, `resumeGame()`, `endGame(result)`, `state`.',
|
|
1090
|
+
'',
|
|
1091
|
+
'## Mobile controls',
|
|
1092
|
+
'',
|
|
1093
|
+
mobileLine,
|
|
1094
|
+
'',
|
|
1095
|
+
'## The MyVillage game contract — DO NOT BREAK',
|
|
1096
|
+
'',
|
|
1097
|
+
'- `MyVillageGame.ready()` — called after assets load in `src/main.js`. Leave it.',
|
|
1098
|
+
'- `this.game.endGame({ won, score })` — call at your win/lose condition. It calls `MyVillageGame.complete(...)`, which is **required**: the host records the session and awards the player\'s reward.',
|
|
1099
|
+
'',
|
|
1100
|
+
'Do **not** add an Exit button — the host always provides its own. During `npm run dev` there is no host, so host calls just log to the console (expected).',
|
|
1101
|
+
'',
|
|
1102
|
+
'## Run / build / deploy',
|
|
1103
|
+
'',
|
|
1104
|
+
' npm install',
|
|
1105
|
+
' npm run dev # local dev server, live reload',
|
|
1106
|
+
' npm run build # production build -> dist/ (must pass before deploy)',
|
|
1107
|
+
' myvillage deploy # build, then upload + submit to MyVillage',
|
|
1108
|
+
'',
|
|
1109
|
+
'Keep asset URLs relative (Vite `base: \'./\'`). The host serves from a sub-path, so absolute `/...` URLs break.',
|
|
1110
|
+
'',
|
|
1111
|
+
'## Cover art (`assets/`)',
|
|
1112
|
+
'',
|
|
1113
|
+
'Starter art ships here — replace with your own, same filenames and sizes:',
|
|
1114
|
+
'',
|
|
1115
|
+
'- `thumbnail.png` — square 1:1 (e.g. 512x512). The game card; required.',
|
|
1116
|
+
'- `banner.png` — ' + banner + '. Featured hero; optional.',
|
|
1117
|
+
'',
|
|
1118
|
+
'## Conventions',
|
|
1119
|
+
'',
|
|
1120
|
+
'- Orientation is **' + orientation + '** (`package.json` -> `myvillage.orientation`). Portrait runs on muni mobile **and** web; landscape is web-only.',
|
|
1121
|
+
'- The follow-camera and game loop already exist — extend them, do not rebuild.',
|
|
1122
|
+
'',
|
|
1123
|
+
].join('\n');
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function generateUnityAgents(name, className, orientation) {
|
|
1127
|
+
const isLandscape = orientation === 'landscape';
|
|
1128
|
+
const reach = isLandscape
|
|
1129
|
+
? 'web (WebGL) only'
|
|
1130
|
+
: 'the muni mobile app and the web';
|
|
1131
|
+
const banner = isLandscape
|
|
1132
|
+
? '16:9 (e.g. 1920x1080) for this landscape game'
|
|
1133
|
+
: '9:16 (e.g. 1080x1920) for this portrait game';
|
|
1134
|
+
|
|
1135
|
+
return [
|
|
1136
|
+
'# AGENTS.md — working on this MyVillage Unity game with an AI agent',
|
|
1137
|
+
'',
|
|
1138
|
+
'**' + name + '** is a MyVillage Unity game built on the **MyVillage GameKit SDK** (a UPM package pinned in `Packages/manifest.json`). The mission is C#; it builds to AssetBundles the M-UNI host loads.',
|
|
1139
|
+
'',
|
|
1140
|
+
'When editing: change the mission logic, keep the **game contract** intact (`CompleteMission` / `FailMission`), and do not break the GameKit `MissionBase` lifecycle.',
|
|
1141
|
+
'',
|
|
1142
|
+
'## Where common changes go',
|
|
1143
|
+
'',
|
|
1144
|
+
'- **Mission logic — `Assets/Scripts/' + className + '.cs`.** Your `MissionBase` subclass. Override `OnBegin()` with your gameplay and call `CompleteMission(finalScore: n)` (or `FailMission(reason)`) when the player finishes. `OnEnd()` is for cleanup.',
|
|
1145
|
+
'- **Scene & objects — the Unity scene** (create via the editor menu `MyVillage -> Create Starter Mission Scene`). Add GameObjects, art, and audio in the Unity editor.',
|
|
1146
|
+
'- **Orientation — `Assets/Editor/MyVillageOrientation.cs`** pins it on first load.',
|
|
1147
|
+
'',
|
|
1148
|
+
'## File map',
|
|
1149
|
+
'',
|
|
1150
|
+
'| Path | Purpose |',
|
|
1151
|
+
'|------|---------|',
|
|
1152
|
+
'| `Assets/Scripts/' + className + '.cs` | **Your mission** — the `MissionBase` subclass. Edit here. |',
|
|
1153
|
+
'| `Assets/Editor/MyVillageOrientation.cs` | Pins player orientation on first load. |',
|
|
1154
|
+
'| `Packages/manifest.json` | Depends on the GameKit SDK (pinned version). Do not hand-edit the SDK. |',
|
|
1155
|
+
'| `myvillage.json` | Game metadata the CLI/deploy reads (slug, gameType, orientation, assets). |',
|
|
1156
|
+
'| `assets/` | Cover art shipped on deploy (thumbnail/banner). |',
|
|
1157
|
+
'| `builds/{ios,android,webgl}/` | Drop-zone for built AssetBundles the CLI deploys. |',
|
|
1158
|
+
'',
|
|
1159
|
+
'## The game contract (GameKit `MissionBase`) — DO NOT BREAK',
|
|
1160
|
+
'',
|
|
1161
|
+
'- The host drives your mission and runs `OnBegin()`. In the editor, `MockMissionHost` stands in and logs host calls to the Console.',
|
|
1162
|
+
'- **`CompleteMission(finalScore: n)`** — call when the player finishes. This is the native equivalent of the web games\' `MyVillageGame.complete()`; the host records the session and awards the player\'s reward. **Required to end the game properly.**',
|
|
1163
|
+
'- `FailMission(reason)` — the lose path. `Host.LogEvent("...")` — telemetry.',
|
|
1164
|
+
'- **Do not build an Exit button** — the host always shows its own and calls `OnEnd()` for cleanup when the player exits.',
|
|
1165
|
+
'',
|
|
1166
|
+
'The same script runs unchanged in the real M-UNI host (a `MissionHostAdapter` replaces `MockMissionHost`). Do not add platform-specific host code.',
|
|
1167
|
+
'',
|
|
1168
|
+
'## Build & deploy',
|
|
1169
|
+
'',
|
|
1170
|
+
'1. In Unity: **`MyVillage -> Build All Platforms`** (ships in GameKit). Builds iOS, Android, and WebGL into `builds/`.',
|
|
1171
|
+
'2. From the terminal: `myvillage deploy` (scans `builds/`, uploads, submits).',
|
|
1172
|
+
'',
|
|
1173
|
+
'This game\'s orientation is **' + orientation + '** — it runs on ' + reach + '.',
|
|
1174
|
+
'',
|
|
1175
|
+
'## Cover art (`assets/`)',
|
|
1176
|
+
'',
|
|
1177
|
+
'Starter art ships here — replace with your own, same filenames and sizes:',
|
|
1178
|
+
'',
|
|
1179
|
+
'- `thumbnail.png` — square 1:1 (e.g. 512x512). The game card; required.',
|
|
1180
|
+
'- `banner.png` — ' + banner + '. Featured hero; optional.',
|
|
1181
|
+
'',
|
|
1182
|
+
'## Gotchas',
|
|
1183
|
+
'',
|
|
1184
|
+
'- Do not edit the GameKit SDK package — it is a pinned dependency. Use its `MissionBase` API.',
|
|
1185
|
+
'- Visual Scripting reflection nodes (`InvokeMember`/`GetMember`/`SetMember`) are rejected by the deploy preflight. Write C# instead.',
|
|
1186
|
+
'',
|
|
1187
|
+
].join('\n');
|
|
1188
|
+
}
|
|
1189
|
+
|
|
113
1190
|
// ── Unity Game Project ─────────────────────────────────────────────
|
|
114
1191
|
|
|
115
1192
|
export async function createUnityGameProject(targetDir, options) {
|
|
@@ -192,6 +1269,7 @@ export async function createUnityGameProject(targetDir, options) {
|
|
|
192
1269
|
copyTemplateArt(targetDir, type, orientation);
|
|
193
1270
|
writeFileSync(join(targetDir, '.gitignore'), generateUnityGitignore());
|
|
194
1271
|
writeFileSync(join(targetDir, 'README.md'), generateUnityReadme(name, slug, className, orientation, unityVersion));
|
|
1272
|
+
writeFileSync(join(targetDir, 'AGENTS.md'), generateUnityAgents(name, className, orientation));
|
|
195
1273
|
|
|
196
1274
|
return { unityVersion, sdkVersion: GAMEKIT_SDK_VERSION, gameType: primaryGameType };
|
|
197
1275
|
}
|
|
@@ -1350,52 +2428,6 @@ function generateUI(type) {
|
|
|
1350
2428
|
let typeSpecificUI = '';
|
|
1351
2429
|
|
|
1352
2430
|
switch (type) {
|
|
1353
|
-
case 'quiz':
|
|
1354
|
-
typeSpecificUI = `
|
|
1355
|
-
showQuestion(question, choices, onAnswer) {
|
|
1356
|
-
this.clearOverlay();
|
|
1357
|
-
|
|
1358
|
-
const panel = this.createElement('div', {
|
|
1359
|
-
position: 'absolute', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
|
|
1360
|
-
width: '90%', maxWidth: '600px', background: 'rgba(48,32,23,0.95)',
|
|
1361
|
-
borderRadius: '16px', padding: '24px', color: '#E4DCCB',
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
const qText = this.createElement('p', {
|
|
1365
|
-
fontSize: '18px', fontWeight: 'bold', marginBottom: '16px',
|
|
1366
|
-
color: '#FFD700', textAlign: 'center',
|
|
1367
|
-
});
|
|
1368
|
-
qText.textContent = question;
|
|
1369
|
-
panel.appendChild(qText);
|
|
1370
|
-
|
|
1371
|
-
choices.forEach((choice, index) => {
|
|
1372
|
-
const btn = this.createElement('button', {
|
|
1373
|
-
display: 'block', width: '100%', padding: '12px',
|
|
1374
|
-
margin: '8px 0', background: '#043922', color: '#E4DCCB',
|
|
1375
|
-
border: '2px solid #228B22', borderRadius: '8px',
|
|
1376
|
-
fontSize: '16px', cursor: 'pointer', textAlign: 'left',
|
|
1377
|
-
});
|
|
1378
|
-
btn.textContent = choice;
|
|
1379
|
-
btn.addEventListener('click', () => onAnswer(index));
|
|
1380
|
-
panel.appendChild(btn);
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
this.overlay.appendChild(panel);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
showScore(score, total) {
|
|
1387
|
-
this.clearOverlay();
|
|
1388
|
-
|
|
1389
|
-
const panel = this.createElement('div', {
|
|
1390
|
-
position: 'absolute', top: '10px', right: '10px',
|
|
1391
|
-
background: 'rgba(48,32,23,0.85)', borderRadius: '12px',
|
|
1392
|
-
padding: '10px 18px', color: '#FFD700', fontSize: '18px', fontWeight: 'bold',
|
|
1393
|
-
});
|
|
1394
|
-
panel.textContent = \`Score: \${score} / \${total}\`;
|
|
1395
|
-
this.overlay.appendChild(panel);
|
|
1396
|
-
}`;
|
|
1397
|
-
break;
|
|
1398
|
-
|
|
1399
2431
|
case 'exploration':
|
|
1400
2432
|
typeSpecificUI = `
|
|
1401
2433
|
showMinimap(playerPos, items) {
|
|
@@ -1458,57 +2490,6 @@ function generateUI(type) {
|
|
|
1458
2490
|
}`;
|
|
1459
2491
|
break;
|
|
1460
2492
|
|
|
1461
|
-
case 'narrative':
|
|
1462
|
-
typeSpecificUI = `
|
|
1463
|
-
showDialogue(speaker, text, choices, onChoice) {
|
|
1464
|
-
this.clearOverlay();
|
|
1465
|
-
|
|
1466
|
-
const panel = this.createElement('div', {
|
|
1467
|
-
position: 'absolute', bottom: '0', left: '0', width: '100%',
|
|
1468
|
-
background: 'rgba(48,32,23,0.95)', padding: '20px 24px',
|
|
1469
|
-
borderTop: '3px solid #B07C00', color: '#E4DCCB',
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
if (speaker) {
|
|
1473
|
-
const nameTag = this.createElement('span', {
|
|
1474
|
-
color: '#FFD700', fontWeight: 'bold', fontSize: '16px',
|
|
1475
|
-
marginBottom: '8px', display: 'block',
|
|
1476
|
-
});
|
|
1477
|
-
nameTag.textContent = speaker;
|
|
1478
|
-
panel.appendChild(nameTag);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
const textEl = this.createElement('p', {
|
|
1482
|
-
fontSize: '15px', lineHeight: '1.5', marginBottom: choices ? '16px' : '0',
|
|
1483
|
-
});
|
|
1484
|
-
textEl.textContent = text;
|
|
1485
|
-
panel.appendChild(textEl);
|
|
1486
|
-
|
|
1487
|
-
if (choices && choices.length > 0) {
|
|
1488
|
-
choices.forEach((choice, index) => {
|
|
1489
|
-
const btn = this.createElement('button', {
|
|
1490
|
-
display: 'block', width: '100%', padding: '10px',
|
|
1491
|
-
margin: '6px 0', background: 'transparent', color: '#FFD700',
|
|
1492
|
-
border: '1px solid #B07C00', borderRadius: '8px',
|
|
1493
|
-
fontSize: '14px', cursor: 'pointer', textAlign: 'left',
|
|
1494
|
-
});
|
|
1495
|
-
btn.textContent = \`> \${choice}\`;
|
|
1496
|
-
btn.addEventListener('click', () => onChoice(index));
|
|
1497
|
-
panel.appendChild(btn);
|
|
1498
|
-
});
|
|
1499
|
-
} else {
|
|
1500
|
-
const hint = this.createElement('p', {
|
|
1501
|
-
fontSize: '12px', color: '#799C9F', marginTop: '8px', textAlign: 'center',
|
|
1502
|
-
});
|
|
1503
|
-
hint.textContent = 'Click to continue...';
|
|
1504
|
-
panel.appendChild(hint);
|
|
1505
|
-
panel.addEventListener('click', () => onChoice(-1));
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
this.overlay.appendChild(panel);
|
|
1509
|
-
}`;
|
|
1510
|
-
break;
|
|
1511
|
-
|
|
1512
2493
|
default:
|
|
1513
2494
|
typeSpecificUI = '';
|
|
1514
2495
|
}
|
|
@@ -1679,89 +2660,6 @@ export class GameLogic {
|
|
|
1679
2660
|
`;
|
|
1680
2661
|
}
|
|
1681
2662
|
|
|
1682
|
-
function generateQuizGameLogic() {
|
|
1683
|
-
return `// Quiz Game Logic
|
|
1684
|
-
// Manages questions, answers, scoring, and progress tracking.
|
|
1685
|
-
|
|
1686
|
-
export class GameLogic {
|
|
1687
|
-
constructor(game) {
|
|
1688
|
-
this.game = game;
|
|
1689
|
-
this.score = 0;
|
|
1690
|
-
this.currentQuestion = 0;
|
|
1691
|
-
|
|
1692
|
-
// Define your quiz questions here
|
|
1693
|
-
this.questions = [
|
|
1694
|
-
{
|
|
1695
|
-
question: 'What is 2 + 2?',
|
|
1696
|
-
choices: ['3', '4', '5', '6'],
|
|
1697
|
-
correct: 1,
|
|
1698
|
-
},
|
|
1699
|
-
{
|
|
1700
|
-
question: 'What is the capital of France?',
|
|
1701
|
-
choices: ['London', 'Berlin', 'Paris', 'Madrid'],
|
|
1702
|
-
correct: 2,
|
|
1703
|
-
},
|
|
1704
|
-
{
|
|
1705
|
-
question: 'Which planet is closest to the Sun?',
|
|
1706
|
-
choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
|
|
1707
|
-
correct: 1,
|
|
1708
|
-
},
|
|
1709
|
-
];
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
start() {
|
|
1713
|
-
this.score = 0;
|
|
1714
|
-
this.currentQuestion = 0;
|
|
1715
|
-
this.showCurrentQuestion();
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
reset() {
|
|
1719
|
-
this.score = 0;
|
|
1720
|
-
this.currentQuestion = 0;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
showCurrentQuestion() {
|
|
1724
|
-
if (this.currentQuestion >= this.questions.length) {
|
|
1725
|
-
// Quiz complete
|
|
1726
|
-
this.game.endGame({
|
|
1727
|
-
won: this.score > this.questions.length / 2,
|
|
1728
|
-
score: this.score,
|
|
1729
|
-
});
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
const q = this.questions[this.currentQuestion];
|
|
1734
|
-
this.game.ui.showQuestion(q.question, q.choices, (answerIndex) => {
|
|
1735
|
-
this.handleAnswer(answerIndex);
|
|
1736
|
-
});
|
|
1737
|
-
this.game.ui.showScore(this.score, this.questions.length);
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
handleAnswer(answerIndex) {
|
|
1741
|
-
const q = this.questions[this.currentQuestion];
|
|
1742
|
-
|
|
1743
|
-
if (answerIndex === q.correct) {
|
|
1744
|
-
this.score++;
|
|
1745
|
-
// Play celebration animation - rotate the sample object faster
|
|
1746
|
-
if (this.game.scene.sampleObject) {
|
|
1747
|
-
this.game.scene.sampleObject.scale.set(1.5, 1.5, 1.5);
|
|
1748
|
-
setTimeout(() => {
|
|
1749
|
-
this.game.scene.sampleObject.scale.set(1, 1, 1);
|
|
1750
|
-
}, 500);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
this.currentQuestion++;
|
|
1755
|
-
this.showCurrentQuestion();
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
update(delta) {
|
|
1759
|
-
// Quiz games are event-driven, so update is minimal
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
`;
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
2663
|
function generateExplorationGameLogic() {
|
|
1766
2664
|
return `// Exploration Game Logic
|
|
1767
2665
|
// Manages character movement, collision detection, and collectible items.
|
|
@@ -1892,107 +2790,3 @@ export class GameLogic {
|
|
|
1892
2790
|
}
|
|
1893
2791
|
`;
|
|
1894
2792
|
}
|
|
1895
|
-
|
|
1896
|
-
function generateNarrativeGameLogic() {
|
|
1897
|
-
return `// Narrative Game Logic
|
|
1898
|
-
// Manages dialogue, choices, scene transitions, and story branching.
|
|
1899
|
-
|
|
1900
|
-
export class GameLogic {
|
|
1901
|
-
constructor(game) {
|
|
1902
|
-
this.game = game;
|
|
1903
|
-
this.currentSceneIndex = 0;
|
|
1904
|
-
this.score = 0;
|
|
1905
|
-
|
|
1906
|
-
// Define your story scenes here.
|
|
1907
|
-
// Each scene has speaker, text, and optional choices.
|
|
1908
|
-
// Choices have 'text' and 'next' (index of the next scene to go to).
|
|
1909
|
-
this.storyScenes = [
|
|
1910
|
-
{
|
|
1911
|
-
speaker: 'Narrator',
|
|
1912
|
-
text: 'You find yourself at the entrance to a mysterious village. The golden gates shine in the sunlight.',
|
|
1913
|
-
choices: [
|
|
1914
|
-
{ text: 'Enter the village', next: 1 },
|
|
1915
|
-
{ text: 'Look around first', next: 2 },
|
|
1916
|
-
],
|
|
1917
|
-
},
|
|
1918
|
-
{
|
|
1919
|
-
speaker: 'Village Elder',
|
|
1920
|
-
text: 'Welcome, young traveler! We have been expecting you. Our village needs your help.',
|
|
1921
|
-
choices: [
|
|
1922
|
-
{ text: 'How can I help?', next: 3 },
|
|
1923
|
-
{ text: 'What happened here?', next: 3 },
|
|
1924
|
-
],
|
|
1925
|
-
},
|
|
1926
|
-
{
|
|
1927
|
-
speaker: 'Narrator',
|
|
1928
|
-
text: 'You notice a weathered signpost and colorful flowers lining a cobblestone path. The village looks peaceful but there is an air of anticipation.',
|
|
1929
|
-
choices: [
|
|
1930
|
-
{ text: 'Follow the path inside', next: 1 },
|
|
1931
|
-
],
|
|
1932
|
-
},
|
|
1933
|
-
{
|
|
1934
|
-
speaker: 'Village Elder',
|
|
1935
|
-
text: 'A great challenge lies ahead. Only someone with courage and wisdom can solve it. Will you accept the quest?',
|
|
1936
|
-
choices: [
|
|
1937
|
-
{ text: 'I accept!', next: 4 },
|
|
1938
|
-
{ text: 'Tell me more about the quest', next: 4 },
|
|
1939
|
-
],
|
|
1940
|
-
},
|
|
1941
|
-
{
|
|
1942
|
-
speaker: 'Narrator',
|
|
1943
|
-
text: 'And so your adventure begins... This is where your story continues! Edit the storyScenes array in GameLogic.js to create your own narrative.',
|
|
1944
|
-
choices: null, // null choices = end of story
|
|
1945
|
-
},
|
|
1946
|
-
];
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
start() {
|
|
1950
|
-
this.currentSceneIndex = 0;
|
|
1951
|
-
this.score = 0;
|
|
1952
|
-
this.showCurrentScene();
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
reset() {
|
|
1956
|
-
this.currentSceneIndex = 0;
|
|
1957
|
-
this.score = 0;
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
showCurrentScene() {
|
|
1961
|
-
const scene = this.storyScenes[this.currentSceneIndex];
|
|
1962
|
-
if (!scene) {
|
|
1963
|
-
this.game.endGame({ won: true, score: this.score });
|
|
1964
|
-
return;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
const choiceTexts = scene.choices ? scene.choices.map((c) => c.text) : null;
|
|
1968
|
-
|
|
1969
|
-
this.game.ui.showDialogue(scene.speaker, scene.text, choiceTexts, (choiceIndex) => {
|
|
1970
|
-
this.handleChoice(choiceIndex);
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
handleChoice(choiceIndex) {
|
|
1975
|
-
const scene = this.storyScenes[this.currentSceneIndex];
|
|
1976
|
-
|
|
1977
|
-
if (!scene.choices || choiceIndex === -1) {
|
|
1978
|
-
// End of story
|
|
1979
|
-
this.game.endGame({ won: true, score: this.score });
|
|
1980
|
-
return;
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
const choice = scene.choices[choiceIndex];
|
|
1984
|
-
if (choice && choice.next !== undefined) {
|
|
1985
|
-
this.currentSceneIndex = choice.next;
|
|
1986
|
-
this.score++;
|
|
1987
|
-
this.showCurrentScene();
|
|
1988
|
-
} else {
|
|
1989
|
-
this.game.endGame({ won: true, score: this.score });
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
update(delta) {
|
|
1994
|
-
// Narrative games are event-driven via UI choices
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
`;
|
|
1998
|
-
}
|