@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 +1 -1
- package/src/utils/templates.js +483 -0
package/package.json
CHANGED
package/src/utils/templates.js
CHANGED
|
@@ -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) {
|