@umicat/platform-sdk 0.2.2 → 0.3.0
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/dist/core/UmicatCore.d.ts +4 -0
- package/dist/core/UmicatCore.js +2 -0
- package/dist/fold/FoldModule.d.ts +58 -0
- package/dist/fold/FoldModule.js +87 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { RealtimeModule } from '../realtime/RealtimeModule.js';
|
|
|
5
5
|
import { AiModule } from '../ai/AiModule.js';
|
|
6
6
|
import { PlatformModule } from '../platform/PlatformModule.js';
|
|
7
7
|
import { VoiceModule } from '../voice/VoiceModule.js';
|
|
8
|
+
import { FoldModule } from '../fold/FoldModule.js';
|
|
8
9
|
import type { UmicatUser } from '../protocol.js';
|
|
9
10
|
export interface UmicatInitOptions {
|
|
10
11
|
/** Handshake timeout in ms when running inside a host. Default 5000. */
|
|
@@ -46,6 +47,9 @@ export declare class UmicatCore {
|
|
|
46
47
|
/** Voice input (speech-to-text) with a live mic level for waveform UIs.
|
|
47
48
|
* Browser recognition in a browser, native platform recognizer in the app. */
|
|
48
49
|
readonly voice: VoiceModule;
|
|
50
|
+
/** Where the device's fold falls across the play area. Always present;
|
|
51
|
+
* reports `dualScreen: false` on every surface without a hinge. */
|
|
52
|
+
readonly fold: FoldModule;
|
|
49
53
|
protected constructor(transport: Transport);
|
|
50
54
|
/** The current authenticated user, or `null` if anonymous / standalone. */
|
|
51
55
|
get user(): UmicatUser | null;
|
package/dist/core/UmicatCore.js
CHANGED
|
@@ -6,6 +6,7 @@ import { RealtimeModule } from '../realtime/RealtimeModule.js';
|
|
|
6
6
|
import { AiModule } from '../ai/AiModule.js';
|
|
7
7
|
import { PlatformModule } from '../platform/PlatformModule.js';
|
|
8
8
|
import { VoiceModule } from '../voice/VoiceModule.js';
|
|
9
|
+
import { FoldModule } from '../fold/FoldModule.js';
|
|
9
10
|
/**
|
|
10
11
|
* Umicat platform services bound to the current (game, user) — with no
|
|
11
12
|
* rendering engine anywhere in sight.
|
|
@@ -48,6 +49,7 @@ export class UmicatCore {
|
|
|
48
49
|
this.ai = new AiModule(transport);
|
|
49
50
|
this.platform = new PlatformModule(transport);
|
|
50
51
|
this.voice = new VoiceModule(transport);
|
|
52
|
+
this.fold = new FoldModule(transport);
|
|
51
53
|
}
|
|
52
54
|
/** The current authenticated user, or `null` if anonymous / standalone. */
|
|
53
55
|
get user() { return this.transport.user; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Transport } from '../core/Transport.js';
|
|
2
|
+
/** A rectangle in CSS pixels, in the game viewport's own coordinate space. */
|
|
3
|
+
export interface FoldRect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface FoldLayout {
|
|
10
|
+
/** Whether the play area should present itself as two screens right now. */
|
|
11
|
+
dualScreen: boolean;
|
|
12
|
+
/** Hinge angle in degrees: 0 shut, 180 flat. Absent on a device with no hinge. */
|
|
13
|
+
angle?: number;
|
|
14
|
+
/** The crease, margins INCLUDED. A band, not a line. */
|
|
15
|
+
division?: FoldRect;
|
|
16
|
+
/** The usable panel above the crease. */
|
|
17
|
+
top?: FoldRect;
|
|
18
|
+
/** The usable panel below it. */
|
|
19
|
+
bottom?: FoldRect;
|
|
20
|
+
/**
|
|
21
|
+
* Cut-outs content must not sit under — the front cameras.
|
|
22
|
+
*
|
|
23
|
+
* Only the ACTIVE ones: a folding phone reports a camera on the display it is
|
|
24
|
+
* not currently using, and reserving space for a lens that is facing away is
|
|
25
|
+
* just lost screen.
|
|
26
|
+
*/
|
|
27
|
+
occlusions: FoldRect[];
|
|
28
|
+
viewport?: {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Live fold state, and a subscription to changes.
|
|
35
|
+
*
|
|
36
|
+
* Reports `dualScreen: false` forever on a host that never pushes — a browser
|
|
37
|
+
* tab, an older native build, a phone that does not fold. Callers do not
|
|
38
|
+
* feature-detect; they lay out against whatever `layout` currently says.
|
|
39
|
+
*/
|
|
40
|
+
export declare class FoldModule {
|
|
41
|
+
private readonly transport;
|
|
42
|
+
private current;
|
|
43
|
+
private readonly listeners;
|
|
44
|
+
constructor(transport: Transport);
|
|
45
|
+
/** What the fold is doing right now. Never null. */
|
|
46
|
+
get layout(): FoldLayout;
|
|
47
|
+
/**
|
|
48
|
+
* Calls `listener` on every change, and ONCE immediately with the current
|
|
49
|
+
* value.
|
|
50
|
+
*
|
|
51
|
+
* The immediate call is the point: a game that only hears about changes comes
|
|
52
|
+
* up laid out for a flat screen on a device that was already folded when it
|
|
53
|
+
* launched, and then looks correct forever after the first hinge movement —
|
|
54
|
+
* which is the worst possible version of this bug, because it fixes itself
|
|
55
|
+
* the moment anyone tries to reproduce it.
|
|
56
|
+
*/
|
|
57
|
+
subscribe(listener: (layout: FoldLayout) => void): () => void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const FOLD_EVENT = 'umicat:fold';
|
|
2
|
+
/** The flat, unfolded answer — what every surface without a hinge reports. */
|
|
3
|
+
const NO_FOLD = { dualScreen: false, occlusions: [] };
|
|
4
|
+
/**
|
|
5
|
+
* Live fold state, and a subscription to changes.
|
|
6
|
+
*
|
|
7
|
+
* Reports `dualScreen: false` forever on a host that never pushes — a browser
|
|
8
|
+
* tab, an older native build, a phone that does not fold. Callers do not
|
|
9
|
+
* feature-detect; they lay out against whatever `layout` currently says.
|
|
10
|
+
*/
|
|
11
|
+
export class FoldModule {
|
|
12
|
+
constructor(transport) {
|
|
13
|
+
this.transport = transport;
|
|
14
|
+
this.current = NO_FOLD;
|
|
15
|
+
this.listeners = new Set();
|
|
16
|
+
transport.on?.(FOLD_EVENT, (payload) => {
|
|
17
|
+
const next = parse(payload);
|
|
18
|
+
if (!next)
|
|
19
|
+
return;
|
|
20
|
+
this.current = next;
|
|
21
|
+
for (const listener of this.listeners)
|
|
22
|
+
listener(next);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** What the fold is doing right now. Never null. */
|
|
26
|
+
get layout() {
|
|
27
|
+
return this.current;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Calls `listener` on every change, and ONCE immediately with the current
|
|
31
|
+
* value.
|
|
32
|
+
*
|
|
33
|
+
* The immediate call is the point: a game that only hears about changes comes
|
|
34
|
+
* up laid out for a flat screen on a device that was already folded when it
|
|
35
|
+
* launched, and then looks correct forever after the first hinge movement —
|
|
36
|
+
* which is the worst possible version of this bug, because it fixes itself
|
|
37
|
+
* the moment anyone tries to reproduce it.
|
|
38
|
+
*/
|
|
39
|
+
subscribe(listener) {
|
|
40
|
+
this.listeners.add(listener);
|
|
41
|
+
listener(this.current);
|
|
42
|
+
return () => { this.listeners.delete(listener); };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function rect(value) {
|
|
46
|
+
if (!value || typeof value !== 'object')
|
|
47
|
+
return undefined;
|
|
48
|
+
const r = value;
|
|
49
|
+
const nums = ['x', 'y', 'width', 'height'].map((k) => r[k]);
|
|
50
|
+
if (nums.some((n) => typeof n !== 'number' || !Number.isFinite(n)))
|
|
51
|
+
return undefined;
|
|
52
|
+
const [x, y, width, height] = nums;
|
|
53
|
+
return { x, y, width, height };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Host payload → `FoldLayout`, or null if it is not one.
|
|
57
|
+
*
|
|
58
|
+
* Validated rather than cast: this crosses a postMessage boundary, and a
|
|
59
|
+
* half-shaped payload that reaches layout code is a crash in the middle of
|
|
60
|
+
* someone's game rather than a line in a console.
|
|
61
|
+
*/
|
|
62
|
+
function parse(payload) {
|
|
63
|
+
if (!payload || typeof payload !== 'object')
|
|
64
|
+
return null;
|
|
65
|
+
const p = payload;
|
|
66
|
+
if (typeof p.dualScreen !== 'boolean')
|
|
67
|
+
return null;
|
|
68
|
+
const occlusions = Array.isArray(p.occlusions)
|
|
69
|
+
? p.occlusions.map(rect).filter((r) => !!r)
|
|
70
|
+
: [];
|
|
71
|
+
const viewport = p.viewport && typeof p.viewport === 'object'
|
|
72
|
+
? p.viewport
|
|
73
|
+
: undefined;
|
|
74
|
+
const top = rect(p.top);
|
|
75
|
+
const bottom = rect(p.bottom);
|
|
76
|
+
return {
|
|
77
|
+
// A split with only one side is not a split. Guarding here keeps every
|
|
78
|
+
// consumer from having to check both panels before using either.
|
|
79
|
+
dualScreen: p.dualScreen && !!top && !!bottom,
|
|
80
|
+
angle: typeof p.angle === 'number' ? p.angle : undefined,
|
|
81
|
+
division: rect(p.division),
|
|
82
|
+
top,
|
|
83
|
+
bottom,
|
|
84
|
+
occlusions,
|
|
85
|
+
viewport,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type { JoinOptions, RoomListEntry, RoomListOptions } from './realtime/Rea
|
|
|
16
16
|
export { AiModule, Npc } from './ai/AiModule.js';
|
|
17
17
|
export type { NpcConfig } from './ai/AiModule.js';
|
|
18
18
|
export { PlatformModule } from './platform/PlatformModule.js';
|
|
19
|
+
export { FoldModule } from './fold/FoldModule.js';
|
|
20
|
+
export type { FoldLayout, FoldRect } from './fold/FoldModule.js';
|
|
19
21
|
export { VoiceModule, webVoiceSupported } from './voice/VoiceModule.js';
|
|
20
22
|
export type { VoiceCallbacks, VoiceSession, VoiceLang } from './voice/VoiceModule.js';
|
|
21
23
|
export { DialogueModule } from './dialogue/DialogueModule.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { RealtimeModule } from './realtime/RealtimeModule.js';
|
|
|
15
15
|
export { UmicatRoom, PlayerDataFacade, RoomDataFacade, ChatFacade, MAX_CHAT_TEXT_LEN, } from './realtime/UmicatRoom.js';
|
|
16
16
|
export { AiModule, Npc } from './ai/AiModule.js';
|
|
17
17
|
export { PlatformModule } from './platform/PlatformModule.js';
|
|
18
|
+
export { FoldModule } from './fold/FoldModule.js';
|
|
18
19
|
export { VoiceModule, webVoiceSupported } from './voice/VoiceModule.js';
|
|
19
20
|
export { DialogueModule } from './dialogue/DialogueModule.js';
|
|
20
21
|
export { DialogueRunner, resolveLangText } from './dialogue/runner.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/platform-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Umicat's platform layer, independent of any rendering engine: identity, cloud saves, shared game data, multiplayer, runtime AI, voice and scripted dialogue. Consumed by @umicat/phaser-sdk and @umicat/three-sdk alike.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|