@myvillage/cli 1.63.0 → 1.64.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "1.63.0",
3
+ "version": "1.64.1",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,6 +55,15 @@ const BRAND = {
55
55
 
56
56
  export function createGameProject(targetDir, options) {
57
57
  const { name, description, type, ageGroup, orientation = 'landscape' } = options;
58
+
59
+ // A quiz is a 2D UI experience, not a 3D world — it gets a dedicated DOM/CSS
60
+ // scaffold (no Three.js, no renderer, no game loop) instead of the shared
61
+ // 3D shell the exploration/narrative/custom types use.
62
+ if (type === 'quiz') {
63
+ createQuizProject(targetDir, options);
64
+ return;
65
+ }
66
+
58
67
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
59
68
 
60
69
  // Create directory structure
@@ -110,6 +119,480 @@ export function createGameProject(targetDir, options) {
110
119
  }
111
120
  }
112
121
 
122
+ // ── Quiz Game Project (2D) ─────────────────────────────────────────
123
+ // A quiz is a UI experience, so this scaffold is plain DOM/CSS — no Three.js,
124
+ // no renderer, no game loop. Two files hold everything a developer edits:
125
+ // src/questions.js - the questions + quiz settings (content)
126
+ // src/styles.css - the look
127
+ // src/main.js runs the quiz lifecycle (start → questions → results) and reports
128
+ // the result through the shared MyVillage game contract.
129
+ function createQuizProject(targetDir, options) {
130
+ const { name, description, ageGroup, orientation = 'portrait' } = options;
131
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
132
+
133
+ const dirs = ['', 'src', 'assets', 'public'];
134
+ for (const dir of dirs) {
135
+ mkdirSync(join(targetDir, dir), { recursive: true });
136
+ }
137
+
138
+ writeFileSync(join(targetDir, 'package.json'), generateQuizPackageJson(name, description, slug, ageGroup, orientation));
139
+ writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
140
+ writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
141
+ writeFileSync(join(targetDir, 'index.html'), generateQuizIndexHtml(name, orientation));
142
+ writeFileSync(join(targetDir, 'README.md'), generateQuizReadme(name, description, ageGroup));
143
+
144
+ writeFileSync(join(targetDir, 'src/main.js'), generateQuizApp());
145
+ writeFileSync(join(targetDir, 'src/questions.js'), generateQuizQuestions(name));
146
+ writeFileSync(join(targetDir, 'src/styles.css'), generateQuizStyles());
147
+ writeFileSync(join(targetDir, 'src/myvillage-game.js'), generateMyVillageGameSdk());
148
+
149
+ writeFileSync(join(targetDir, 'assets/README.md'), generateAssetsReadme(orientation));
150
+ copyTemplateArt(targetDir, 'quiz', orientation);
151
+ }
152
+
153
+ function generateQuizPackageJson(name, description, slug, ageGroup, orientation) {
154
+ return JSON.stringify({
155
+ name: slug,
156
+ version: '1.0.0',
157
+ description,
158
+ private: true,
159
+ type: 'module',
160
+ scripts: {
161
+ dev: 'vite',
162
+ build: 'vite build',
163
+ preview: 'vite preview',
164
+ },
165
+ // No Three.js — a quiz is pure DOM/CSS.
166
+ devDependencies: {
167
+ vite: '^5.0.0',
168
+ },
169
+ myvillage: {
170
+ name: name,
171
+ gameId: null,
172
+ gameType: 'quiz',
173
+ targetAge: ageGroup,
174
+ orientation,
175
+ aspectRatio: orientation === 'portrait' ? '9:16' : '16:9',
176
+ assets: {
177
+ thumbnail: 'assets/thumbnail.png',
178
+ banner: 'assets/banner.png',
179
+ },
180
+ villages: [],
181
+ lastDeployed: null,
182
+ },
183
+ }, null, 2) + '\n';
184
+ }
185
+
186
+ function generateQuizIndexHtml(name, orientation) {
187
+ return `<!DOCTYPE html>
188
+ <html lang="en">
189
+ <head>
190
+ <meta charset="UTF-8" />
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
192
+ <!-- orientation: ${orientation} -->
193
+ <title>${name}</title>
194
+ <link rel="stylesheet" href="/src/styles.css" />
195
+ </head>
196
+ <body>
197
+ <div id="app"></div>
198
+ <script type="module" src="/src/main.js"></script>
199
+ </body>
200
+ </html>
201
+ `;
202
+ }
203
+
204
+ function generateQuizQuestions(name) {
205
+ return `// Quiz content — edit this file to make the quiz your own.
206
+ //
207
+ // Each question has the prompt, a list of choices, and \`correct\`: the index of
208
+ // the right choice, starting at 0. Add or remove questions freely.
209
+
210
+ export const quizConfig = {
211
+ // Shown on the start screen.
212
+ title: ${JSON.stringify(name)},
213
+ intro: 'Answer the questions and see how many you get right!',
214
+ // Fraction of questions you must get right to "win" (0.6 = 60%).
215
+ passThreshold: 0.6,
216
+ // Briefly highlight the correct / incorrect answer after each pick.
217
+ showFeedback: true,
218
+ };
219
+
220
+ export const questions = [
221
+ {
222
+ question: 'What is 2 + 2?',
223
+ choices: ['3', '4', '5', '6'],
224
+ correct: 1,
225
+ },
226
+ {
227
+ question: 'What is the capital of France?',
228
+ choices: ['London', 'Berlin', 'Paris', 'Madrid'],
229
+ correct: 2,
230
+ },
231
+ {
232
+ question: 'Which planet is closest to the Sun?',
233
+ choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
234
+ correct: 1,
235
+ },
236
+ ];
237
+ `;
238
+ }
239
+
240
+ function generateQuizApp() {
241
+ return `// Quiz Game — a 2D question/answer game.
242
+ //
243
+ // Edit your questions and settings in src/questions.js, and the look in
244
+ // src/styles.css. This file runs the quiz: it renders the start screen, each
245
+ // question (with a progress bar + score), instant right/wrong feedback, and a
246
+ // results screen — then reports the result to the MyVillage host.
247
+
248
+ import { MyVillageGame } from './myvillage-game.js';
249
+ import { quizConfig, questions } from './questions.js';
250
+
251
+ class QuizGame {
252
+ constructor(root) {
253
+ this.root = root;
254
+ this.index = 0;
255
+ this.score = 0;
256
+ this.answered = false;
257
+
258
+ this.renderStart();
259
+
260
+ // Tell the MyVillage host the game is loaded and interactive.
261
+ MyVillageGame.ready();
262
+ }
263
+
264
+ renderStart() {
265
+ this.root.innerHTML = \`
266
+ <main class="screen">
267
+ <div class="card start-card">
268
+ <h1 class="title">\${quizConfig.title}</h1>
269
+ <p class="intro">\${quizConfig.intro}</p>
270
+ <button class="btn primary" id="start-btn">Start Quiz</button>
271
+ </div>
272
+ </main>
273
+ \`;
274
+ this.root.querySelector('#start-btn').addEventListener('click', () => this.begin());
275
+ }
276
+
277
+ begin() {
278
+ this.index = 0;
279
+ this.score = 0;
280
+ this.renderQuestion();
281
+ }
282
+
283
+ renderQuestion() {
284
+ if (this.index >= questions.length) {
285
+ this.renderResults();
286
+ return;
287
+ }
288
+
289
+ this.answered = false;
290
+ const q = questions[this.index];
291
+ const total = questions.length;
292
+ const progress = (this.index / total) * 100;
293
+
294
+ this.root.innerHTML = \`
295
+ <main class="screen">
296
+ <header class="quiz-header">
297
+ <div class="progress"><div class="progress-fill" style="width: \${progress}%"></div></div>
298
+ <div class="meta">
299
+ <span>Question \${this.index + 1} of \${total}</span>
300
+ <span>Score: \${this.score}</span>
301
+ </div>
302
+ </header>
303
+ <div class="card question-card">
304
+ <p class="question">\${q.question}</p>
305
+ <div class="choices">
306
+ \${q.choices.map((choice, i) => \`<button class="btn choice" data-index="\${i}">\${choice}</button>\`).join('')}
307
+ </div>
308
+ </div>
309
+ </main>
310
+ \`;
311
+
312
+ this.root.querySelectorAll('.choice').forEach((btn) => {
313
+ btn.addEventListener('click', () => this.answer(Number(btn.dataset.index)));
314
+ });
315
+ }
316
+
317
+ answer(choiceIndex) {
318
+ if (this.answered) return; // ignore extra taps while showing feedback
319
+ this.answered = true;
320
+
321
+ const q = questions[this.index];
322
+ if (choiceIndex === q.correct) this.score++;
323
+
324
+ // Reveal the right answer (and the wrong pick, if any).
325
+ const buttons = this.root.querySelectorAll('.choice');
326
+ buttons.forEach((btn, i) => {
327
+ btn.disabled = true;
328
+ if (quizConfig.showFeedback) {
329
+ if (i === q.correct) btn.classList.add('correct');
330
+ else if (i === choiceIndex) btn.classList.add('incorrect');
331
+ }
332
+ });
333
+
334
+ const delay = quizConfig.showFeedback ? 900 : 150;
335
+ window.setTimeout(() => {
336
+ this.index++;
337
+ this.renderQuestion();
338
+ }, delay);
339
+ }
340
+
341
+ renderResults() {
342
+ const total = questions.length;
343
+ const won = total > 0 && this.score / total >= quizConfig.passThreshold;
344
+ const pct = total > 0 ? Math.round((this.score / total) * 100) : 0;
345
+
346
+ this.root.innerHTML = \`
347
+ <main class="screen">
348
+ <div class="card results-card">
349
+ <h2 class="title">\${won ? 'Great job!' : 'Quiz complete'}</h2>
350
+ <p class="result-score">\${this.score} / \${total}</p>
351
+ <p class="result-pct">\${pct}% correct</p>
352
+ <button class="btn primary" id="again-btn">Play Again</button>
353
+ </div>
354
+ </main>
355
+ \`;
356
+ this.root.querySelector('#again-btn').addEventListener('click', () => this.begin());
357
+
358
+ // REQUIRED: tell the MyVillage host the game is finished. The host records
359
+ // the session, awards this game's MVT reward, and returns the player to the
360
+ // catalog. During \`npm run dev\` there's no host, so this just logs.
361
+ MyVillageGame.complete({ score: this.score, won, custom: { total } });
362
+ }
363
+ }
364
+
365
+ new QuizGame(document.getElementById('app'));
366
+ `;
367
+ }
368
+
369
+ function generateQuizStyles() {
370
+ return `/* Quiz styling — tweak freely. Colors use the MyVillage palette. */
371
+
372
+ :root {
373
+ --gold: #FFD700;
374
+ --primary: #B07C00;
375
+ --cream: #E4DCCB;
376
+ --dark: #302017;
377
+ --deep-green: #043922;
378
+ --green: #228B22;
379
+ --teal: #799C9F;
380
+ --correct: #2e9e4f;
381
+ --incorrect: #c0392b;
382
+ }
383
+
384
+ * { margin: 0; padding: 0; box-sizing: border-box; }
385
+
386
+ html, body {
387
+ height: 100%;
388
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
389
+ }
390
+
391
+ body {
392
+ background: linear-gradient(160deg, var(--deep-green), var(--dark));
393
+ color: var(--cream);
394
+ display: flex;
395
+ -webkit-font-smoothing: antialiased;
396
+ }
397
+
398
+ #app { flex: 1; display: flex; }
399
+
400
+ .screen {
401
+ flex: 1;
402
+ display: flex;
403
+ flex-direction: column;
404
+ justify-content: center;
405
+ align-items: center;
406
+ gap: 16px;
407
+ padding: 24px;
408
+ width: 100%;
409
+ }
410
+
411
+ .card {
412
+ width: 100%;
413
+ max-width: 480px;
414
+ background: rgba(48, 32, 23, 0.92);
415
+ border: 2px solid var(--primary);
416
+ border-radius: 20px;
417
+ padding: 28px 24px;
418
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
419
+ }
420
+
421
+ .start-card, .results-card { text-align: center; }
422
+
423
+ .title {
424
+ color: var(--gold);
425
+ font-size: 30px;
426
+ margin-bottom: 12px;
427
+ }
428
+
429
+ .intro {
430
+ font-size: 16px;
431
+ line-height: 1.5;
432
+ margin-bottom: 24px;
433
+ color: var(--cream);
434
+ }
435
+
436
+ /* Header: progress bar + meta row */
437
+ .quiz-header { width: 100%; max-width: 480px; }
438
+
439
+ .progress {
440
+ height: 8px;
441
+ background: rgba(255, 255, 255, 0.15);
442
+ border-radius: 99px;
443
+ overflow: hidden;
444
+ margin-bottom: 10px;
445
+ }
446
+
447
+ .progress-fill {
448
+ height: 100%;
449
+ background: var(--gold);
450
+ border-radius: 99px;
451
+ transition: width 0.3s ease;
452
+ }
453
+
454
+ .meta {
455
+ display: flex;
456
+ justify-content: space-between;
457
+ font-size: 14px;
458
+ color: var(--teal);
459
+ font-weight: 600;
460
+ }
461
+
462
+ .question {
463
+ font-size: 20px;
464
+ font-weight: 700;
465
+ color: var(--gold);
466
+ margin-bottom: 20px;
467
+ text-align: center;
468
+ line-height: 1.4;
469
+ }
470
+
471
+ .choices { display: flex; flex-direction: column; gap: 12px; }
472
+
473
+ /* Buttons */
474
+ .btn {
475
+ font-family: inherit;
476
+ font-size: 16px;
477
+ font-weight: 600;
478
+ border-radius: 12px;
479
+ border: none;
480
+ cursor: pointer;
481
+ min-height: 52px;
482
+ padding: 14px 18px;
483
+ transition: transform 0.05s ease, background 0.2s ease, border-color 0.2s ease;
484
+ }
485
+
486
+ .btn:active { transform: scale(0.98); }
487
+
488
+ .btn.primary {
489
+ background: var(--primary);
490
+ color: #fff;
491
+ width: 100%;
492
+ font-size: 18px;
493
+ }
494
+
495
+ .btn.primary:hover { background: #c98f0a; }
496
+
497
+ .choice {
498
+ background: var(--deep-green);
499
+ color: var(--cream);
500
+ border: 2px solid var(--green);
501
+ text-align: left;
502
+ width: 100%;
503
+ }
504
+
505
+ .choice:hover:not(:disabled) { border-color: var(--gold); }
506
+ .choice:disabled { cursor: default; opacity: 0.95; }
507
+
508
+ .choice.correct {
509
+ background: var(--correct);
510
+ border-color: var(--correct);
511
+ color: #fff;
512
+ }
513
+
514
+ .choice.incorrect {
515
+ background: var(--incorrect);
516
+ border-color: var(--incorrect);
517
+ color: #fff;
518
+ }
519
+
520
+ .result-score {
521
+ font-size: 48px;
522
+ font-weight: 800;
523
+ color: var(--gold);
524
+ margin: 12px 0 4px;
525
+ }
526
+
527
+ .result-pct {
528
+ font-size: 18px;
529
+ color: var(--teal);
530
+ margin-bottom: 24px;
531
+ }
532
+ `;
533
+ }
534
+
535
+ function generateQuizReadme(name, description, ageGroup) {
536
+ return `# ${name}
537
+
538
+ ${description}
539
+
540
+ - **Game Type**: Quiz
541
+ - **Target Age Group**: ${ageGroup}
542
+ - **Built with**: MyVillageOS CLI
543
+
544
+ A 2D quiz game — no 3D engine. Edit two files to make it yours:
545
+
546
+ - **src/questions.js** — your questions, answers, and quiz settings (title, pass mark, feedback)
547
+ - **src/styles.css** — colors, fonts, and layout
548
+
549
+ ## Getting Started
550
+
551
+ \`\`\`bash
552
+ npm install
553
+ npm run dev # play it locally with live reload
554
+ npm run build # build for production (outputs to dist/)
555
+ \`\`\`
556
+
557
+ ## Editing questions
558
+
559
+ Open \`src/questions.js\`. Each question is an object with the prompt, the list of
560
+ \`choices\`, and \`correct\` — the index of the right choice (0 = the first):
561
+
562
+ \`\`\`js
563
+ {
564
+ question: 'Which planet is closest to the Sun?',
565
+ choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
566
+ correct: 1, // 'Mercury'
567
+ }
568
+ \`\`\`
569
+
570
+ \`quizConfig\` at the top of the file controls the title, intro text, the pass
571
+ threshold (fraction correct needed to "win"), and whether to flash right/wrong
572
+ feedback after each answer.
573
+
574
+ ## The game contract
575
+
576
+ When the quiz finishes it calls \`MyVillageGame.complete({ score, won })\`, so the
577
+ MyVillage host records the session and awards the player this game's MVT reward.
578
+ \`MyVillageGame.ready()\` is called on load. During \`npm run dev\` there's no host
579
+ attached, so these just log to the console — that's expected.
580
+
581
+ ## Deploying
582
+
583
+ Starter cover art ships in \`assets/\` — swap in your own thumbnail.png + banner.png, then:
584
+
585
+ \`\`\`bash
586
+ myvillage deploy
587
+ \`\`\`
588
+
589
+ ## Learn More
590
+
591
+ - [Vite Documentation](https://vitejs.dev/)
592
+ - [MyVillageOS Developer Guide](https://portal.myvillageproject.ai)
593
+ `;
594
+ }
595
+
113
596
  // ── Unity Game Project ─────────────────────────────────────────────
114
597
 
115
598
  export async function createUnityGameProject(targetDir, options) {