@umicat/platform-sdk 0.3.0 → 0.5.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.
@@ -26,6 +26,8 @@ export declare class PostMessageTransport implements Transport {
26
26
  * longer to boot. One-shot hello was losing the handshake on Android.
27
27
  */
28
28
  static connect(timeoutMs?: number, sdkVersion?: string, helloRetryMs?: number): Promise<PostMessageTransport | null>;
29
+ /** The most recent host push of each type, for subscribers that arrive late. */
30
+ private readonly lastEvent;
29
31
  private installResultListener;
30
32
  on(event: string, handler: (payload: unknown) => void): () => void;
31
33
  call<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
@@ -14,6 +14,8 @@ export class PostMessageTransport {
14
14
  this.pending = new Map();
15
15
  this.seq = 0;
16
16
  this.eventHandlers = new Map();
17
+ /** The most recent host push of each type, for subscribers that arrive late. */
18
+ this.lastEvent = new Map();
17
19
  }
18
20
  /**
19
21
  * Perform the handshake. Resolves once parent has replied with `umicat:init`.
@@ -95,6 +97,17 @@ export class PostMessageTransport {
95
97
  return;
96
98
  }
97
99
  // Host-pushed event stream (e.g. 'umicat:voice') → registered handlers.
100
+ //
101
+ // Remembered as well as delivered. A host pushes STATE as soon as the
102
+ // handshake completes, and the modules that want it are constructed after
103
+ // `connect()` returns — so the first push of a fold arrived with nobody
104
+ // listening and was never repeated. The symptom is a game laid out for one
105
+ // screen on a device that is already folded, which then fixes itself the
106
+ // moment anything moves.
107
+ //
108
+ // Only the LAST of each type: this is for state, and a replayed queue of
109
+ // old transcript fragments would be worse than dropping them.
110
+ this.lastEvent.set(data.type, data);
98
111
  const handlers = this.eventHandlers.get(data.type);
99
112
  if (handlers)
100
113
  for (const handler of handlers)
@@ -108,6 +121,10 @@ export class PostMessageTransport {
108
121
  this.eventHandlers.set(event, handlers);
109
122
  }
110
123
  handlers.add(handler);
124
+ // Catch up, if this type already arrived before anyone subscribed.
125
+ const last = this.lastEvent.get(event);
126
+ if (last !== undefined)
127
+ handler(last);
111
128
  return () => { handlers.delete(handler); };
112
129
  }
113
130
  call(method, params, timeoutMs = 10000) {
@@ -0,0 +1,21 @@
1
+ import type { FoldModule } from './FoldModule.js';
2
+ export declare class ScreensModule {
3
+ /** Above the crease when folded; the whole viewport when flat. */
4
+ readonly top: HTMLElement;
5
+ /** Below the crease. Hidden — but still yours, and still populated — when
6
+ * the device is flat. */
7
+ readonly bottom: HTMLElement;
8
+ private second;
9
+ private readonly listeners;
10
+ constructor(fold: FoldModule);
11
+ /** Whether `bottom` is a real second screen right now. */
12
+ get hasSecondScreen(): boolean;
13
+ /**
14
+ * Calls `listener` on every change, and once immediately — same contract as
15
+ * `FoldModule.subscribe`, and for the same reason: a game told only about
16
+ * CHANGES lays itself out for one screen on a device that was already folded
17
+ * when it launched.
18
+ */
19
+ subscribe(listener: (screens: ScreensModule) => void): () => void;
20
+ private apply;
21
+ }
@@ -0,0 +1,101 @@
1
+ // Two screens a game can put ANYTHING on.
2
+ //
3
+ // `FoldModule` answers "where is the crease"; this answers "here are the two
4
+ // surfaces". A game that wants a map, an inventory, a text box or a second
5
+ // rendered view on the lower screen appends it to `bottom` and is done — the
6
+ // SDK keeps both boxes glued to the panels as the hinge moves.
7
+ //
8
+ // **Both elements always exist, and neither is ever replaced.** The obvious
9
+ // alternative — `bottom: HTMLElement | null`, null when the device is flat —
10
+ // is more honest about the hardware and much worse to use: every game would
11
+ // branch at startup, and anything already appended would be destroyed on the
12
+ // first unfold. Instead `bottom` is simply hidden while there is no second
13
+ // screen, so a game appends once and its content survives folding, unfolding
14
+ // and folding again. `hasSecondScreen` is there for the game that genuinely
15
+ // wants to say something different when there is only one.
16
+ //
17
+ // **`position: fixed` children land inside their screen, not the window.** The
18
+ // containers carry a `transform`, and a transformed ancestor becomes the
19
+ // containing block for fixed descendants. That is what lets an existing HUD —
20
+ // authored as fixed, anchored to "bottom-right" — move onto a panel whole,
21
+ // without touching a single widget.
22
+ /** Pointer-events are off on the container and on for what the game adds, so
23
+ * an empty top screen does not swallow taps meant for the world below it. */
24
+ const BASE_STYLE = {
25
+ position: 'fixed',
26
+ pointerEvents: 'none',
27
+ // Clips to the panel: content must not bleed across the crease, which is the
28
+ // one place on this device where "slightly too tall" is physically bent.
29
+ overflow: 'hidden',
30
+ // Establishes the containing block (see above) AND its own stacking context.
31
+ transform: 'translateZ(0)',
32
+ // Under the SDK's own touch layer (z-index 10), which shares the lower panel.
33
+ zIndex: '5',
34
+ };
35
+ export class ScreensModule {
36
+ constructor(fold) {
37
+ this.second = false;
38
+ this.listeners = new Set();
39
+ this.top = make('top');
40
+ this.bottom = make('bottom');
41
+ fold.subscribe((layout) => this.apply(layout));
42
+ }
43
+ /** Whether `bottom` is a real second screen right now. */
44
+ get hasSecondScreen() {
45
+ return this.second;
46
+ }
47
+ /**
48
+ * Calls `listener` on every change, and once immediately — same contract as
49
+ * `FoldModule.subscribe`, and for the same reason: a game told only about
50
+ * CHANGES lays itself out for one screen on a device that was already folded
51
+ * when it launched.
52
+ */
53
+ subscribe(listener) {
54
+ this.listeners.add(listener);
55
+ listener(this);
56
+ return () => { this.listeners.delete(listener); };
57
+ }
58
+ apply(layout) {
59
+ const split = layout.dualScreen && !!layout.top && !!layout.bottom;
60
+ this.second = split;
61
+ if (split) {
62
+ box(this.top, layout.top);
63
+ box(this.bottom, layout.bottom);
64
+ this.bottom.style.display = '';
65
+ }
66
+ else {
67
+ // Flat: the top screen IS the window, and there is no second one.
68
+ Object.assign(this.top.style, { left: '0', top: '0', width: '100%', height: '100%' });
69
+ this.bottom.style.display = 'none';
70
+ }
71
+ for (const listener of this.listeners)
72
+ listener(this);
73
+ }
74
+ }
75
+ function make(name) {
76
+ // Headless (tests, SSR): hand back a detached stub rather than throwing. A
77
+ // game is never rendering in that case, so the box has nothing to be glued
78
+ // to — but `screens.top` must still be an element, or every caller needs a
79
+ // null check for a situation that only exists in a test runner.
80
+ if (typeof document === 'undefined') {
81
+ return { style: {}, dataset: {}, setAttribute() { }, appendChild() { } };
82
+ }
83
+ const el = document.createElement('div');
84
+ el.dataset.umicatScreen = name;
85
+ // Survives the editor's "hide every static element" sweep, which otherwise
86
+ // takes anything it does not recognise in index.html.
87
+ el.setAttribute('data-umicat-keep', '');
88
+ Object.assign(el.style, BASE_STYLE);
89
+ if (name === 'bottom')
90
+ el.style.display = 'none';
91
+ (document.body ?? document.documentElement).appendChild(el);
92
+ return el;
93
+ }
94
+ function box(el, r) {
95
+ Object.assign(el.style, {
96
+ left: `${r.x}px`,
97
+ top: `${r.y}px`,
98
+ width: `${r.width}px`,
99
+ height: `${r.height}px`,
100
+ });
101
+ }
@@ -0,0 +1,37 @@
1
+ import type { FoldModule, FoldLayout } from './FoldModule.js';
2
+ /**
3
+ * Publishes the fold geometry as CSS custom properties, so a game's own DOM can
4
+ * follow the crease without reading the SDK at all:
5
+ *
6
+ * --umicat-top-height, --umicat-bottom-top, --umicat-bottom-height,
7
+ * --umicat-fold-top, --umicat-fold-height
8
+ *
9
+ * plus `data-umicat-dual-screen` on `<html>`, because a stylesheet cannot ask a
10
+ * module a question.
11
+ *
12
+ * **Engine-agnostic on purpose.** These NAMES are a contract between the SDK and
13
+ * a game's stylesheet — Gomoku's chat panel moves to the lower screen with no
14
+ * JavaScript at all — and a contract with two definitions drifts. It lived in
15
+ * `@umicat/three-sdk` while 3D was the only consumer; it moved here the moment
16
+ * the Phaser side needed the same names.
17
+ *
18
+ * All values are `0` when there is no fold, so a stylesheet that uses them is
19
+ * correct on a flat screen too.
20
+ *
21
+ * **Never give these a fallback in `var()`.** Without the property the whole
22
+ * `calc()` is invalid, the declaration is dropped, and the unfolded rule stands
23
+ * — which is right. A `0px` fallback computes a real but wrong position: for a
24
+ * bottom-anchored element, one off the screen.
25
+ */
26
+ export declare function installFoldVariables(fold: FoldModule): () => void;
27
+ /**
28
+ * Puts a canvas in the panel above the crease, or back where it was.
29
+ *
30
+ * The CSS BOX only — never the drawing buffer. Which half of the job that is
31
+ * depends on the engine: Phaser's scale manager sizes the buffer itself once
32
+ * the box changes, while a three.js game has to be told (`viewportSize`).
33
+ * Shared because the positioning is identical and the trap is too: a buffer
34
+ * sized to the upper panel inside a canvas still stretched across the window
35
+ * draws the picture at twice its height.
36
+ */
37
+ export declare function placeCanvasInPanel(canvas: HTMLCanvasElement, layout: FoldLayout): void;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Publishes the fold geometry as CSS custom properties, so a game's own DOM can
3
+ * follow the crease without reading the SDK at all:
4
+ *
5
+ * --umicat-top-height, --umicat-bottom-top, --umicat-bottom-height,
6
+ * --umicat-fold-top, --umicat-fold-height
7
+ *
8
+ * plus `data-umicat-dual-screen` on `<html>`, because a stylesheet cannot ask a
9
+ * module a question.
10
+ *
11
+ * **Engine-agnostic on purpose.** These NAMES are a contract between the SDK and
12
+ * a game's stylesheet — Gomoku's chat panel moves to the lower screen with no
13
+ * JavaScript at all — and a contract with two definitions drifts. It lived in
14
+ * `@umicat/three-sdk` while 3D was the only consumer; it moved here the moment
15
+ * the Phaser side needed the same names.
16
+ *
17
+ * All values are `0` when there is no fold, so a stylesheet that uses them is
18
+ * correct on a flat screen too.
19
+ *
20
+ * **Never give these a fallback in `var()`.** Without the property the whole
21
+ * `calc()` is invalid, the declaration is dropped, and the unfolded rule stands
22
+ * — which is right. A `0px` fallback computes a real but wrong position: for a
23
+ * bottom-anchored element, one off the screen.
24
+ */
25
+ export function installFoldVariables(fold) {
26
+ return fold.subscribe(publish);
27
+ }
28
+ function publish(layout) {
29
+ if (typeof document === 'undefined')
30
+ return;
31
+ const root = document.documentElement;
32
+ const px = (n) => `${Math.round(n ?? 0)}px`;
33
+ const on = layout.dualScreen;
34
+ root.style.setProperty('--umicat-top-height', px(on ? layout.top?.height : 0));
35
+ root.style.setProperty('--umicat-bottom-top', px(on ? layout.bottom?.y : 0));
36
+ root.style.setProperty('--umicat-bottom-height', px(on ? layout.bottom?.height : 0));
37
+ root.style.setProperty('--umicat-fold-top', px(on ? layout.division?.y : 0));
38
+ root.style.setProperty('--umicat-fold-height', px(on ? layout.division?.height : 0));
39
+ if (on)
40
+ root.dataset.umicatDualScreen = '';
41
+ else
42
+ delete root.dataset.umicatDualScreen;
43
+ }
44
+ /**
45
+ * Puts a canvas in the panel above the crease, or back where it was.
46
+ *
47
+ * The CSS BOX only — never the drawing buffer. Which half of the job that is
48
+ * depends on the engine: Phaser's scale manager sizes the buffer itself once
49
+ * the box changes, while a three.js game has to be told (`viewportSize`).
50
+ * Shared because the positioning is identical and the trap is too: a buffer
51
+ * sized to the upper panel inside a canvas still stretched across the window
52
+ * draws the picture at twice its height.
53
+ */
54
+ export function placeCanvasInPanel(canvas, layout) {
55
+ if (layout.dualScreen && layout.top) {
56
+ Object.assign(canvas.style, {
57
+ position: 'fixed',
58
+ left: `${layout.top.x}px`,
59
+ top: `${layout.top.y}px`,
60
+ width: `${layout.top.width}px`,
61
+ height: `${layout.top.height}px`,
62
+ });
63
+ }
64
+ else {
65
+ Object.assign(canvas.style, { position: '', left: '', top: '', width: '', height: '' });
66
+ }
67
+ }
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ 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
19
  export { FoldModule } from './fold/FoldModule.js';
20
+ export { installFoldVariables, placeCanvasInPanel } from './fold/foldVariables.js';
20
21
  export type { FoldLayout, FoldRect } from './fold/FoldModule.js';
21
22
  export { VoiceModule, webVoiceSupported } from './voice/VoiceModule.js';
22
23
  export type { VoiceCallbacks, VoiceSession, VoiceLang } from './voice/VoiceModule.js';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { UmicatRoom, PlayerDataFacade, RoomDataFacade, ChatFacade, MAX_CHAT_TEXT
16
16
  export { AiModule, Npc } from './ai/AiModule.js';
17
17
  export { PlatformModule } from './platform/PlatformModule.js';
18
18
  export { FoldModule } from './fold/FoldModule.js';
19
+ export { installFoldVariables, placeCanvasInPanel } from './fold/foldVariables.js';
19
20
  export { VoiceModule, webVoiceSupported } from './voice/VoiceModule.js';
20
21
  export { DialogueModule } from './dialogue/DialogueModule.js';
21
22
  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.0",
3
+ "version": "0.5.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",