@auraindustry/aurajs 0.1.1 → 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.
- package/package.json +1 -1
- package/src/asset-pack.mjs +2 -1
- package/src/authored-runtime.mjs +14 -0
- package/src/bin-integrity.mjs +33 -26
- package/src/conformance/cases/systems-and-gameplay-cases.mjs +861 -6
- package/src/external-package-surface.mjs +1 -1
- package/src/package-integrity.mjs +18 -4
- package/src/publish-command.mjs +133 -13
- package/src/publish-validation.mjs +22 -11
- package/src/scaffold/project-docs.mjs +58 -40
- package/templates/create/2d/src/runtime/app.js +4 -0
- package/templates/create/2d-survivor/src/runtime/app.js +4 -0
- package/templates/create/3d/src/runtime/app.js +4 -0
- package/templates/create/3d-collectathon/src/runtime/app.js +4 -0
- package/templates/create/blank/assets/splash/aurajs-gg-wordmark.webp +0 -0
- package/templates/create/blank/assets/splash/bg.webp +0 -0
- package/templates/create/blank/assets/splash/boot-loop.wav +0 -0
- package/templates/create/blank/assets/splash/boot-sting.wav +0 -0
- package/templates/create/blank/assets/splash/logo-mascot-sheet.webp +0 -0
- package/templates/create/blank/assets/splash/logoholo.webp +0 -0
- package/templates/create/blank/src/main.js +5 -1
- package/templates/create/blank/src/runtime/splash.js +305 -0
- package/templates/create/local-multiplayer/scenes/gameplay.scene.js +186 -12
- package/templates/create/local-multiplayer/src/runtime/capabilities.js +8 -1
- package/templates/create/shared/assets/splash/aurajs-gg-wordmark.webp +0 -0
- package/templates/create/shared/assets/splash/bg.webp +0 -0
- package/templates/create/shared/assets/splash/boot-loop.wav +0 -0
- package/templates/create/shared/assets/splash/boot-sting.wav +0 -0
- package/templates/create/shared/assets/splash/logo-mascot-sheet.webp +0 -0
- package/templates/create/shared/assets/splash/logoholo.webp +0 -0
- package/templates/create/shared/src/runtime/splash.js +305 -0
- package/templates/create/video-cutscene/src/runtime/app.js +4 -0
- package/templates/create-bin/play.js +114 -2
- package/templates/starter/assets/splash/aurajs-gg-wordmark.webp +0 -0
- package/templates/starter/assets/splash/bg.webp +0 -0
- package/templates/starter/assets/splash/boot-loop.wav +0 -0
- package/templates/starter/assets/splash/boot-sting.wav +0 -0
- package/templates/starter/assets/splash/logo-mascot-sheet.webp +0 -0
- package/templates/starter/assets/splash/logoholo.webp +0 -0
- package/templates/starter/src/main.js +4 -0
- package/templates/starter/src/runtime/splash.js +305 -0
|
@@ -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
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// {{PROJECT_TITLE}} — AuraJS blank starter
|
|
2
|
+
import { initSplash, updateSplash, drawSplash, isSplashActive } from './runtime/splash.js';
|
|
2
3
|
|
|
3
4
|
function hasMethod(obj, method) {
|
|
4
5
|
return Boolean(obj) && typeof obj[method] === 'function';
|
|
@@ -16,13 +17,16 @@ function assertRuntimeCapabilities() {
|
|
|
16
17
|
|
|
17
18
|
aura.setup = function () {
|
|
18
19
|
assertRuntimeCapabilities();
|
|
20
|
+
initSplash();
|
|
19
21
|
console.log('{{PROJECT_TITLE}} ready');
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
aura.update = function (
|
|
24
|
+
aura.update = function (dt) {
|
|
25
|
+
if (isSplashActive()) { updateSplash(dt); return; }
|
|
23
26
|
// game logic
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
aura.draw = function () {
|
|
30
|
+
if (isSplashActive()) { drawSplash(); return; }
|
|
27
31
|
aura.draw2d.clear(aura.rgba(0.06, 0.06, 0.08, 1.0));
|
|
28
32
|
};
|
|
@@ -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
|
+
}
|
|
@@ -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, '
|
|
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:
|
|
308
|
-
transportStatus:
|
|
309
|
-
joinPath:
|
|
310
|
-
lastReasonCode:
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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 =
|
|
617
|
+
const allState = resolveDrawStates(sceneState);
|
|
444
618
|
for (const key of Object.keys(allState).sort()) {
|
|
445
619
|
drawLocalPlayerState(allState[key], key);
|
|
446
620
|
}
|