@auraindustry/aurajs 0.1.0 → 0.1.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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/src/asset-pack.mjs +5 -1
  3. package/src/authored-runtime.mjs +14 -0
  4. package/src/bin-integrity.mjs +33 -26
  5. package/src/cli.mjs +17 -2
  6. package/src/commands/project-authoring.mjs +20 -0
  7. package/src/config.mjs +17 -0
  8. package/src/conformance/cases/systems-and-gameplay-cases.mjs +861 -6
  9. package/src/external-package-surface.mjs +1 -1
  10. package/src/package-integrity.mjs +18 -4
  11. package/src/publish-command.mjs +133 -13
  12. package/src/publish-validation.mjs +22 -11
  13. package/src/scaffold/project-docs.mjs +60 -41
  14. package/src/web-conformance.mjs +4 -4
  15. package/templates/create/2d/src/runtime/app.js +4 -0
  16. package/templates/create/2d-survivor/src/runtime/app.js +4 -0
  17. package/templates/create/3d/src/runtime/app.js +4 -0
  18. package/templates/create/3d-collectathon/src/runtime/app.js +4 -0
  19. package/templates/create/blank/assets/splash/aurajs-gg-wordmark.webp +0 -0
  20. package/templates/create/blank/assets/splash/bg.webp +0 -0
  21. package/templates/create/blank/assets/splash/boot-loop.wav +0 -0
  22. package/templates/create/blank/assets/splash/boot-sting.wav +0 -0
  23. package/templates/create/blank/assets/splash/logo-mascot-sheet.webp +0 -0
  24. package/templates/create/blank/assets/splash/logoholo.webp +0 -0
  25. package/templates/create/blank/src/main.js +5 -1
  26. package/templates/create/blank/src/runtime/splash.js +305 -0
  27. package/templates/create/local-multiplayer/aura.config.json +1 -0
  28. package/templates/create/local-multiplayer/docs/design/loop.md +3 -1
  29. package/templates/create/local-multiplayer/scenes/gameplay.scene.js +216 -13
  30. package/templates/create/local-multiplayer/src/runtime/capabilities.js +8 -1
  31. package/templates/create/local-multiplayer/ui/hud.screen.js +12 -7
  32. package/templates/create/shared/assets/splash/aurajs-gg-wordmark.webp +0 -0
  33. package/templates/create/shared/assets/splash/bg.webp +0 -0
  34. package/templates/create/shared/assets/splash/boot-loop.wav +0 -0
  35. package/templates/create/shared/assets/splash/boot-sting.wav +0 -0
  36. package/templates/create/shared/assets/splash/logo-mascot-sheet.webp +0 -0
  37. package/templates/create/shared/assets/splash/logoholo.webp +0 -0
  38. package/templates/create/shared/src/runtime/splash.js +305 -0
  39. package/templates/create/video-cutscene/src/runtime/app.js +4 -0
  40. package/templates/create-bin/play.js +121 -4
  41. package/templates/starter/assets/splash/aurajs-gg-wordmark.webp +0 -0
  42. package/templates/starter/assets/splash/bg.webp +0 -0
  43. package/templates/starter/assets/splash/boot-loop.wav +0 -0
  44. package/templates/starter/assets/splash/boot-sting.wav +0 -0
  45. package/templates/starter/assets/splash/logo-mascot-sheet.webp +0 -0
  46. package/templates/starter/assets/splash/logoholo.webp +0 -0
  47. package/templates/starter/src/main.js +4 -0
  48. package/templates/starter/src/runtime/splash.js +305 -0
@@ -52,6 +52,11 @@ function readAuraEnvBoolean(name) {
52
52
  return null;
53
53
  }
54
54
 
55
+ function normalizeLauncherBaseUrl(value) {
56
+ const normalized = String(value || '').trim().replace(/\/+$/, '');
57
+ return normalized || null;
58
+ }
59
+
55
60
  function isKnownConnectivityMode(value) {
56
61
  const normalized = String(value || '').trim().toLowerCase();
57
62
  return normalized === 'local'
@@ -134,9 +139,16 @@ function resolveDiagnosticsConfig() {
134
139
  };
135
140
  }
136
141
 
142
+ function resolveLauncherConfig() {
143
+ return {
144
+ baseUrl: normalizeLauncherBaseUrl(readAuraEnv('AURA_MULTIPLAYER_LAUNCHER_BASE_URL')),
145
+ };
146
+ }
147
+
137
148
  const ROOM_CONFIG = resolveRoomConfig();
138
149
  const CONNECTIVITY_CONFIG = resolveConnectivityConfig();
139
150
  const DIAGNOSTICS_CONFIG = resolveDiagnosticsConfig();
151
+ const LAUNCHER_CONFIG = resolveLauncherConfig();
140
152
  const LAUNCH_JOIN_CODE = (() => {
141
153
  const value = readAuraEnv('AURA_MULTIPLAYER_JOIN_CODE');
142
154
  return value ? value.toUpperCase() : null;
@@ -166,13 +178,20 @@ function assertLocalMultiplayerCapabilities() {
166
178
  if (!hasMethod(aura.draw2d, 'text')) missing.push('aura.draw2d.text');
167
179
  if (typeof aura.rgb !== 'function') missing.push('aura.rgb');
168
180
  if (!hasMethod(aura.multiplayer, 'configure')) missing.push('aura.multiplayer.configure');
181
+ if (!hasMethod(aura.multiplayer, 'configureRollbackLane')) missing.push('aura.multiplayer.configureRollbackLane');
182
+ if (!hasMethod(aura.multiplayer, 'clearRollbackLane')) missing.push('aura.multiplayer.clearRollbackLane');
169
183
  if (!hasMethod(aura.multiplayer, 'getAllPlayerInputs')) missing.push('aura.multiplayer.getAllPlayerInputs');
170
- if (!hasMethod(aura.multiplayer, 'getAllState')) missing.push('aura.multiplayer.getAllState');
184
+ if (!hasMethod(aura.multiplayer, 'getInterpolatedAllState')) missing.push('aura.multiplayer.getInterpolatedAllState');
185
+ if (!hasMethod(aura.multiplayer, 'getLocalId')) missing.push('aura.multiplayer.getLocalId');
186
+ if (!hasMethod(aura.multiplayer, 'getNetworkDiagnostics')) missing.push('aura.multiplayer.getNetworkDiagnostics');
171
187
  if (!hasMethod(aura.multiplayer, 'getPlayerCount')) missing.push('aura.multiplayer.getPlayerCount');
188
+ if (!hasMethod(aura.multiplayer, 'getRollbackDiagnostics')) missing.push('aura.multiplayer.getRollbackDiagnostics');
189
+ if (!hasMethod(aura.multiplayer, 'getRollbackState')) missing.push('aura.multiplayer.getRollbackState');
172
190
  if (!hasMethod(aura.multiplayer, 'getRoomInfo')) missing.push('aura.multiplayer.getRoomInfo');
173
191
  if (!hasMethod(aura.multiplayer, 'getState')) missing.push('aura.multiplayer.getState');
174
192
  if (!hasMethod(aura.multiplayer, 'host')) missing.push('aura.multiplayer.host');
175
193
  if (!hasMethod(aura.multiplayer, 'isConnected')) missing.push('aura.multiplayer.isConnected');
194
+ if (!hasMethod(aura.multiplayer, 'onStateUpdate')) missing.push('aura.multiplayer.onStateUpdate');
176
195
  if (!hasMethod(aura.multiplayer, 'onPlayerJoin')) missing.push('aura.multiplayer.onPlayerJoin');
177
196
  if (!hasMethod(aura.multiplayer, 'onPlayerLeave')) missing.push('aura.multiplayer.onPlayerLeave');
178
197
  if (!hasMethod(aura.multiplayer, 'sendInput')) missing.push('aura.multiplayer.sendInput');
@@ -207,6 +226,24 @@ function createSceneState() {
207
226
  hostedRoom: null,
208
227
  pendingElapsed: 0,
209
228
  registeredHostCallbacks: false,
229
+ registeredRuntimeCallbacks: false,
230
+ rollbackKey: null,
231
+ stateUpdateMeta: null,
232
+ };
233
+ }
234
+
235
+ function resolveDrawStates(sceneState) {
236
+ const drawState = aura.multiplayer.getInterpolatedAllState() || {};
237
+ if (sceneState.role !== 'client' || !sceneState.rollbackKey) {
238
+ return drawState;
239
+ }
240
+ const rollbackState = aura.multiplayer.getRollbackState?.(sceneState.rollbackKey);
241
+ if (!rollbackState || typeof rollbackState !== 'object') {
242
+ return drawState;
243
+ }
244
+ return {
245
+ ...drawState,
246
+ [sceneState.rollbackKey]: rollbackState,
210
247
  };
211
248
  }
212
249
 
@@ -232,7 +269,57 @@ function readInput() {
232
269
  };
233
270
  }
234
271
 
272
+ function readFiniteNumber(value) {
273
+ const numeric = Number(value);
274
+ return Number.isFinite(numeric) ? numeric : null;
275
+ }
276
+
277
+ function formatDiagnosticNumber(value, suffix = '') {
278
+ const numeric = readFiniteNumber(value);
279
+ return numeric === null ? '-' : `${Math.round(numeric)}${suffix}`;
280
+ }
281
+
282
+ function readLocalPlayerId(fallback = null) {
283
+ const localId = Number(aura.multiplayer.getLocalId?.());
284
+ return Number.isInteger(localId) && localId >= 0 ? localId : fallback;
285
+ }
286
+
287
+ function localPlayerStateKey(playerId) {
288
+ return Number.isInteger(playerId) && playerId >= 0 ? `player_${playerId}` : null;
289
+ }
290
+
291
+ function defaultLocalPlayerLabel(playerId, role = 'client') {
292
+ if (!Number.isInteger(playerId) || playerId < 0) {
293
+ return 'player';
294
+ }
295
+ if (role === 'host' && playerId === 0) {
296
+ return 'host';
297
+ }
298
+ return `p${playerId}`;
299
+ }
300
+
301
+ function createFallbackPlayerState(playerId, role = 'client') {
302
+ return createLocalPlayerState(playerId, defaultLocalPlayerLabel(playerId, role));
303
+ }
304
+
305
+ function normalizeStateUpdateMetadata(metadata) {
306
+ if (!metadata || typeof metadata !== 'object') {
307
+ return null;
308
+ }
309
+ return {
310
+ source: typeof metadata.source === 'string' && metadata.source.trim() ? metadata.source.trim() : null,
311
+ sequence: readFiniteNumber(metadata.sequence),
312
+ serverTimeMs: readFiniteNumber(metadata.serverTimeMs),
313
+ tickIntervalMs: readFiniteNumber(metadata.tickIntervalMs),
314
+ jitterMs: readFiniteNumber(metadata.jitterMs),
315
+ bufferDelayMs: readFiniteNumber(metadata.bufferDelayMs),
316
+ historyDepth: readFiniteNumber(metadata.historyDepth),
317
+ bufferedServerTimeMs: readFiniteNumber(metadata.bufferedServerTimeMs),
318
+ };
319
+ }
320
+
235
321
  function currentStatusLine(sceneState, roomCode) {
322
+ const shareLink = currentLauncherJoinLink(sceneState);
236
323
  if (sceneState.role === 'pending') {
237
324
  if (launchedViaExplicitJoin()) {
238
325
  return `Joining room code ${roomCode}...`;
@@ -240,6 +327,9 @@ function currentStatusLine(sceneState, roomCode) {
240
327
  return 'Checking for a room-code match before hosting this room...';
241
328
  }
242
329
  if (sceneState.role === 'host') {
330
+ if (shareLink) {
331
+ return `Share launcher link: ${displayShareLink(shareLink)}`;
332
+ }
243
333
  if (usesInternetBackedHosting()) {
244
334
  return `Share this room code anywhere: npm run join -- ${roomCode}`;
245
335
  }
@@ -251,6 +341,17 @@ function currentStatusLine(sceneState, roomCode) {
251
341
  return 'Joined through the local-first room-code multiplayer flow.';
252
342
  }
253
343
 
344
+ function currentLauncherJoinLink(sceneState) {
345
+ if (!usesInternetBackedHosting() || !LAUNCHER_CONFIG.baseUrl) {
346
+ return null;
347
+ }
348
+ return `${LAUNCHER_CONFIG.baseUrl}/join/${encodeURIComponent(currentRoomCode(sceneState))}`;
349
+ }
350
+
351
+ function displayShareLink(link) {
352
+ return String(link || '').replace(/^https?:\/\//, '');
353
+ }
354
+
254
355
  export function createGameplayScene(context = {}) {
255
356
  const appState = context.appState && typeof context.appState === 'object'
256
357
  ? context.appState
@@ -272,15 +373,39 @@ export function createGameplayScene(context = {}) {
272
373
  function syncHud() {
273
374
  const roomInfo = currentRoomInfo(sceneState);
274
375
  const roomCode = currentRoomCode(sceneState);
376
+ const shareLink = currentLauncherJoinLink(sceneState);
377
+ const network = aura.multiplayer.getNetworkDiagnostics?.() || {};
378
+ const rollback = aura.multiplayer.getRollbackDiagnostics?.(sceneState.rollbackKey || undefined) || {};
379
+ const stateUpdateMeta = sceneState.stateUpdateMeta || {};
380
+ const transportPath = network.transportPath || roomInfo?.transportPath || (usesInternetBackedHosting() ? 'internet_pending' : 'local_room');
381
+ const transportStatus = network.transportStatus || roomInfo?.transportStatus || (aura.multiplayer.isConnected() ? 'connected' : 'waiting');
382
+ const joinPath = roomInfo?.joinPath || (usesInternetBackedHosting() ? 'internet_fallback' : 'local');
383
+ const roomReason = roomInfo?.lastReasonCode || network.lastReasonCode || null;
275
384
  const diagnostics = DIAGNOSTICS_CONFIG.enabled
276
385
  ? {
277
- mode: roomInfo?.requestedMode || CONNECTIVITY_CONFIG.mode,
278
- scope: roomInfo?.scope || (usesInternetBackedHosting() ? 'internet' : 'local'),
279
- transportPath: roomInfo?.transportPath || (usesInternetBackedHosting() ? 'internet_pending' : 'local_room'),
280
- transportStatus: roomInfo?.transportStatus || (aura.multiplayer.isConnected() ? 'connected' : 'waiting'),
281
- joinPath: roomInfo?.joinPath || (usesInternetBackedHosting() ? 'internet_fallback' : 'local'),
282
- lastReasonCode: roomInfo?.lastReasonCode || null,
386
+ mode: `${network.requestedMode || roomInfo?.requestedMode || CONNECTIVITY_CONFIG.mode}${rollback.enabled ? ' / rollback on' : ' / rollback off'}`,
387
+ scope: network.scope || roomInfo?.scope || (usesInternetBackedHosting() ? 'internet' : 'local'),
388
+ transportPath: `${transportPath}${stateUpdateMeta.source ? ` / ${stateUpdateMeta.source}` : ''}`,
389
+ transportStatus: `${transportStatus} / seq ${formatDiagnosticNumber(network.lastSequence ?? stateUpdateMeta.sequence)} / gap ${formatDiagnosticNumber(network.sequenceGapCount)}`,
390
+ joinPath: `${joinPath} / buf ${formatDiagnosticNumber(network.bufferDelayMs ?? stateUpdateMeta.bufferDelayMs, 'ms')} / jit ${formatDiagnosticNumber(network.jitterMs ?? stateUpdateMeta.jitterMs, 'ms')} / q ${formatDiagnosticNumber(network.queuedEventCount)}`,
391
+ lastReasonCode: `${roomReason || rollback.lastReasonCode || '-'} / rb ${formatDiagnosticNumber(rollback.rollbackCount)} rp ${formatDiagnosticNumber(rollback.replayCount)}`,
283
392
  pingMs: Number(aura.multiplayer.getPing?.()),
393
+ source: stateUpdateMeta.source || null,
394
+ sequence: network.lastSequence ?? stateUpdateMeta.sequence,
395
+ serverTimeMs: network.lastSnapshotServerTimeMs ?? stateUpdateMeta.serverTimeMs,
396
+ tickIntervalMs: network.lastSnapshotTickIntervalMs ?? stateUpdateMeta.tickIntervalMs,
397
+ jitterMs: network.jitterMs ?? stateUpdateMeta.jitterMs,
398
+ bufferDelayMs: network.bufferDelayMs ?? stateUpdateMeta.bufferDelayMs,
399
+ historyDepth: network.historyDepth ?? stateUpdateMeta.historyDepth,
400
+ bufferedServerTimeMs: network.bufferedServerTimeMs ?? stateUpdateMeta.bufferedServerTimeMs,
401
+ rollbackEnabled: rollback.enabled === true,
402
+ rollbackKey: rollback.key || sceneState.rollbackKey || null,
403
+ rollbackCount: readFiniteNumber(rollback.rollbackCount),
404
+ replayCount: readFiniteNumber(rollback.replayCount),
405
+ continuityResetCount: readFiniteNumber(rollback.continuityResetCount),
406
+ lastCorrectionMagnitude: readFiniteNumber(rollback.lastCorrectionMagnitude),
407
+ lastAuthoritativeServerTick: readFiniteNumber(rollback.lastAuthoritativeServerTick),
408
+ lastAckInputTick: readFiniteNumber(rollback.lastAckInputTick),
284
409
  }
285
410
  : null;
286
411
  context.setHudScreen?.('hud', {
@@ -290,6 +415,7 @@ export function createGameplayScene(context = {}) {
290
415
  playerCount: Number(aura.multiplayer.getPlayerCount?.() || 0),
291
416
  connected: aura.multiplayer.isConnected() === true,
292
417
  showControlsHint: multiplayerUi.showControlsHint !== false,
418
+ shareLink: shareLink ? displayShareLink(shareLink) : null,
293
419
  statusLine: currentStatusLine(sceneState, roomCode),
294
420
  diagnostics,
295
421
  });
@@ -312,6 +438,68 @@ export function createGameplayScene(context = {}) {
312
438
  });
313
439
  }
314
440
 
441
+ function ensureRuntimeCallbacks() {
442
+ if (sceneState.registeredRuntimeCallbacks) return;
443
+ sceneState.registeredRuntimeCallbacks = true;
444
+ aura.multiplayer.onStateUpdate((_snapshot, metadata) => {
445
+ sceneState.stateUpdateMeta = normalizeStateUpdateMetadata(metadata);
446
+ });
447
+ }
448
+
449
+ function clearClientRollback() {
450
+ if (sceneState.rollbackKey) {
451
+ aura.multiplayer.clearRollbackLane(sceneState.rollbackKey);
452
+ } else if (aura.multiplayer.getRollbackDiagnostics?.()?.enabled) {
453
+ aura.multiplayer.clearRollbackLane();
454
+ }
455
+ sceneState.rollbackKey = null;
456
+ }
457
+
458
+ function syncRoleFromRuntime() {
459
+ const runtimeRole = aura.multiplayer.getRoomInfo?.()?.role;
460
+ if (runtimeRole === 'host' && sceneState.role !== 'host') {
461
+ sceneState.role = 'host';
462
+ ensureHostCallbacks();
463
+ } else if (runtimeRole === 'client' && sceneState.role !== 'client') {
464
+ sceneState.role = 'client';
465
+ }
466
+ }
467
+
468
+ function ensureClientRollback() {
469
+ const connected = aura.multiplayer.isConnected() === true;
470
+ const roomRole = aura.multiplayer.getRoomInfo?.()?.role || sceneState.role;
471
+ if (!connected || roomRole !== 'client') {
472
+ clearClientRollback();
473
+ return null;
474
+ }
475
+ const localId = readLocalPlayerId();
476
+ const rollbackKey = localPlayerStateKey(localId);
477
+ if (!rollbackKey) {
478
+ clearClientRollback();
479
+ return null;
480
+ }
481
+ const diagnostics = aura.multiplayer.getRollbackDiagnostics?.(rollbackKey) || {};
482
+ if (sceneState.rollbackKey !== rollbackKey || diagnostics?.enabled !== true || diagnostics?.key !== rollbackKey) {
483
+ if (sceneState.rollbackKey && sceneState.rollbackKey !== rollbackKey) {
484
+ aura.multiplayer.clearRollbackLane(sceneState.rollbackKey);
485
+ }
486
+ aura.multiplayer.configureRollbackLane({
487
+ key: rollbackKey,
488
+ playerId: localId,
489
+ speed: LOCAL_MULTIPLAYER_CONFIG.playerSpeed,
490
+ historyLimit: 12,
491
+ bounds: {
492
+ minX: 24,
493
+ maxX: LOCAL_MULTIPLAYER_CONFIG.width - LOCAL_MULTIPLAYER_CONFIG.playerWidth - 24,
494
+ minY: 96,
495
+ maxY: LOCAL_MULTIPLAYER_CONFIG.height - LOCAL_MULTIPLAYER_CONFIG.playerHeight - 28,
496
+ },
497
+ });
498
+ sceneState.rollbackKey = rollbackKey;
499
+ }
500
+ return { key: rollbackKey, playerId: localId };
501
+ }
502
+
315
503
  function activateHost() {
316
504
  if (sceneState.role === 'host') return;
317
505
  const hostOptions = {
@@ -332,15 +520,20 @@ export function createGameplayScene(context = {}) {
332
520
  }
333
521
  sceneState.hostedRoom = aura.multiplayer.host(hostOptions);
334
522
  sceneState.role = 'host';
523
+ clearClientRollback();
335
524
  ensureHostCallbacks();
336
- aura.multiplayer.setState('player_0', createLocalPlayerState(0, 'host'));
525
+ const hostPlayerId = readLocalPlayerId(0);
526
+ const hostKey = localPlayerStateKey(hostPlayerId) || 'player_0';
527
+ aura.multiplayer.setState(hostKey, createFallbackPlayerState(hostPlayerId, 'host'));
337
528
  }
338
529
 
339
530
  function resetRun() {
531
+ clearClientRollback();
340
532
  sceneState.role = 'pending';
341
533
  sceneState.hostedRoom = null;
342
534
  sceneState.pendingElapsed = 0;
343
535
  sceneState.registeredHostCallbacks = false;
536
+ sceneState.stateUpdateMeta = null;
344
537
  multiplayerSession.runsStarted += 1;
345
538
  multiplayerUi.showControlsHint = true;
346
539
  syncSessionState();
@@ -356,10 +549,11 @@ export function createGameplayScene(context = {}) {
356
549
  'config/gameplay/local-multiplayer.config.js',
357
550
  'content/gameplay/room-layout.js',
358
551
  ],
359
- summary: 'Room-code multiplayer starter. `npm run dev` hosts, `npm run join -- CODE` joins, `aura.config.json -> multiplayer` owns room/connectivity defaults, and the HUD surfaces live transport diagnostics.',
552
+ summary: 'Room-code multiplayer starter. `npm run dev` hosts, `npm run join -- CODE` joins, `aura.config.json -> multiplayer` owns room/connectivity defaults, and the HUD surfaces live transport plus rollback diagnostics.',
360
553
 
361
554
  setup() {
362
555
  assertLocalMultiplayerCapabilities();
556
+ ensureRuntimeCallbacks();
363
557
  aura.multiplayer.configure({
364
558
  maxPlayers: LOCAL_MULTIPLAYER_CONFIG.maxPlayers,
365
559
  tickRate: LOCAL_MULTIPLAYER_CONFIG.tickRate,
@@ -385,9 +579,14 @@ export function createGameplayScene(context = {}) {
385
579
  }
386
580
  }
387
581
 
582
+ syncRoleFromRuntime();
583
+
388
584
  if (sceneState.role === 'host') {
389
- const hostState = aura.multiplayer.getState('player_0') || createLocalPlayerState(0, 'host');
390
- aura.multiplayer.setState('player_0', applyLocalPlayerMovement(hostState, input, frameDt));
585
+ clearClientRollback();
586
+ const hostPlayerId = readLocalPlayerId(0);
587
+ const hostKey = localPlayerStateKey(hostPlayerId) || 'player_0';
588
+ const hostState = aura.multiplayer.getState(hostKey) || createFallbackPlayerState(hostPlayerId, 'host');
589
+ aura.multiplayer.setState(hostKey, applyLocalPlayerMovement(hostState, input, frameDt));
391
590
 
392
591
  const inputs = aura.multiplayer.getAllPlayerInputs() || {};
393
592
  for (const [playerId, playerInput] of Object.entries(inputs)) {
@@ -397,7 +596,10 @@ export function createGameplayScene(context = {}) {
397
596
  aura.multiplayer.setState(key, applyLocalPlayerMovement(current, playerInput, frameDt));
398
597
  }
399
598
  } else if (sceneState.role === 'client' && aura.multiplayer.isConnected()) {
599
+ ensureClientRollback();
400
600
  aura.multiplayer.sendInput(input);
601
+ } else {
602
+ clearClientRollback();
401
603
  }
402
604
 
403
605
  syncSessionState();
@@ -405,13 +607,14 @@ export function createGameplayScene(context = {}) {
405
607
  },
406
608
 
407
609
  onExit() {
610
+ clearClientRollback();
408
611
  context.clearHudScreen?.();
409
612
  },
410
613
 
411
614
  draw() {
412
615
  aura.draw2d.clear(aura.rgb(0.07, 0.09, 0.13));
413
616
 
414
- const allState = aura.multiplayer.getAllState() || {};
617
+ const allState = resolveDrawStates(sceneState);
415
618
  for (const key of Object.keys(allState).sort()) {
416
619
  drawLocalPlayerState(allState[key], key);
417
620
  }
@@ -434,7 +637,7 @@ export function createGameplayScene(context = {}) {
434
637
  notes: [
435
638
  'Keep the room-code multiplayer dev loop starter-owned here instead of splitting it between src/main.js and layout-generated files.',
436
639
  'Use appState.session for durable room/session summary, appState.ui for HUD presentation state, and keep connection-side details scene-local.',
437
- 'aura.config.json -> multiplayer is the project-level source of truth for room code, room name, diagnostics, and relay defaults.',
640
+ 'aura.config.json -> multiplayer is the project-level source of truth for room code, room name, diagnostics, relay defaults, and optional launcher join pages.',
438
641
  'config/gameplay/local-multiplayer.config.js stays focused on local gameplay tuning like port, player speed, bounds, and fallback timing.',
439
642
  ],
440
643
  };
@@ -10,13 +10,20 @@ export function assertRuntimeCapabilities() {
10
10
  if (!hasMethod(aura.draw2d, 'text')) missing.push('aura.draw2d.text');
11
11
  if (typeof aura.rgb !== 'function') missing.push('aura.rgb');
12
12
  if (!hasMethod(aura.multiplayer, 'configure')) missing.push('aura.multiplayer.configure');
13
+ if (!hasMethod(aura.multiplayer, 'configureRollbackLane')) missing.push('aura.multiplayer.configureRollbackLane');
14
+ if (!hasMethod(aura.multiplayer, 'clearRollbackLane')) missing.push('aura.multiplayer.clearRollbackLane');
13
15
  if (!hasMethod(aura.multiplayer, 'getAllPlayerInputs')) missing.push('aura.multiplayer.getAllPlayerInputs');
14
- if (!hasMethod(aura.multiplayer, 'getAllState')) missing.push('aura.multiplayer.getAllState');
16
+ if (!hasMethod(aura.multiplayer, 'getInterpolatedAllState')) missing.push('aura.multiplayer.getInterpolatedAllState');
17
+ if (!hasMethod(aura.multiplayer, 'getLocalId')) missing.push('aura.multiplayer.getLocalId');
18
+ if (!hasMethod(aura.multiplayer, 'getNetworkDiagnostics')) missing.push('aura.multiplayer.getNetworkDiagnostics');
15
19
  if (!hasMethod(aura.multiplayer, 'getPlayerCount')) missing.push('aura.multiplayer.getPlayerCount');
20
+ if (!hasMethod(aura.multiplayer, 'getRollbackDiagnostics')) missing.push('aura.multiplayer.getRollbackDiagnostics');
21
+ if (!hasMethod(aura.multiplayer, 'getRollbackState')) missing.push('aura.multiplayer.getRollbackState');
16
22
  if (!hasMethod(aura.multiplayer, 'getRoomInfo')) missing.push('aura.multiplayer.getRoomInfo');
17
23
  if (!hasMethod(aura.multiplayer, 'getState')) missing.push('aura.multiplayer.getState');
18
24
  if (!hasMethod(aura.multiplayer, 'host')) missing.push('aura.multiplayer.host');
19
25
  if (!hasMethod(aura.multiplayer, 'isConnected')) missing.push('aura.multiplayer.isConnected');
26
+ if (!hasMethod(aura.multiplayer, 'onStateUpdate')) missing.push('aura.multiplayer.onStateUpdate');
20
27
  if (!hasMethod(aura.multiplayer, 'onPlayerJoin')) missing.push('aura.multiplayer.onPlayerJoin');
21
28
  if (!hasMethod(aura.multiplayer, 'onPlayerLeave')) missing.push('aura.multiplayer.onPlayerLeave');
22
29
  if (!hasMethod(aura.multiplayer, 'sendInput')) missing.push('aura.multiplayer.sendInput');
@@ -23,6 +23,7 @@ export function drawLocalMultiplayerHud({
23
23
  playerCount = 0,
24
24
  connected = false,
25
25
  showControlsHint = true,
26
+ shareLink = null,
26
27
  statusLine = '',
27
28
  diagnostics = null,
28
29
  } = {}) {
@@ -47,13 +48,17 @@ export function drawLocalMultiplayerHud({
47
48
  aura.draw2d.text(`Reason: ${diagnostics.lastReasonCode || '-'}`, LOCAL_MULTIPLAYER_CONFIG.width - 248, 122, 11, aura.rgb(0.82, 0.9, 0.86));
48
49
  }
49
50
 
50
- aura.draw2d.text(
51
- 'Room-code multiplayer. Put room + relay defaults in aura.config.json -> multiplayer.',
52
- 18,
53
- LOCAL_MULTIPLAYER_CONFIG.height - 28,
54
- 10,
55
- aura.rgb(0.76, 0.82, 0.92),
56
- );
51
+ if (shareLink) {
52
+ aura.draw2d.text(`Join link: ${shareLink}`, 18, LOCAL_MULTIPLAYER_CONFIG.height - 28, 10, aura.rgb(0.76, 0.82, 0.92));
53
+ } else {
54
+ aura.draw2d.text(
55
+ 'Room-code multiplayer. Put room + relay defaults in aura.config.json -> multiplayer.',
56
+ 18,
57
+ LOCAL_MULTIPLAYER_CONFIG.height - 28,
58
+ 10,
59
+ aura.rgb(0.76, 0.82, 0.92),
60
+ );
61
+ }
57
62
  aura.draw2d.text(`Connected: ${connected}`, LOCAL_MULTIPLAYER_CONFIG.width - 166, 16, 11, aura.rgb(0.82, 0.9, 0.86));
58
63
  }
59
64