@auraindustry/aurajs 0.1.1 → 0.1.5

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 (144) hide show
  1. package/README.md +7 -0
  2. package/benchmarks/perf-thresholds.json +27 -0
  3. package/package.json +6 -1
  4. package/src/ai-guidance.mjs +302 -0
  5. package/src/asset-pack.mjs +2 -1
  6. package/src/authored-project.mjs +498 -2
  7. package/src/authored-runtime.mjs +14 -0
  8. package/src/bin-integrity.mjs +33 -26
  9. package/src/build-contract/capabilities.mjs +87 -1
  10. package/src/build-contract/constants.mjs +1 -0
  11. package/src/build-contract.mjs +2 -0
  12. package/src/bundler.mjs +143 -13
  13. package/src/cli.mjs +681 -13
  14. package/src/commands/packs.mjs +741 -0
  15. package/src/commands/project-authoring.mjs +128 -1
  16. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  17. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  18. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  19. package/src/conformance/cases/systems-and-gameplay-cases.mjs +1126 -10
  20. package/src/conformance-mobile.mjs +166 -0
  21. package/src/conformance.mjs +89 -30
  22. package/src/evidence-bundle.mjs +242 -0
  23. package/src/external-package-surface.mjs +1 -1
  24. package/src/headless-test/runtime-coordinator.mjs +186 -33
  25. package/src/headless-test.mjs +2 -0
  26. package/src/helpers/2d/index.mjs +183 -0
  27. package/src/helpers/index.mjs +26 -0
  28. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  29. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  30. package/src/helpers/starter-utils/animation-2d.js +337 -0
  31. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  32. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  33. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  34. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  35. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  36. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  37. package/src/helpers/starter-utils/core.js +150 -0
  38. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  39. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  40. package/src/helpers/starter-utils/index.js +26 -0
  41. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  42. package/src/helpers/starter-utils/journal-2d.js +267 -0
  43. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  44. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  45. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  46. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  47. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  48. package/src/helpers/starter-utils/triggers.js +662 -0
  49. package/src/helpers/starter-utils/tween-2d.js +615 -0
  50. package/src/helpers/starter-utils/wave-director.js +101 -0
  51. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  52. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  53. package/src/mobile/android/build.mjs +606 -0
  54. package/src/mobile/android/host-artifact.mjs +280 -0
  55. package/src/mobile/ios/build.mjs +1323 -0
  56. package/src/mobile/ios/host-artifact.mjs +819 -0
  57. package/src/mobile/shared/capabilities.mjs +174 -0
  58. package/src/package-integrity.mjs +18 -4
  59. package/src/packs/catalog.mjs +259 -0
  60. package/src/perf-benchmark-runner.mjs +17 -12
  61. package/src/perf-benchmark.mjs +408 -4
  62. package/src/publish-command.mjs +434 -17
  63. package/src/publish-validation.mjs +22 -11
  64. package/src/replay-runtime.mjs +257 -0
  65. package/src/scaffold/config.mjs +2 -0
  66. package/src/scaffold/fs.mjs +8 -1
  67. package/src/scaffold/project-docs.mjs +101 -41
  68. package/src/scaffold.mjs +4 -0
  69. package/src/session-runtime.mjs +4 -3
  70. package/src/web-conformance.mjs +0 -36
  71. package/templates/create/2d/src/runtime/app.js +4 -0
  72. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  73. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  74. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  75. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  76. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  77. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  78. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  79. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  80. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  81. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  82. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  83. package/templates/create/2d-survivor/src/runtime/app.js +4 -0
  84. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  85. package/templates/create/3d/src/runtime/app.js +4 -0
  86. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  87. package/templates/create/3d/src/runtime/materials.js +10 -0
  88. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  89. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  90. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  91. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  92. package/templates/create/3d-collectathon/src/runtime/app.js +4 -0
  93. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  94. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  95. package/templates/create/blank/assets/splash/aurajs-gg-wordmark.webp +0 -0
  96. package/templates/create/blank/assets/splash/bg.webp +0 -0
  97. package/templates/create/blank/assets/splash/boot-loop.wav +0 -0
  98. package/templates/create/blank/assets/splash/boot-sting.wav +0 -0
  99. package/templates/create/blank/assets/splash/logo-mascot-sheet.webp +0 -0
  100. package/templates/create/blank/assets/splash/logoholo.webp +0 -0
  101. package/templates/create/blank/src/main.js +5 -1
  102. package/templates/create/blank/src/runtime/splash.js +305 -0
  103. package/templates/create/local-multiplayer/scenes/gameplay.scene.js +186 -12
  104. package/templates/create/local-multiplayer/src/runtime/capabilities.js +8 -1
  105. package/templates/create/shared/assets/splash/aurajs-gg-wordmark.webp +0 -0
  106. package/templates/create/shared/assets/splash/bg.webp +0 -0
  107. package/templates/create/shared/assets/splash/boot-loop.wav +0 -0
  108. package/templates/create/shared/assets/splash/boot-sting.wav +0 -0
  109. package/templates/create/shared/assets/splash/logo-mascot-sheet.webp +0 -0
  110. package/templates/create/shared/assets/splash/logoholo.webp +0 -0
  111. package/templates/create/shared/src/runtime/splash.js +305 -0
  112. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  113. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  114. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  115. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  116. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  117. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  118. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  119. package/templates/create/shared/src/starter-utils/index.js +15 -1
  120. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  121. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  122. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  123. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  124. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  125. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  126. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  127. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  128. package/templates/create/video-cutscene/src/runtime/app.js +4 -0
  129. package/templates/create-bin/play.js +148 -7
  130. package/templates/skills/auramaxx/SKILL.md +46 -0
  131. package/templates/skills/auramaxx/project-requirements.md +68 -0
  132. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  133. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  134. package/templates/starter/assets/splash/aurajs-gg-wordmark.webp +0 -0
  135. package/templates/starter/assets/splash/bg.webp +0 -0
  136. package/templates/starter/assets/splash/boot-loop.wav +0 -0
  137. package/templates/starter/assets/splash/boot-sting.wav +0 -0
  138. package/templates/starter/assets/splash/logo-mascot-sheet.webp +0 -0
  139. package/templates/starter/assets/splash/logoholo.webp +0 -0
  140. package/templates/starter/src/main.js +4 -0
  141. package/templates/starter/src/runtime/splash.js +305 -0
  142. package/templates/skills/aurajs/SKILL.md +0 -96
  143. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  144. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -178,13 +178,20 @@ function assertLocalMultiplayerCapabilities() {
178
178
  if (!hasMethod(aura.draw2d, 'text')) missing.push('aura.draw2d.text');
179
179
  if (typeof aura.rgb !== 'function') missing.push('aura.rgb');
180
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');
181
183
  if (!hasMethod(aura.multiplayer, 'getAllPlayerInputs')) missing.push('aura.multiplayer.getAllPlayerInputs');
182
- 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');
183
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');
184
190
  if (!hasMethod(aura.multiplayer, 'getRoomInfo')) missing.push('aura.multiplayer.getRoomInfo');
185
191
  if (!hasMethod(aura.multiplayer, 'getState')) missing.push('aura.multiplayer.getState');
186
192
  if (!hasMethod(aura.multiplayer, 'host')) missing.push('aura.multiplayer.host');
187
193
  if (!hasMethod(aura.multiplayer, 'isConnected')) missing.push('aura.multiplayer.isConnected');
194
+ if (!hasMethod(aura.multiplayer, 'onStateUpdate')) missing.push('aura.multiplayer.onStateUpdate');
188
195
  if (!hasMethod(aura.multiplayer, 'onPlayerJoin')) missing.push('aura.multiplayer.onPlayerJoin');
189
196
  if (!hasMethod(aura.multiplayer, 'onPlayerLeave')) missing.push('aura.multiplayer.onPlayerLeave');
190
197
  if (!hasMethod(aura.multiplayer, 'sendInput')) missing.push('aura.multiplayer.sendInput');
@@ -219,6 +226,24 @@ function createSceneState() {
219
226
  hostedRoom: null,
220
227
  pendingElapsed: 0,
221
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,
222
247
  };
223
248
  }
224
249
 
@@ -244,6 +269,55 @@ function readInput() {
244
269
  };
245
270
  }
246
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
+
247
321
  function currentStatusLine(sceneState, roomCode) {
248
322
  const shareLink = currentLauncherJoinLink(sceneState);
249
323
  if (sceneState.role === 'pending') {
@@ -300,15 +374,38 @@ export function createGameplayScene(context = {}) {
300
374
  const roomInfo = currentRoomInfo(sceneState);
301
375
  const roomCode = currentRoomCode(sceneState);
302
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;
303
384
  const diagnostics = DIAGNOSTICS_CONFIG.enabled
304
385
  ? {
305
- mode: roomInfo?.requestedMode || CONNECTIVITY_CONFIG.mode,
306
- scope: roomInfo?.scope || (usesInternetBackedHosting() ? 'internet' : 'local'),
307
- transportPath: roomInfo?.transportPath || (usesInternetBackedHosting() ? 'internet_pending' : 'local_room'),
308
- transportStatus: roomInfo?.transportStatus || (aura.multiplayer.isConnected() ? 'connected' : 'waiting'),
309
- joinPath: roomInfo?.joinPath || (usesInternetBackedHosting() ? 'internet_fallback' : 'local'),
310
- 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)}`,
311
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),
312
409
  }
313
410
  : null;
314
411
  context.setHudScreen?.('hud', {
@@ -341,6 +438,68 @@ export function createGameplayScene(context = {}) {
341
438
  });
342
439
  }
343
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
+
344
503
  function activateHost() {
345
504
  if (sceneState.role === 'host') return;
346
505
  const hostOptions = {
@@ -361,15 +520,20 @@ export function createGameplayScene(context = {}) {
361
520
  }
362
521
  sceneState.hostedRoom = aura.multiplayer.host(hostOptions);
363
522
  sceneState.role = 'host';
523
+ clearClientRollback();
364
524
  ensureHostCallbacks();
365
- 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'));
366
528
  }
367
529
 
368
530
  function resetRun() {
531
+ clearClientRollback();
369
532
  sceneState.role = 'pending';
370
533
  sceneState.hostedRoom = null;
371
534
  sceneState.pendingElapsed = 0;
372
535
  sceneState.registeredHostCallbacks = false;
536
+ sceneState.stateUpdateMeta = null;
373
537
  multiplayerSession.runsStarted += 1;
374
538
  multiplayerUi.showControlsHint = true;
375
539
  syncSessionState();
@@ -385,10 +549,11 @@ export function createGameplayScene(context = {}) {
385
549
  'config/gameplay/local-multiplayer.config.js',
386
550
  'content/gameplay/room-layout.js',
387
551
  ],
388
- 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.',
389
553
 
390
554
  setup() {
391
555
  assertLocalMultiplayerCapabilities();
556
+ ensureRuntimeCallbacks();
392
557
  aura.multiplayer.configure({
393
558
  maxPlayers: LOCAL_MULTIPLAYER_CONFIG.maxPlayers,
394
559
  tickRate: LOCAL_MULTIPLAYER_CONFIG.tickRate,
@@ -414,9 +579,14 @@ export function createGameplayScene(context = {}) {
414
579
  }
415
580
  }
416
581
 
582
+ syncRoleFromRuntime();
583
+
417
584
  if (sceneState.role === 'host') {
418
- const hostState = aura.multiplayer.getState('player_0') || createLocalPlayerState(0, 'host');
419
- 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));
420
590
 
421
591
  const inputs = aura.multiplayer.getAllPlayerInputs() || {};
422
592
  for (const [playerId, playerInput] of Object.entries(inputs)) {
@@ -426,7 +596,10 @@ export function createGameplayScene(context = {}) {
426
596
  aura.multiplayer.setState(key, applyLocalPlayerMovement(current, playerInput, frameDt));
427
597
  }
428
598
  } else if (sceneState.role === 'client' && aura.multiplayer.isConnected()) {
599
+ ensureClientRollback();
429
600
  aura.multiplayer.sendInput(input);
601
+ } else {
602
+ clearClientRollback();
430
603
  }
431
604
 
432
605
  syncSessionState();
@@ -434,13 +607,14 @@ export function createGameplayScene(context = {}) {
434
607
  },
435
608
 
436
609
  onExit() {
610
+ clearClientRollback();
437
611
  context.clearHudScreen?.();
438
612
  },
439
613
 
440
614
  draw() {
441
615
  aura.draw2d.clear(aura.rgb(0.07, 0.09, 0.13));
442
616
 
443
- const allState = aura.multiplayer.getAllState() || {};
617
+ const allState = resolveDrawStates(sceneState);
444
618
  for (const key of Object.keys(allState).sort()) {
445
619
  drawLocalPlayerState(allState[key], key);
446
620
  }
@@ -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');
@@ -0,0 +1,305 @@
1
+ // AuraJS Splash — unified retro paper-card boot screen.
2
+ // Auto-wired by createApp(). Shows once on launch, then hands off to the game.
3
+
4
+ const FADE_IN = 0.7;
5
+ const HOLD = 1.8;
6
+ const FADE_OUT = 0.7;
7
+ const TOTAL = FADE_IN + HOLD + FADE_OUT;
8
+
9
+ const PAPER = [0.89, 0.89, 0.89];
10
+ const PAPER_SHADOW = [0.80, 0.80, 0.80];
11
+ const PAPER_EDGE = [0.41, 0.41, 0.41];
12
+ const INK = [0.1, 0.1, 0.1];
13
+ const INK_MUTED = [0.42, 0.42, 0.42];
14
+ const MASCOT_FRAME_W = 64;
15
+ const MASCOT_FRAME_H = 84;
16
+ const MASCOT_SEQUENCE = Object.freeze([0, 1, 2, 1]);
17
+ const SPLASH_STING_PATH = 'splash/boot-sting.wav';
18
+ const SPLASH_LOOP_PATH = 'splash/boot-loop.wav';
19
+ const SPLASH_BUS = 'splash';
20
+ const QUIET_BUSES = Object.freeze(['default', 'music', 'sfx']);
21
+
22
+ let state = null;
23
+
24
+ function has(obj, method) {
25
+ return Boolean(obj) && typeof obj[method] === 'function';
26
+ }
27
+
28
+ function color(rgb, alpha = 1) {
29
+ return aura.rgba(rgb[0], rgb[1], rgb[2], alpha);
30
+ }
31
+
32
+ function drawShadowText(text, x, y, options = {}) {
33
+ const {
34
+ shadowOffset = 2,
35
+ shadowColor = color(INK_MUTED, 0.3),
36
+ ...rest
37
+ } = options;
38
+ aura.draw2d.text(text, x + shadowOffset, y + shadowOffset, {
39
+ ...rest,
40
+ color: shadowColor,
41
+ });
42
+ aura.draw2d.text(text, x, y, rest);
43
+ }
44
+
45
+ export function initSplash() {
46
+ state = {
47
+ t: 0,
48
+ logo: null,
49
+ mascot: null,
50
+ wordmark: null,
51
+ font: null,
52
+ stingHandle: null,
53
+ loopHandle: null,
54
+ busVolumes: null,
55
+ pausedHandles: [],
56
+ };
57
+ try { if (has(aura.assets, 'load')) state.logo = aura.assets.load('splash/logoholo.webp'); } catch (_) {}
58
+ try { if (has(aura.assets, 'load')) state.mascot = aura.assets.load('splash/logo-mascot-sheet.webp'); } catch (_) {}
59
+ try { if (has(aura.assets, 'load')) state.wordmark = aura.assets.load('splash/aurajs-gg-wordmark.webp'); } catch (_) {}
60
+ try {
61
+ if (has(aura.assets, 'loadBitmapFont')) {
62
+ const result = aura.assets.loadBitmapFont();
63
+ if (result && result.ok && result.font) state.font = result.font;
64
+ }
65
+ } catch (_) {}
66
+ captureBusVolumes();
67
+ applySplashBusIsolation();
68
+ try {
69
+ if (aura.audio && aura.audio.supported !== false && typeof aura.audio.play === 'function') {
70
+ state.loopHandle = aura.audio.play(SPLASH_LOOP_PATH, {
71
+ loop: true,
72
+ volume: 0.22,
73
+ bus: SPLASH_BUS,
74
+ });
75
+ state.stingHandle = aura.audio.play(SPLASH_STING_PATH, {
76
+ loop: false,
77
+ volume: 0.54,
78
+ bus: SPLASH_BUS,
79
+ });
80
+ }
81
+ } catch (_) {}
82
+ }
83
+
84
+ export function isSplashActive() {
85
+ return state !== null;
86
+ }
87
+
88
+ export function updateSplash(dt) {
89
+ if (!state) return;
90
+ syncPausedTracks();
91
+ try {
92
+ if (aura.audio && typeof aura.audio.update === 'function') {
93
+ aura.audio.update(Number(dt) > 0 ? Number(dt) : (1 / 60));
94
+ }
95
+ } catch (_) {}
96
+ state.t += dt;
97
+ if (state.t >= TOTAL) {
98
+ stopSplashAudio();
99
+ state = null;
100
+ }
101
+ }
102
+
103
+ export function drawSplash() {
104
+ if (!state) return;
105
+ const { width: w, height: h } = has(aura.window, 'getSize')
106
+ ? aura.window.getSize()
107
+ : { width: 640, height: 480 };
108
+ const t = state.t;
109
+ const cx = w / 2;
110
+ const cy = h / 2;
111
+
112
+ let a = 1;
113
+ if (t < FADE_IN) a = easeOut(t / FADE_IN);
114
+ else if (t > FADE_IN + HOLD) a = 1 - easeIn((t - FADE_IN - HOLD) / FADE_OUT);
115
+
116
+ aura.draw2d.clear(color(PAPER));
117
+
118
+ const panelW = Math.min(Math.floor(w * 0.48), 580);
119
+ const panelH = Math.min(Math.floor(h * 0.72), 620);
120
+ const panelX = Math.floor(cx - (panelW * 0.5));
121
+ const panelY = Math.floor(cy - (panelH * 0.5));
122
+
123
+ aura.draw2d.rectFill(panelX + 6, panelY + 6, panelW, panelH, color(INK, 0.08 * a));
124
+ aura.draw2d.rectFill(panelX, panelY, panelW, panelH, color(PAPER_EDGE, a));
125
+ aura.draw2d.rectFill(panelX + 6, panelY + 6, panelW - 12, panelH - 12, color(PAPER, a));
126
+ aura.draw2d.rectFill(panelX + 12, panelY + 12, panelW - 24, panelH - 24, color(PAPER, a * 0.96));
127
+
128
+ const floatY = Math.sin(t * 1.4 * Math.PI * 2) * 2;
129
+ const breathe = 0.985 + 0.015 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2));
130
+ const mascotScale = Math.max(1, Math.min(Math.floor(Math.min(w, h) / 260), 2));
131
+ const mascotW = Math.floor(MASCOT_FRAME_W * mascotScale * breathe);
132
+ const mascotH = Math.floor(MASCOT_FRAME_H * mascotScale * breathe);
133
+ const mascotX = Math.floor(cx - (mascotW * 0.5));
134
+ const mascotY = Math.floor(panelY + panelH * 0.40 + floatY);
135
+ const mascotFrame = MASCOT_SEQUENCE[Math.floor(t * 7.5) % MASCOT_SEQUENCE.length] || 0;
136
+
137
+ aura.draw2d.rectFill(
138
+ mascotX + Math.floor(mascotW * 0.18),
139
+ mascotY + mascotH - 8,
140
+ Math.floor(mascotW * 0.64),
141
+ 6,
142
+ color(INK, a * 0.08),
143
+ );
144
+
145
+ if (state.mascot) {
146
+ aura.draw2d.sprite(state.mascot, mascotX, mascotY, {
147
+ width: mascotW,
148
+ height: mascotH,
149
+ frameX: mascotFrame * MASCOT_FRAME_W,
150
+ frameY: 0,
151
+ frameW: MASCOT_FRAME_W,
152
+ frameH: MASCOT_FRAME_H,
153
+ alpha: a,
154
+ });
155
+ } else if (state.logo) {
156
+ const sz = Math.min(Math.floor(panelW * 0.24), Math.floor(h * 0.14)) * breathe;
157
+ aura.draw2d.sprite(state.logo, Math.floor(cx - sz / 2), Math.floor(mascotY + 6), {
158
+ width: sz,
159
+ height: sz,
160
+ alpha: a,
161
+ });
162
+ }
163
+
164
+ const ta = clamp(a * easeOut(clamp((t - 0.2) / (FADE_IN * 0.6))));
165
+ const sa = clamp(a * easeOut(clamp((t - 0.4) / (FADE_IN * 0.6))));
166
+ const scale = Math.min(w, h) / 480;
167
+ const tsz = Math.max(24, Math.round(scale * 28));
168
+ const ssz = Math.max(11, Math.round(scale * 12));
169
+ const wordmarkW = Math.min(Math.floor(panelW * 0.52), 290);
170
+ const wordmarkH = Math.floor(wordmarkW * (768 / 1408));
171
+ const wordmarkX = Math.floor(cx - (wordmarkW * 0.5));
172
+ const wordmarkY = Math.floor(panelY + 36);
173
+ const sY = Math.floor(panelY + panelH - 108);
174
+ const fo = state.font ? { font: state.font } : {};
175
+
176
+ if (state.wordmark) {
177
+ aura.draw2d.sprite(state.wordmark, wordmarkX, wordmarkY, {
178
+ width: wordmarkW,
179
+ height: wordmarkH,
180
+ alpha: ta,
181
+ tint: color(INK, ta),
182
+ });
183
+ } else {
184
+ drawShadowText('AuraJS.gg', cx, wordmarkY + Math.floor(wordmarkH * 0.55), {
185
+ ...fo,
186
+ size: tsz,
187
+ color: color(INK, ta),
188
+ shadowColor: color(INK_MUTED, ta * 0.28),
189
+ align: 'center',
190
+ });
191
+ }
192
+
193
+ drawShadowText('Open-Source. MIT.', cx, sY, {
194
+ ...fo,
195
+ size: ssz,
196
+ color: color(INK_MUTED, sa),
197
+ shadowColor: color(PAPER_EDGE, sa * 0.18),
198
+ shadowOffset: 1,
199
+ align: 'center',
200
+ });
201
+ drawShadowText('Who needs publishers?', cx, sY + Math.max(16, Math.round(ssz * 1.45)), {
202
+ ...fo,
203
+ size: ssz,
204
+ color: color(INK_MUTED, sa),
205
+ shadowColor: color(PAPER_EDGE, sa * 0.18),
206
+ shadowOffset: 1,
207
+ align: 'center',
208
+ });
209
+
210
+ const rw = Math.min(panelW - 140, 240);
211
+ aura.draw2d.rectFill(Math.floor(cx - rw / 2), Math.floor(sY + Math.max(34, ssz * 3.2)), rw, 2, color(PAPER_EDGE, sa * 0.38));
212
+ }
213
+
214
+ function easeOut(t) {
215
+ const u = 1 - clamp(t);
216
+ return 1 - (u * u * u);
217
+ }
218
+
219
+ function easeIn(t) {
220
+ const c = clamp(t);
221
+ return c * c * c;
222
+ }
223
+
224
+ function clamp(v) {
225
+ return v < 0 ? 0 : v > 1 ? 1 : v;
226
+ }
227
+
228
+ function stopSplashAudio() {
229
+ if (!state || !aura.audio || typeof aura.audio.stop !== 'function') return;
230
+ try {
231
+ if (state.stingHandle != null) aura.audio.stop(state.stingHandle);
232
+ } catch (_) {}
233
+ try {
234
+ if (state.loopHandle != null) aura.audio.stop(state.loopHandle);
235
+ } catch (_) {}
236
+ restoreAudioState();
237
+ state.stingHandle = null;
238
+ state.loopHandle = null;
239
+ }
240
+
241
+ function captureBusVolumes() {
242
+ if (!state || !aura.audio || typeof aura.audio.getMixerState !== 'function') return;
243
+ try {
244
+ const mixer = aura.audio.getMixerState();
245
+ const buses = Array.isArray(mixer?.buses) ? mixer.buses : [];
246
+ state.busVolumes = buses.reduce((acc, entry) => {
247
+ const bus = String(entry?.bus || '').trim();
248
+ if (!bus) return acc;
249
+ acc[bus] = Number(entry?.volume);
250
+ return acc;
251
+ }, {});
252
+ } catch (_) {}
253
+ }
254
+
255
+ function applySplashBusIsolation() {
256
+ if (!state || !aura.audio || typeof aura.audio.setBusVolume !== 'function') return;
257
+ try { aura.audio.setBusVolume(SPLASH_BUS, 1); } catch (_) {}
258
+ for (const bus of QUIET_BUSES) {
259
+ try { aura.audio.setBusVolume(bus, 0); } catch (_) {}
260
+ }
261
+ }
262
+
263
+ function syncPausedTracks() {
264
+ if (!state || !aura.audio || typeof aura.audio.getMixerState !== 'function' || typeof aura.audio.pause !== 'function') return;
265
+ try {
266
+ const mixer = aura.audio.getMixerState();
267
+ const tracks = Array.isArray(mixer?.tracks) ? mixer.tracks : [];
268
+ const splashHandles = [state.stingHandle, state.loopHandle].filter((handle) => handle != null);
269
+ const pausedHandles = new Set(Array.isArray(state.pausedHandles) ? state.pausedHandles : []);
270
+ for (const track of tracks) {
271
+ const handle = Number(track?.handle);
272
+ if (!Number.isInteger(handle) || handle <= 0) continue;
273
+ if (splashHandles.includes(handle)) continue;
274
+ if (track?.paused === true || pausedHandles.has(handle)) continue;
275
+ try {
276
+ aura.audio.pause(handle);
277
+ pausedHandles.add(handle);
278
+ } catch (_) {}
279
+ }
280
+ state.pausedHandles = Array.from(pausedHandles);
281
+ } catch (_) {}
282
+ }
283
+
284
+ function restoreAudioState() {
285
+ if (!state || !aura.audio) return;
286
+ if (typeof aura.audio.setBusVolume === 'function') {
287
+ const snapshot = state.busVolumes && typeof state.busVolumes === 'object' ? state.busVolumes : null;
288
+ if (snapshot) {
289
+ for (const [bus, volume] of Object.entries(snapshot)) {
290
+ try { aura.audio.setBusVolume(bus, Number(volume)); } catch (_) {}
291
+ }
292
+ } else {
293
+ for (const bus of QUIET_BUSES) {
294
+ try { aura.audio.setBusVolume(bus, 1); } catch (_) {}
295
+ }
296
+ }
297
+ try { aura.audio.setBusVolume(SPLASH_BUS, 1); } catch (_) {}
298
+ }
299
+ if (typeof aura.audio.resume === 'function') {
300
+ for (const handle of Array.isArray(state.pausedHandles) ? state.pausedHandles : []) {
301
+ try { aura.audio.resume(handle); } catch (_) {}
302
+ }
303
+ }
304
+ state.pausedHandles = [];
305
+ }