@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
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import { createSceneRegistry } from './scene-registry.js';
2
2
  import { createProjectInspector } from './project-inspector.js';
3
3
  import { assertRuntimeCapabilities } from './capabilities.js';
4
+ import { initSplash, updateSplash, drawSplash, isSplashActive } from './splash.js';
4
5
 
5
6
  export function createApp() {
6
7
  const sceneRegistry = createSceneRegistry({
@@ -26,13 +27,16 @@ export function createApp() {
26
27
  },
27
28
  setup() {
28
29
  assertRuntimeCapabilities();
30
+ initSplash();
29
31
  activeScene()?.setup?.();
30
32
  },
31
33
  update(dt) {
34
+ if (isSplashActive()) { updateSplash(dt); return; }
32
35
  projectInspector.syncInput(globalThis.aura?.input || null);
33
36
  activeScene()?.update?.(dt);
34
37
  },
35
38
  draw() {
39
+ if (isSplashActive()) { drawSplash(); return; }
36
40
  activeScene()?.draw?.();
37
41
  projectInspector.draw({ activeSceneId });
38
42
  },
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from 'node:child_process';
3
+ import { spawn, spawnSync } from 'node:child_process';
4
4
  import { cpSync, createWriteStream, existsSync, mkdirSync, readFileSync } from 'node:fs';
5
5
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -58,9 +58,50 @@ const ALL_COMMANDS = ['dev', 'join', 'play', 'fork', 'publish', 'session', 'stat
58
58
  const ROOM_CODE_PATTERN = /^[A-Z0-9]{4,8}$/;
59
59
  const FORK_EXCLUDED_TOP_LEVEL = new Set(['.aura', '.git', '.logs', 'build', 'dist', 'node_modules']);
60
60
 
61
+ function resolveAuraMaxxBinary() {
62
+ return process.platform === 'win32' ? 'auramaxx.cmd' : 'auramaxx';
63
+ }
64
+
65
+ function resolveNpmBinary() {
66
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
67
+ }
68
+
69
+ function isAuraMaxxInstalled() {
70
+ const probe = spawnSync(resolveAuraMaxxBinary(), ['--version'], {
71
+ stdio: 'ignore',
72
+ env: process.env,
73
+ });
74
+ return !probe.error && probe.status === 0;
75
+ }
76
+
77
+ async function installAuraMaxxGlobally() {
78
+ return new Promise((resolveInstall) => {
79
+ const child = spawn(
80
+ resolveNpmBinary(),
81
+ ['install', '-g', 'auramaxx', '--foreground-scripts'],
82
+ {
83
+ cwd: process.cwd(),
84
+ stdio: 'inherit',
85
+ env: process.env,
86
+ },
87
+ );
88
+
89
+ child.on('error', () => resolveInstall(false));
90
+ child.on('close', (code) => resolveInstall((code ?? 1) === 0));
91
+ });
92
+ }
93
+
61
94
  function resolveLocalAuraCli(startRoot) {
62
95
  let current = resolve(startRoot);
63
96
  while (true) {
97
+ const monorepoCandidate = resolve(current, 'packages', 'aurascript', 'src', 'cli', 'src', 'cli.mjs');
98
+ if (existsSync(monorepoCandidate)) {
99
+ return monorepoCandidate;
100
+ }
101
+ const packageSourceCandidate = resolve(current, 'src', 'cli', 'src', 'cli.mjs');
102
+ if (existsSync(packageSourceCandidate)) {
103
+ return packageSourceCandidate;
104
+ }
64
105
  const candidate = resolve(current, 'node_modules', '@auraindustry', 'aurajs', 'src', 'cli.mjs');
65
106
  if (existsSync(candidate)) {
66
107
  return candidate;
@@ -495,6 +536,41 @@ function parseArgs(argv) {
495
536
  };
496
537
  }
497
538
 
539
+ function parsePublishArgs(args) {
540
+ const passthroughArgs = [];
541
+ let npmToken = null;
542
+
543
+ for (let index = 0; index < args.length; index += 1) {
544
+ const token = String(args[index] || '');
545
+ if (token === '--token') {
546
+ if ((index + 1) >= args.length) {
547
+ throw createCliError('publish requires a token value after --token.');
548
+ }
549
+ npmToken = String(args[index + 1] || '').trim();
550
+ index += 1;
551
+ continue;
552
+ }
553
+ if (token.startsWith('--token=')) {
554
+ npmToken = token.slice('--token='.length).trim();
555
+ continue;
556
+ }
557
+ passthroughArgs.push(token);
558
+ }
559
+
560
+ if (npmToken !== null && npmToken.length === 0) {
561
+ throw createCliError('publish requires a non-empty token value after --token.');
562
+ }
563
+
564
+ return {
565
+ commandArgs: passthroughArgs,
566
+ env: npmToken
567
+ ? {
568
+ NODE_AUTH_TOKEN: npmToken,
569
+ }
570
+ : null,
571
+ };
572
+ }
573
+
498
574
  function readJsonIfExists(filePath) {
499
575
  if (!existsSync(filePath)) {
500
576
  return null;
@@ -604,6 +680,7 @@ function resolveProjectMultiplayerConfig() {
604
680
  relayHost: null,
605
681
  coordinatorUrl: null,
606
682
  relayUrl: null,
683
+ launcherBaseUrl: null,
607
684
  showDiagnostics: null,
608
685
  chatEnabled: null,
609
686
  chatHistoryLimit: null,
@@ -619,6 +696,7 @@ function resolveProjectMultiplayerConfig() {
619
696
  relayHost: readOptionalCliText(raw.relay || raw.relayHost),
620
697
  coordinatorUrl: readOptionalCliText(raw.coordinatorUrl),
621
698
  relayUrl: readOptionalCliText(raw.relayUrl),
699
+ launcherBaseUrl: readOptionalCliText(raw.launcherBaseUrl || raw.launcherUrl),
622
700
  showDiagnostics: typeof raw.showDiagnostics === 'boolean' ? raw.showDiagnostics : null,
623
701
  chatEnabled: typeof raw.chatEnabled === 'boolean' ? raw.chatEnabled : null,
624
702
  chatHistoryLimit: historyLimit,
@@ -679,6 +757,9 @@ async function resolveLocalMultiplayerCommandEnv() {
679
757
  if (relayUrl) {
680
758
  env.AURA_RELAY_URL = relayUrl;
681
759
  }
760
+ if (projectMultiplayer.launcherBaseUrl) {
761
+ env.AURA_MULTIPLAYER_LAUNCHER_BASE_URL = projectMultiplayer.launcherBaseUrl;
762
+ }
682
763
  if (typeof projectMultiplayer.showDiagnostics === 'boolean') {
683
764
  env.AURA_MULTIPLAYER_SHOW_DIAGNOSTICS = projectMultiplayer.showDiagnostics ? '1' : '0';
684
765
  }
@@ -900,7 +981,7 @@ function assertJoinSupported() {
900
981
  : 'project capability declarations';
901
982
  throw new Error(
902
983
  `join unavailable: multiplayer is disabled in ${sourceDetail}. `
903
- + `Use \`npm run play\` for a normal local launch or \`${packageBin} play\` for a packaged launch.`,
984
+ + `Use \`npm run dev\` for the local authoring loop, \`npm run play\` for a packaged local launch, or \`${packageBin} play\` for an installed package launch.`,
904
985
  );
905
986
  }
906
987
  }
@@ -1015,6 +1096,38 @@ async function chooseDefaultCommand() {
1015
1096
  return choice;
1016
1097
  }
1017
1098
 
1099
+ async function maybeOfferAuraMaxxInstall() {
1100
+ if (isAuraMaxxInstalled()) {
1101
+ return;
1102
+ }
1103
+
1104
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1105
+ return;
1106
+ }
1107
+
1108
+ printSection('AuraMaxx', 'AuraMaxx is not installed globally.');
1109
+ const choice = await promptSelect(
1110
+ ' Install AuraMaxx now?',
1111
+ [
1112
+ { value: 'yes', label: 'Yes, install AuraMaxx' },
1113
+ { value: 'no', label: 'No, continue playing' },
1114
+ ],
1115
+ 'yes',
1116
+ );
1117
+
1118
+ if (choice !== 'yes') {
1119
+ return;
1120
+ }
1121
+
1122
+ printSection('AuraMaxx', 'Installing global CLI...');
1123
+ const installed = await installAuraMaxxGlobally();
1124
+ if (installed) {
1125
+ printSection('AuraMaxx', 'Installed. Continuing play...');
1126
+ return;
1127
+ }
1128
+ printSection('AuraMaxx', 'Install failed. Continuing play...');
1129
+ }
1130
+
1018
1131
  async function main() {
1019
1132
  const parsed = parseArgs(process.argv.slice(2));
1020
1133
 
@@ -1043,7 +1156,8 @@ async function main() {
1043
1156
 
1044
1157
  if (command === 'play') {
1045
1158
  printBanner('PLAY');
1046
- printSection(toDisplayTitle(packageName), 'Starting game + dev session...');
1159
+ printSection(toDisplayTitle(packageName), 'Starting packaged local game session...');
1160
+ await maybeOfferAuraMaxxInstall();
1047
1161
  const externalAssets = await maybePrepareExternalAssets('play');
1048
1162
  const multiplayerEnv = await resolveLocalMultiplayerCommandEnv();
1049
1163
  await runCommand(
@@ -1140,8 +1254,11 @@ async function main() {
1140
1254
  printSection(toDisplayTitle(packageName), 'Publish lifecycle detected, skipping wrapper recursion.');
1141
1255
  return;
1142
1256
  }
1257
+ const publish = parsePublishArgs(commandArgs);
1143
1258
  printSection(toDisplayTitle(packageName), 'Publishing npm package (source + assets)...');
1144
- await runCommand('publish', resolveAuraCliInvocation(['publish', ...commandArgs]));
1259
+ await runCommand('publish', resolveAuraCliInvocation(['publish', ...publish.commandArgs]), {
1260
+ env: publish.env,
1261
+ });
1145
1262
  return;
1146
1263
  }
1147
1264
 
@@ -3,6 +3,7 @@
3
3
  // - modules.physics: enables aura.physics.*
4
4
  // - modules.network: enables aura.net.*
5
5
  // - modules.steam: enables aura.steam.*
6
+ import { initSplash, updateSplash, drawSplash, isSplashActive } from './runtime/splash.js';
6
7
 
7
8
  let x = 400;
8
9
  let y = 300;
@@ -153,6 +154,7 @@ function clearScreen(color) {
153
154
 
154
155
  aura.setup = function () {
155
156
  assertRuntimeCapabilities();
157
+ initSplash();
156
158
  console.log("{{PROJECT_NAME}} started");
157
159
  if (!hasAsset(PLAYER_SPRITE_PATH)) {
158
160
  console.log(`Tip: add assets/${PLAYER_SPRITE_PATH} to preview sprite draw2d options.`);
@@ -163,6 +165,7 @@ aura.setup = function () {
163
165
  };
164
166
 
165
167
  aura.update = function (dt) {
168
+ if (isSplashActive()) { updateSplash(dt); return; }
166
169
  if (keyDown("arrowright")) x += speed * dt;
167
170
  if (keyDown("arrowleft")) x -= speed * dt;
168
171
  if (keyDown("arrowup")) y -= speed * dt;
@@ -186,6 +189,7 @@ aura.update = function (dt) {
186
189
  };
187
190
 
188
191
  aura.draw = function () {
192
+ if (isSplashActive()) { drawSplash(); return; }
189
193
  const bg = rgba(0.08, 0.08, 0.12, 1.0);
190
194
  clearScreen(bg);
191
195