@umicat/platform-sdk 0.2.1 → 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.
@@ -62,7 +62,23 @@ export class Npc {
62
62
  this.history = [];
63
63
  }
64
64
  async say(playerLine, opts) {
65
- this.history.push({ from: 'player', text: playerLine });
65
+ /**
66
+ * The conversation this turn BELONGS to, captured before the await.
67
+ *
68
+ * `reset()` points `this.history` at a new array. Reading the field again
69
+ * after the round trip therefore appends this answer to whatever
70
+ * conversation is current when it lands — so a game that was reset while
71
+ * an answer was in flight starts its next conversation with the previous
72
+ * one's last sentence already in the model's context. Invisible to the
73
+ * player, since the game has moved on and will not display it, and fully
74
+ * present to the model, which carries on from it.
75
+ *
76
+ * Bound here, a turn that outlives its conversation writes into an array
77
+ * nothing reads again, which is what "forget the conversation" has to
78
+ * mean for a turn already in the air.
79
+ */
80
+ const history = this.history;
81
+ history.push({ from: 'player', text: playerLine });
66
82
  const res = await this.ai.act({
67
83
  playbook: this.config.playbook,
68
84
  persona: {
@@ -73,10 +89,10 @@ export class Npc {
73
89
  },
74
90
  actions: this.config.actions,
75
91
  observation: opts?.observation,
76
- history: this.history,
92
+ history,
77
93
  });
78
94
  if (res.ok && res.say)
79
- this.history.push({ from: 'npc', text: res.say });
95
+ history.push({ from: 'npc', text: res.say });
80
96
  return res;
81
97
  }
82
98
  /** Record a world event the NPC should be aware of next turn (no LLM call). */
@@ -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;
@@ -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.2.1",
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",