aurasu 0.1.0 → 0.1.2
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/README.md +16 -0
- package/aura.capabilities.json +26 -0
- package/aura.config.json +1 -1
- package/bin/play.js +1185 -18
- package/config/gameplay/game.config.js +11 -0
- package/content/gameplay/beatmap.content.js +17 -0
- package/package.json +9 -3
- package/prefabs/playfield.prefab.js +17 -0
- package/scenes/gameplay.scene.js +2255 -0
- package/src/main.js +12 -2224
- package/src/runtime/app-state.js +100 -0
- package/src/runtime/app.js +1060 -0
- package/src/runtime/capabilities.js +14 -0
- package/src/runtime/project-inspector.js +615 -0
- package/src/runtime/project-registry.js +91 -0
- package/src/runtime/scene-flow.js +290 -0
- package/src/runtime/scene-registry.js +25 -0
- package/src/runtime/screen-shell.js +222 -0
- package/src/runtime/ui-settings.js +227 -0
- package/src/runtime/ui-theme.js +297 -0
- package/ui/hud.screen.js +14 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import screenModule1 from "../../ui/hud.screen.js";
|
|
2
|
+
|
|
3
|
+
const projectRegistry = {
|
|
4
|
+
"schema": "aurajs.project-registry.v1",
|
|
5
|
+
"template": "example-aurasu",
|
|
6
|
+
"projectTitle": "Aurasu",
|
|
7
|
+
"startSceneId": "gameplay",
|
|
8
|
+
"scenes": [
|
|
9
|
+
{
|
|
10
|
+
"id": "gameplay",
|
|
11
|
+
"path": "scenes/gameplay.scene.js",
|
|
12
|
+
"exportName": "createGameplayScene",
|
|
13
|
+
"role": null
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"screens": [
|
|
17
|
+
{
|
|
18
|
+
"id": "hud",
|
|
19
|
+
"path": "ui/hud.screen.js",
|
|
20
|
+
"exportName": "default",
|
|
21
|
+
"role": "hud"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"prefabs": [
|
|
25
|
+
{
|
|
26
|
+
"id": "playfield",
|
|
27
|
+
"path": "prefabs/playfield.prefab.js",
|
|
28
|
+
"exportName": "default",
|
|
29
|
+
"role": "world"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"configFiles": [
|
|
33
|
+
"config/gameplay/game.config.js"
|
|
34
|
+
],
|
|
35
|
+
"contentFiles": [
|
|
36
|
+
"content/gameplay/beatmap.content.js"
|
|
37
|
+
],
|
|
38
|
+
"continuity": {
|
|
39
|
+
"schema": "aurajs.project-continuity.v1",
|
|
40
|
+
"startSceneId": "gameplay",
|
|
41
|
+
"sharedAppStatePath": "src/runtime/app-state.js",
|
|
42
|
+
"sceneFlowPath": "src/runtime/scene-flow.js",
|
|
43
|
+
"screenShellPath": "src/runtime/screen-shell.js",
|
|
44
|
+
"projectRegistryPath": "src/runtime/project-registry.js",
|
|
45
|
+
"sceneRegistryPath": "src/runtime/scene-registry.js",
|
|
46
|
+
"sceneStateOwnerPath": "scenes",
|
|
47
|
+
"runtimeFiles": [
|
|
48
|
+
"src/runtime/app-state.js",
|
|
49
|
+
"src/runtime/project-registry.js",
|
|
50
|
+
"src/runtime/scene-flow.js",
|
|
51
|
+
"src/runtime/scene-registry.js",
|
|
52
|
+
"src/runtime/screen-shell.js"
|
|
53
|
+
],
|
|
54
|
+
"saveRoots": {
|
|
55
|
+
"slots": ".aura/state/slots",
|
|
56
|
+
"checkpoints": ".aura/state/checkpoints"
|
|
57
|
+
},
|
|
58
|
+
"devRestore": {
|
|
59
|
+
"flags": [
|
|
60
|
+
"--restore-slot",
|
|
61
|
+
"--restore-checkpoint"
|
|
62
|
+
],
|
|
63
|
+
"note": "Supported native dev restarts reapply slot, checkpoint, or live continuity payloads after setup."
|
|
64
|
+
},
|
|
65
|
+
"notes": [
|
|
66
|
+
"Keep shared continuity in appState.session/ui/runtime through the app-context helpers backed by src/runtime/app-state.js, and keep scene-local restore state JSON-safe inside scene getState/applyState hooks.",
|
|
67
|
+
"Use src/runtime/scene-flow.js for route-style scene payload continuity read through context.getCurrentScenePayload(), and use src/runtime/screen-shell.js for HUD, overlay, and modal payload continuity instead of ad hoc globals."
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const screenModules = {
|
|
73
|
+
"hud": screenModule1,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
Object.defineProperty(projectRegistry, 'dataFiles', {
|
|
77
|
+
enumerable: false,
|
|
78
|
+
get() {
|
|
79
|
+
return [...projectRegistry.configFiles, ...projectRegistry.contentFiles];
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export function resolveProjectScreen(screenId) {
|
|
84
|
+
return screenModules[String(screenId || '')] || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createProjectRegistry() {
|
|
88
|
+
return projectRegistry;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default projectRegistry;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
export const SCENE_FLOW_SCHEMA = 'aurajs.scene-flow.v1';
|
|
2
|
+
|
|
3
|
+
function cloneValue(value) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value.map((entry) => cloneValue(entry));
|
|
6
|
+
}
|
|
7
|
+
if (value && typeof value === 'object') {
|
|
8
|
+
const output = {};
|
|
9
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
10
|
+
output[key] = cloneValue(entry);
|
|
11
|
+
}
|
|
12
|
+
return output;
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeSceneId(sceneId) {
|
|
18
|
+
if (typeof sceneId !== 'string') return null;
|
|
19
|
+
const normalized = sceneId.trim();
|
|
20
|
+
return normalized.length > 0 ? normalized : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function snapshotEntry(entry) {
|
|
24
|
+
if (!entry) return null;
|
|
25
|
+
return {
|
|
26
|
+
sceneId: entry.sceneId,
|
|
27
|
+
data: cloneValue(entry.data),
|
|
28
|
+
mountId: entry.mountId,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function exposeEntry(entry) {
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
return {
|
|
35
|
+
sceneId: entry.sceneId,
|
|
36
|
+
data: cloneValue(entry.data),
|
|
37
|
+
mountId: entry.mountId,
|
|
38
|
+
scene: entry.scene,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resultOk(state, extra = {}) {
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
reasonCode: 'scene_flow_ok',
|
|
46
|
+
...extra,
|
|
47
|
+
state,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resultErr(reasonCode) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
reasonCode,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createSceneFlow({
|
|
59
|
+
resolveScene = null,
|
|
60
|
+
initialSceneId = null,
|
|
61
|
+
initialData = null,
|
|
62
|
+
onEnter = null,
|
|
63
|
+
onExit = null,
|
|
64
|
+
onPause = null,
|
|
65
|
+
onResume = null,
|
|
66
|
+
} = {}) {
|
|
67
|
+
let stack = [];
|
|
68
|
+
let nextMountId = 1;
|
|
69
|
+
let mutationCount = 0;
|
|
70
|
+
|
|
71
|
+
function resolveKnownScene(sceneId) {
|
|
72
|
+
const normalized = normalizeSceneId(sceneId);
|
|
73
|
+
if (!normalized) return { ok: false, reasonCode: 'scene_flow_invalid_scene_id' };
|
|
74
|
+
if (typeof resolveScene !== 'function') {
|
|
75
|
+
return { ok: true, sceneId: normalized, scene: null };
|
|
76
|
+
}
|
|
77
|
+
const scene = resolveScene(normalized);
|
|
78
|
+
if (!scene) return { ok: false, reasonCode: 'scene_flow_unknown_scene' };
|
|
79
|
+
return { ok: true, sceneId: normalized, scene };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createEntry(resolved, data = null) {
|
|
83
|
+
return {
|
|
84
|
+
sceneId: resolved.sceneId,
|
|
85
|
+
scene: resolved.scene,
|
|
86
|
+
data: cloneValue(data),
|
|
87
|
+
mountId: nextMountId++,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildState() {
|
|
92
|
+
const current = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
93
|
+
return {
|
|
94
|
+
schema: SCENE_FLOW_SCHEMA,
|
|
95
|
+
currentSceneId: current ? current.sceneId : null,
|
|
96
|
+
current: snapshotEntry(current),
|
|
97
|
+
stack: stack.map((entry) => snapshotEntry(entry)),
|
|
98
|
+
depth: stack.length,
|
|
99
|
+
canPop: stack.length > 1,
|
|
100
|
+
mutationCount,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function invokeLifecycle(callback, payload) {
|
|
105
|
+
if (typeof callback !== 'function') return;
|
|
106
|
+
callback(payload);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildStateFromEntries(entries, nextMutationCount = mutationCount) {
|
|
110
|
+
const current = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
111
|
+
return {
|
|
112
|
+
schema: SCENE_FLOW_SCHEMA,
|
|
113
|
+
currentSceneId: current ? current.sceneId : null,
|
|
114
|
+
current: snapshotEntry(current),
|
|
115
|
+
stack: entries.map((entry) => snapshotEntry(entry)),
|
|
116
|
+
depth: entries.length,
|
|
117
|
+
canPop: entries.length > 1,
|
|
118
|
+
mutationCount: nextMutationCount,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function restoreEntry(snapshot, fallbackMountId) {
|
|
123
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
124
|
+
return { ok: false, reasonCode: 'scene_flow_invalid_state' };
|
|
125
|
+
}
|
|
126
|
+
const resolved = resolveKnownScene(snapshot.sceneId);
|
|
127
|
+
if (!resolved.ok) {
|
|
128
|
+
return resultErr(resolved.reasonCode);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
entry: {
|
|
133
|
+
sceneId: resolved.sceneId,
|
|
134
|
+
scene: resolved.scene,
|
|
135
|
+
data: cloneValue(snapshot.data),
|
|
136
|
+
mountId: Number.isInteger(Number(snapshot.mountId)) && Number(snapshot.mountId) > 0
|
|
137
|
+
? Number(snapshot.mountId)
|
|
138
|
+
: Number(fallbackMountId),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (initialSceneId != null) {
|
|
144
|
+
const resolved = resolveKnownScene(initialSceneId);
|
|
145
|
+
if (!resolved.ok) {
|
|
146
|
+
throw new Error(`[scene-flow] invalid initial scene [reason:${resolved.reasonCode}]`);
|
|
147
|
+
}
|
|
148
|
+
stack = [createEntry(resolved, initialData)];
|
|
149
|
+
mutationCount = 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
replace(sceneId, data = null) {
|
|
154
|
+
const resolved = resolveKnownScene(sceneId);
|
|
155
|
+
if (!resolved.ok) return resultErr(resolved.reasonCode);
|
|
156
|
+
|
|
157
|
+
const previous = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
158
|
+
const next = createEntry(resolved, data);
|
|
159
|
+
|
|
160
|
+
if (previous) {
|
|
161
|
+
invokeLifecycle(onExit, {
|
|
162
|
+
operation: 'replace',
|
|
163
|
+
current: exposeEntry(previous),
|
|
164
|
+
next: exposeEntry(next),
|
|
165
|
+
result: null,
|
|
166
|
+
});
|
|
167
|
+
stack[stack.length - 1] = next;
|
|
168
|
+
} else {
|
|
169
|
+
stack.push(next);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
mutationCount += 1;
|
|
173
|
+
invokeLifecycle(onEnter, {
|
|
174
|
+
operation: 'replace',
|
|
175
|
+
current: exposeEntry(next),
|
|
176
|
+
previous: exposeEntry(previous),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return resultOk(buildState(), {
|
|
180
|
+
current: exposeEntry(next),
|
|
181
|
+
previous: snapshotEntry(previous),
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
push(sceneId, data = null) {
|
|
185
|
+
const resolved = resolveKnownScene(sceneId);
|
|
186
|
+
if (!resolved.ok) return resultErr(resolved.reasonCode);
|
|
187
|
+
|
|
188
|
+
const previous = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
189
|
+
const next = createEntry(resolved, data);
|
|
190
|
+
|
|
191
|
+
if (previous) {
|
|
192
|
+
invokeLifecycle(onPause, {
|
|
193
|
+
operation: 'push',
|
|
194
|
+
current: exposeEntry(previous),
|
|
195
|
+
next: exposeEntry(next),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
stack.push(next);
|
|
200
|
+
mutationCount += 1;
|
|
201
|
+
invokeLifecycle(onEnter, {
|
|
202
|
+
operation: 'push',
|
|
203
|
+
current: exposeEntry(next),
|
|
204
|
+
previous: exposeEntry(previous),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return resultOk(buildState(), {
|
|
208
|
+
current: exposeEntry(next),
|
|
209
|
+
previous: snapshotEntry(previous),
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
pop(result = null) {
|
|
213
|
+
if (stack.length <= 1) return resultErr('scene_flow_cannot_pop_root');
|
|
214
|
+
|
|
215
|
+
const popped = stack.pop();
|
|
216
|
+
const current = stack[stack.length - 1] || null;
|
|
217
|
+
const safeResult = cloneValue(result);
|
|
218
|
+
|
|
219
|
+
invokeLifecycle(onExit, {
|
|
220
|
+
operation: 'pop',
|
|
221
|
+
current: exposeEntry(popped),
|
|
222
|
+
next: exposeEntry(current),
|
|
223
|
+
result: safeResult,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
mutationCount += 1;
|
|
227
|
+
invokeLifecycle(onResume, {
|
|
228
|
+
operation: 'pop',
|
|
229
|
+
current: exposeEntry(current),
|
|
230
|
+
popped: exposeEntry(popped),
|
|
231
|
+
result: safeResult,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return resultOk(buildState(), {
|
|
235
|
+
current: exposeEntry(current),
|
|
236
|
+
popped: snapshotEntry(popped),
|
|
237
|
+
result: safeResult,
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
current() {
|
|
241
|
+
return exposeEntry(stack.length > 0 ? stack[stack.length - 1] : null);
|
|
242
|
+
},
|
|
243
|
+
canPop() {
|
|
244
|
+
return stack.length > 1;
|
|
245
|
+
},
|
|
246
|
+
applyState(snapshot) {
|
|
247
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
248
|
+
return resultErr('scene_flow_invalid_state');
|
|
249
|
+
}
|
|
250
|
+
if (snapshot.schema && snapshot.schema !== SCENE_FLOW_SCHEMA) {
|
|
251
|
+
return resultErr('scene_flow_invalid_state');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const sourceStack = Array.isArray(snapshot.stack)
|
|
255
|
+
? snapshot.stack
|
|
256
|
+
: snapshot.current
|
|
257
|
+
? [snapshot.current]
|
|
258
|
+
: [];
|
|
259
|
+
if (sourceStack.length <= 0) {
|
|
260
|
+
return resultErr('scene_flow_invalid_state');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const restored = [];
|
|
264
|
+
for (let index = 0; index < sourceStack.length; index += 1) {
|
|
265
|
+
const result = restoreEntry(sourceStack[index], nextMountId + index);
|
|
266
|
+
if (!result.ok) {
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
restored.push(result.entry);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
stack = restored;
|
|
273
|
+
const maxMountId = restored.reduce(
|
|
274
|
+
(highest, entry) => (entry.mountId > highest ? entry.mountId : highest),
|
|
275
|
+
0,
|
|
276
|
+
);
|
|
277
|
+
nextMountId = maxMountId + 1;
|
|
278
|
+
mutationCount = Number.isInteger(Number(snapshot.mutationCount)) && Number(snapshot.mutationCount) >= 0
|
|
279
|
+
? Number(snapshot.mutationCount)
|
|
280
|
+
: mutationCount;
|
|
281
|
+
|
|
282
|
+
return resultOk(buildStateFromEntries(restored, mutationCount), {
|
|
283
|
+
current: exposeEntry(restored[restored.length - 1] || null),
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
getState() {
|
|
287
|
+
return buildState();
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import projectRegistry from './project-registry.js';
|
|
2
|
+
import { createGameplayScene as sceneFactory1 } from "../../scenes/gameplay.scene.js";
|
|
3
|
+
|
|
4
|
+
const sceneFactories = {
|
|
5
|
+
"gameplay": sceneFactory1,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createSceneRegistry(context = {}) {
|
|
9
|
+
const scenes = {};
|
|
10
|
+
for (const entry of projectRegistry.scenes || []) {
|
|
11
|
+
const resolved = sceneFactories[entry.id];
|
|
12
|
+
if (typeof resolved === 'function') {
|
|
13
|
+
scenes[entry.id] = resolved(context);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (typeof resolved !== 'undefined' && resolved !== null) {
|
|
17
|
+
scenes[entry.id] = resolved;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
defaultSceneId: projectRegistry.startSceneId || null,
|
|
23
|
+
scenes,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export const SCREEN_SHELL_SCHEMA = 'aurajs.screen-shell.v1';
|
|
2
|
+
|
|
3
|
+
function cloneValue(value) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value.map((entry) => cloneValue(entry));
|
|
6
|
+
}
|
|
7
|
+
if (value && typeof value === 'object') {
|
|
8
|
+
const output = {};
|
|
9
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
10
|
+
output[key] = cloneValue(entry);
|
|
11
|
+
}
|
|
12
|
+
return output;
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeScreenId(screenId) {
|
|
18
|
+
if (typeof screenId !== 'string') return null;
|
|
19
|
+
const normalized = screenId.trim();
|
|
20
|
+
return normalized.length > 0 ? normalized : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function snapshotEntry(entry) {
|
|
24
|
+
if (!entry) return null;
|
|
25
|
+
return {
|
|
26
|
+
screenId: entry.screenId,
|
|
27
|
+
data: cloneValue(entry.data),
|
|
28
|
+
mountId: entry.mountId,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function snapshotState({ hud, overlay, modals, mutationCount }) {
|
|
33
|
+
return {
|
|
34
|
+
schema: SCREEN_SHELL_SCHEMA,
|
|
35
|
+
hud: snapshotEntry(hud),
|
|
36
|
+
overlay: snapshotEntry(overlay),
|
|
37
|
+
modals: modals.map((entry) => snapshotEntry(entry)),
|
|
38
|
+
modalDepth: modals.length,
|
|
39
|
+
mutationCount,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resultOk(state, extra = {}) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
reasonCode: 'screen_shell_ok',
|
|
47
|
+
...extra,
|
|
48
|
+
state,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resultErr(reasonCode) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reasonCode,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createScreenShell({ resolveScreen = null } = {}) {
|
|
60
|
+
let hud = null;
|
|
61
|
+
let overlay = null;
|
|
62
|
+
let modals = [];
|
|
63
|
+
let nextMountId = 1;
|
|
64
|
+
let mutationCount = 0;
|
|
65
|
+
|
|
66
|
+
function resolveKnownScreen(screenId) {
|
|
67
|
+
const normalized = normalizeScreenId(screenId);
|
|
68
|
+
if (!normalized) return { ok: false, reasonCode: 'screen_shell_invalid_screen_id' };
|
|
69
|
+
if (typeof resolveScreen !== 'function') {
|
|
70
|
+
return { ok: true, screenId: normalized };
|
|
71
|
+
}
|
|
72
|
+
const resolved = resolveScreen(normalized);
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
return { ok: false, reasonCode: 'screen_shell_unknown_screen' };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, screenId: normalized };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createEntry(screenId, data = null) {
|
|
80
|
+
return {
|
|
81
|
+
screenId,
|
|
82
|
+
data: cloneValue(data),
|
|
83
|
+
mountId: nextMountId++,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildState() {
|
|
88
|
+
return snapshotState({
|
|
89
|
+
hud,
|
|
90
|
+
overlay,
|
|
91
|
+
modals,
|
|
92
|
+
mutationCount,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function restoreEntry(snapshot, fallbackMountId) {
|
|
97
|
+
if (!snapshot) return null;
|
|
98
|
+
if (typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
99
|
+
return { ok: false, reasonCode: 'screen_shell_invalid_state' };
|
|
100
|
+
}
|
|
101
|
+
const resolved = resolveKnownScreen(snapshot.screenId);
|
|
102
|
+
if (!resolved.ok) {
|
|
103
|
+
return resultErr(resolved.reasonCode);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
entry: {
|
|
108
|
+
screenId: resolved.screenId,
|
|
109
|
+
data: cloneValue(snapshot.data),
|
|
110
|
+
mountId: Number.isInteger(Number(snapshot.mountId)) && Number(snapshot.mountId) > 0
|
|
111
|
+
? Number(snapshot.mountId)
|
|
112
|
+
: Number(fallbackMountId),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
setHud(screenId, data = null) {
|
|
119
|
+
const resolved = resolveKnownScreen(screenId);
|
|
120
|
+
if (!resolved.ok) return resultErr(resolved.reasonCode);
|
|
121
|
+
hud = createEntry(resolved.screenId, data);
|
|
122
|
+
mutationCount += 1;
|
|
123
|
+
return resultOk(buildState(), {
|
|
124
|
+
slot: 'hud',
|
|
125
|
+
hud: snapshotEntry(hud),
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
clearHud() {
|
|
129
|
+
hud = null;
|
|
130
|
+
mutationCount += 1;
|
|
131
|
+
return resultOk(buildState(), { slot: 'hud', hud: null });
|
|
132
|
+
},
|
|
133
|
+
showOverlay(screenId, data = null) {
|
|
134
|
+
const resolved = resolveKnownScreen(screenId);
|
|
135
|
+
if (!resolved.ok) return resultErr(resolved.reasonCode);
|
|
136
|
+
overlay = createEntry(resolved.screenId, data);
|
|
137
|
+
mutationCount += 1;
|
|
138
|
+
return resultOk(buildState(), {
|
|
139
|
+
slot: 'overlay',
|
|
140
|
+
overlay: snapshotEntry(overlay),
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
clearOverlay() {
|
|
144
|
+
overlay = null;
|
|
145
|
+
mutationCount += 1;
|
|
146
|
+
return resultOk(buildState(), {
|
|
147
|
+
slot: 'overlay',
|
|
148
|
+
overlay: null,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
pushModal(screenId, data = null) {
|
|
152
|
+
const resolved = resolveKnownScreen(screenId);
|
|
153
|
+
if (!resolved.ok) return resultErr(resolved.reasonCode);
|
|
154
|
+
const entry = createEntry(resolved.screenId, data);
|
|
155
|
+
modals.push(entry);
|
|
156
|
+
mutationCount += 1;
|
|
157
|
+
return resultOk(buildState(), {
|
|
158
|
+
modal: snapshotEntry(entry),
|
|
159
|
+
modalDepth: modals.length,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
popModal(result = null) {
|
|
163
|
+
if (modals.length <= 0) return resultErr('screen_shell_empty_modal_stack');
|
|
164
|
+
const popped = modals.pop();
|
|
165
|
+
mutationCount += 1;
|
|
166
|
+
return resultOk(buildState(), {
|
|
167
|
+
popped: snapshotEntry(popped),
|
|
168
|
+
result: cloneValue(result),
|
|
169
|
+
modalDepth: modals.length,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
applyState(snapshot) {
|
|
173
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
174
|
+
return resultErr('screen_shell_invalid_state');
|
|
175
|
+
}
|
|
176
|
+
if (snapshot.schema && snapshot.schema !== SCREEN_SHELL_SCHEMA) {
|
|
177
|
+
return resultErr('screen_shell_invalid_state');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const restoredHud = restoreEntry(snapshot.hud, nextMountId);
|
|
181
|
+
if (restoredHud && !restoredHud.ok) {
|
|
182
|
+
return restoredHud;
|
|
183
|
+
}
|
|
184
|
+
const restoredOverlay = restoreEntry(snapshot.overlay, nextMountId + 1);
|
|
185
|
+
if (restoredOverlay && !restoredOverlay.ok) {
|
|
186
|
+
return restoredOverlay;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const restoredModals = [];
|
|
190
|
+
const sourceModals = Array.isArray(snapshot.modals) ? snapshot.modals : [];
|
|
191
|
+
for (let index = 0; index < sourceModals.length; index += 1) {
|
|
192
|
+
const restored = restoreEntry(sourceModals[index], nextMountId + 2 + index);
|
|
193
|
+
if (!restored.ok) {
|
|
194
|
+
return restored;
|
|
195
|
+
}
|
|
196
|
+
restoredModals.push(restored.entry);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
hud = restoredHud ? restoredHud.entry : null;
|
|
200
|
+
overlay = restoredOverlay ? restoredOverlay.entry : null;
|
|
201
|
+
modals = restoredModals;
|
|
202
|
+
const maxMountId = [hud, overlay, ...modals].reduce(
|
|
203
|
+
(highest, entry) => (entry && entry.mountId > highest ? entry.mountId : highest),
|
|
204
|
+
0,
|
|
205
|
+
);
|
|
206
|
+
nextMountId = maxMountId + 1;
|
|
207
|
+
mutationCount = Number.isInteger(Number(snapshot.mutationCount)) && Number(snapshot.mutationCount) >= 0
|
|
208
|
+
? Number(snapshot.mutationCount)
|
|
209
|
+
: mutationCount;
|
|
210
|
+
|
|
211
|
+
return resultOk(buildState(), {
|
|
212
|
+
slot: 'restore',
|
|
213
|
+
hud: snapshotEntry(hud),
|
|
214
|
+
overlay: snapshotEntry(overlay),
|
|
215
|
+
modalDepth: modals.length,
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
getState() {
|
|
219
|
+
return buildState();
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|