@myvillage/cli 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ function normEngine(v) {
24
24
  function normType(v) {
25
25
  if (!v) return null;
26
26
  v = String(v).toLowerCase();
27
- return ['quiz', 'exploration', 'narrative', 'custom'].includes(v) ? v : null;
27
+ return ['quiz', 'exploration', 'narrative', 'custom', 'arena'].includes(v) ? v : null;
28
28
  }
29
29
  function normAge(v) {
30
30
  if (!v) return null;
@@ -127,6 +127,7 @@ export async function createGameCommand(options = {}) {
127
127
  { name: 'Exploration Game - Navigate and discover', value: 'exploration' },
128
128
  { name: 'Narrative Game - Story-driven experience', value: 'narrative' },
129
129
  { name: 'Custom - Start with base Three.js template', value: 'custom' },
130
+ { name: 'Arena (multiplayer) - Real-time first-person tag / blaster matches', value: 'arena' },
130
131
  ],
131
132
  });
132
133
  if (needName)
@@ -213,6 +214,10 @@ export async function createGameCommand(options = {}) {
213
214
  console.log(` ${brand.gold('myvillage deploy')} ${brand.teal('- Deploy to MyVillageOS')}`);
214
215
  console.log();
215
216
  console.log(brand.teal(` Game type: ${game.type} | Orientation: ${orientation} | Age group: ${game.ageGroup}`));
217
+ if (game.type === 'arena') {
218
+ console.log(brand.teal(' Multiplayer: open the dev server in two browser tabs and press Quick match in both.'));
219
+ console.log(brand.teal(' Match rules + map: src/arena.json (sent with every deploy).'));
220
+ }
216
221
  console.log(brand.teal(` Cover art: drop thumbnail.png / banner.png in ${slug}/assets/ before deploy.`));
217
222
  console.log();
218
223
  } catch (err) {
@@ -22,6 +22,42 @@ function readPackageJson(dir) {
22
22
  }
23
23
  }
24
24
 
25
+ const MAX_MULTIPLAYER_BYTES = 64 * 1024;
26
+
27
+ /**
28
+ * The game's real-time multiplayer config for the deploy metadata, from
29
+ * `myvillage.multiplayer` in package.json: `{ arena: 'src/arena.json' }` (a
30
+ * path) or `{ arena: {...} }` (inline). Returns undefined when the game doesn't
31
+ * declare one (the platform keeps its current setting), null to turn it off.
32
+ * Throws a readable error for a missing or malformed config.
33
+ */
34
+ export function readMultiplayerConfig(dir, mv) {
35
+ const setting = mv?.multiplayer;
36
+ if (setting === undefined) return undefined;
37
+ if (setting === null || setting === false) return null;
38
+ if (typeof setting !== 'object' || Array.isArray(setting)) {
39
+ throw new Error('myvillage.multiplayer must be an object like { "arena": "src/arena.json" }');
40
+ }
41
+ let arena = setting.arena ?? {};
42
+ if (typeof arena === 'string') {
43
+ const file = join(dir, arena);
44
+ if (!existsSync(file)) throw new Error(`myvillage.multiplayer.arena points to ${arena}, which doesn't exist`);
45
+ try {
46
+ arena = JSON.parse(readFileSync(file, 'utf-8'));
47
+ } catch (err) {
48
+ throw new Error(`${arena} is not valid JSON: ${err.message}`);
49
+ }
50
+ }
51
+ if (typeof arena !== 'object' || arena === null || Array.isArray(arena)) {
52
+ throw new Error('myvillage.multiplayer.arena must be a JSON object (or a path to one)');
53
+ }
54
+ const config = { arena };
55
+ if (JSON.stringify(config).length > MAX_MULTIPLAYER_BYTES) {
56
+ throw new Error('Multiplayer config is larger than 64 KB — simplify the map');
57
+ }
58
+ return config;
59
+ }
60
+
25
61
  function readMyVillageJson(dir) {
26
62
  const configPath = join(dir, 'myvillage.json');
27
63
  if (!existsSync(configPath)) return null;
@@ -217,6 +253,17 @@ async function deployThreeJsGame(projectDir, pkg, options = {}) {
217
253
  // If this game is already live, confirm + unpublish before we rebuild/upload.
218
254
  if (!(await confirmRedeployIfPublished(pkg.myvillage.gameId, options))) return;
219
255
 
256
+ // Multiplayer rules + map ship with the deploy; a broken config should fail
257
+ // before anything is built or uploaded.
258
+ let multiplayer;
259
+ try {
260
+ multiplayer = readMultiplayerConfig(projectDir, pkg.myvillage);
261
+ } catch (err) {
262
+ if (json) console.log(JSON.stringify({ ok: false, error: err.message }));
263
+ else console.log(chalk.red(`\n ✗ ${err.message}`));
264
+ return;
265
+ }
266
+
220
267
  // Village visibility is part of the submission — gather it up front (before
221
268
  // the build) so it travels in the deploy metadata.
222
269
  const villageIds = await gatherVillages(pkg.myvillage.villages || [], options);
@@ -263,6 +310,7 @@ async function deployThreeJsGame(projectDir, pkg, options = {}) {
263
310
  orientation: pkg.myvillage.orientation || 'landscape',
264
311
  entryFile: 'index.html',
265
312
  villageIds,
313
+ ...(multiplayer !== undefined && { multiplayer }),
266
314
  };
267
315
  formData.append('metadata', JSON.stringify(metadata));
268
316
 
package/src/index.js CHANGED
@@ -215,7 +215,7 @@ export function run() {
215
215
  .option('--description <text>', 'Game description')
216
216
  .option('--engine <engine>', 'Engine: threejs | unity')
217
217
  .option('--orientation <orientation>', 'portrait | landscape')
218
- .option('--type <type>', 'Three.js type: quiz | exploration | narrative | custom')
218
+ .option('--type <type>', 'Three.js type: quiz | exploration | narrative | custom | arena (multiplayer)')
219
219
  .option('--category <category>', 'Unity category: education | action | adventure | puzzle | other')
220
220
  .option('--target-age <age>', 'Target age: under-5 | 6-12 | 13-and-over')
221
221
  .option('--dir <path>', 'Parent directory to scaffold into (defaults to cwd)')