@uzuhq/code-sdk 0.8.11 → 0.9.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/README.md CHANGED
@@ -14,14 +14,15 @@ npm install @uzuhq/code-sdk
14
14
 
15
15
  すべてのブリッジメッセージは `{ channel, type, payload }` の共通構造を持つ。
16
16
 
17
- | チャネル | 用途 | 説明 |
18
- | -------- | ---------------------------- | ------------------------------------------------------------- |
19
- | `sdk` | SDK/プラットフォームコマンド | サウンド再生、マイク制御、ルーム移動など SDK 内部のメッセージ |
20
- | `game` | ゲーム独自メッセージ | `send()` / `on()` で送受信するカスタムメッセージ |
17
+ | チャネル | 用途 | 説明 |
18
+ | ---------- | ---------------------------- | -------------------------------------------------------------------------------- |
19
+ | `sdk` | SDK/プラットフォームコマンド | サウンド再生、マイク操作、通話の状態など SDK 内部のメッセージ |
20
+ | `game` | ゲーム独自メッセージ | `send()` / `on()` で送受信するカスタムメッセージ |
21
+ | `platform` | アプリとサーバーの伝言 | 通話の申告・一時停止の要求・presence。SDK が中身を見ずに運ぶ。シナリオは触らない |
21
22
 
22
23
  `send()` は `channel: 'game'` のメッセージのみを送信する。`on()` は `channel: 'game'` のメッセージのみを受信し、handler には `payload` が直接渡される。
23
24
 
24
- SDK チャネルのメッセージは `playSound()`, `setMicEnabled()` などの専用関数から自動送信される。
25
+ SDK チャネルのメッセージは `playSound()`, `muteMic()` などの専用関数から自動送信される。
25
26
 
26
27
  ---
27
28
 
@@ -75,14 +76,15 @@ run({
75
76
 
76
77
  ### GameConfig
77
78
 
78
- | キー | 型 | 必須 | 説明 |
79
- | ------------------------- | -------------------------------------------------------------- | ---- | -------------------------- |
80
- | `logic` | `GameLogic<S>` | Yes | ゲームロジック定義 |
81
- | `onState` | `(state: S, myPlayerId: string) => void` | Yes | state 更新時のコールバック |
82
- | `inputs` | `(sendAction: (action: string, payload: any) => void) => void` | Yes | 入力ハンドラ登録 |
83
- | `events` | `Record<string, (data: any) => void>` | No | ゲームイベントハンドラ |
84
- | `playerCount` | `number` | Yes | プレイヤー数 |
85
- | `onConnectionStateChange` | `(state: ConnectionState) => void` | No | 接続状態変化コールバック |
79
+ | キー | 型 | 必須 | 説明 |
80
+ | ------------- | -------------------------------------------------------------- | ---- | -------------------------- |
81
+ | `logic` | `GameLogic<S>` | Yes | ゲームロジック定義 |
82
+ | `onState` | `(state: S, myPlayerId: string) => void` | Yes | state 更新時のコールバック |
83
+ | `inputs` | `(sendAction: (action: string, payload: any) => void) => void` | Yes | 入力ハンドラ登録 |
84
+ | `events` | `Record<string, (data: any) => void>` | No | ゲームイベントハンドラ |
85
+ | `playerCount` | `number` | Yes | プレイヤー数 |
86
+
87
+ 接続の状態は `run()` の設定ではなく [`onPresenceChange()`](#接続と通話の状態) で読む。
86
88
 
87
89
  ### GameLogic
88
90
 
@@ -127,13 +129,14 @@ const logic: GameLogic<MyState> = {
127
129
  };
128
130
  ```
129
131
 
130
- | キー | 型 | 必須 | 説明 |
131
- | --------------- | ---------------------------------------- | ---- | --------------------------------------------------------------- |
132
- | `setup` | `(args: SetupArgs) => S` | Yes | 初期 state を生成。`args.players` は配役を受け取る参加者だけ |
133
- | `actions` | `Record<string, ActionHandler<S>>` | Yes | クライアント先読み + サーバーの 2 回走る。決定的であること |
134
- | `serverActions` | `Record<string, ServerActionHandler<S>>` | No | サーバーでのみ走る。async 可。`ctx` に `tick` / `random` が入る |
135
- | `update` | `(args: UpdateArgs<S>) => void` | Yes | 毎 tick 実行 (`tickRate` が 0 なら呼ばれない) |
136
- | `tickRate` | `number` | No | 秒間 tick 数 (default: 0 = tick なし) |
132
+ | キー | 型 | 必須 | 説明 |
133
+ | --------------- | ---------------------------------------- | ---- | -------------------------------------------------------------------- |
134
+ | `setup` | `(args: SetupArgs) => S` | Yes | 初期 state を生成。`args.players` は配役を受け取る参加者だけ |
135
+ | `actions` | `Record<string, ActionHandler<S>>` | Yes | クライアント先読み + サーバーの 2 回走る。決定的であること |
136
+ | `serverActions` | `Record<string, ServerActionHandler<S>>` | No | サーバーでのみ走る。async 可。`ctx` に `tick` / `random` が入る |
137
+ | `update` | `(args: UpdateArgs<S>) => void` | Yes | 毎 tick 実行 (`tickRate` が 0 なら呼ばれない) |
138
+ | `tickRate` | `number` | No | 秒間 tick 数 (default: 0 = tick なし) |
139
+ | `voicePlan` | `(args: VoicePlanArgs<S>) => VoicePlan` | No | 通話の部屋とマイクの方針 ([通話の部屋とマイク](#通話の部屋とマイク)) |
137
140
 
138
141
  ハンドラの引数は 1 つのオブジェクトで、使うものだけ書けばよい。
139
142
  `state` / `payload` / `playerId` はその呼び出しの事実、`ctx` は実行環境が与えるもの。
@@ -204,11 +207,23 @@ const remain = Math.ceil(minus(state.phaseEndsAt, gameTime()) / 1000);
204
207
  演出だけを止めたいときに限り、読み取り専用で参照できる。
205
208
 
206
209
  ```ts
207
- import { isPaused, onPauseChange } from '@uzuhq/code-sdk';
210
+ import { getPause, onPauseChange } from '@uzuhq/code-sdk';
211
+
212
+ onPauseChange((pause) => (pause ? engine.stop() : engine.start()));
213
+ ```
214
+
215
+ `getPause()` / `onPauseChange` が渡すのは止まった理由 (`Pause`)。止まっていなければ `null`。
208
216
 
209
- onPauseChange((paused) => (paused ? engine.stop() : engine.start()));
217
+ ```ts
218
+ type Pause =
219
+ | null
220
+ | { reason: 'emergency'; by: SeatId } // 緊急一時停止。by は止めたプレイヤー
221
+ | { reason: 'waiting'; for: SeatId[]; since: number }; // 自動中断用。今のサーバーは出さない
210
222
  ```
211
223
 
224
+ 理由は今後増えることがあるので、知らない `reason` でも `null` でなければ止まっているものとして扱う。
225
+ 止まっている画面はアプリが描く。
226
+
212
227
  `GameLogic` からは触れない。停止を state に持ち込むと「停止中は state が変わらない」という
213
228
  前提が崩れる。
214
229
 
@@ -269,32 +284,99 @@ stopBgm(); // BGM 停止
269
284
 
270
285
  ---
271
286
 
272
- ## デバイス連携
287
+ ## 通話の部屋とマイク
288
+
289
+ 通話の部屋とマイクの方針は、ロジックの `voicePlan` が state から返す。画面から部屋を直接変える
290
+ API は無い。部屋を変えたいときは action で state を変え、`voicePlan` がそこから新しい部屋を返す。
273
291
 
274
- ### `setMicEnabled(enabled: boolean): void`
292
+ ```ts
293
+ import type { GameLogic } from '@uzuhq/code-sdk';
294
+
295
+ const logic: GameLogic<MyState> = {
296
+ // ...
297
+ voicePlan: ({ state, playerId }) => ({
298
+ room: state.rooms[playerId] ?? null, // null は全体の部屋
299
+ mic: state.phase === 'reading' ? 'locked' : 'free',
300
+ }),
301
+ };
302
+ ```
275
303
 
276
- マイクのオンオフを切り替える。
304
+ - state が変わるたびに、サーバーがプレイヤー 1 人ずつについて呼ぶ。`deadlines` の `at` と同じく
305
+ **state だけから決まる軽い純関数にすること** (実時刻や乱数を読むと、呼ぶたびに答えが変わって部屋が暴れる)
306
+ - 書かなければ全員が全体の部屋、マイクは本人の自由
307
+ - 投げたり使えない値を返したりしたら、直前の結果を使い続ける
308
+ - 観測席 (観戦・進行管理) は対象外。全体の部屋にミュートで入り、聴く部屋は `setListeningRoom()` で選ぶ
309
+
310
+ | `mic` | 場面に入ったとき | 場面の中 | 場面を抜けたとき |
311
+ | -------- | ---------------- | -------------------------------------------------------------------------- | --------------------------------------- |
312
+ | `free` | 何もしない | 本人の自由 | 何もしない |
313
+ | `muted` | ミュートする | 本人は外せる | 何もしない (ミュートは本人が外す) |
314
+ | `locked` | ミュートする | 本人も外せない。押すと「この場面ではマイクを使えません」のダイアログが出る | ロックだけ外れる (ミュートは本人が外す) |
315
+
316
+ 作品にできるのはマイクを制限することだけで、開くことはできない。場面を抜けても、ミュートは自動では外れない。勝手にマイクを開くと、本人の知らないうちに声が他の人へ届いてしまうため。
317
+
318
+ 緊急一時停止の間は全員が全体の部屋に集まり、`locked` のロックも外れる (ミュートは切れたまま)。
319
+ 再開すると部屋に戻り、またロックしてミュートする。
320
+
321
+ ### `muteMic(): void`
322
+
323
+ 本人のマイクをミュートする。
324
+
325
+ ```ts
326
+ import { muteMic } from '@uzuhq/code-sdk';
327
+
328
+ muteMic();
329
+ ```
330
+
331
+ - 作品からできるのはミュートだけで、外すことはできない。外すのは本人が HUD のマイクボタンで行う (勝手にマイクを開くと、本人の知らないうちに声が他の人へ届くため)
332
+ - 場面ごとに決まったミュートは `voicePlan` の `mic` で宣言する
333
+ - 今のマイクの状態は `getPresence().me.voice` で読む
334
+
335
+ ### `setListeningRoom(room: string | null): void`
336
+
337
+ 観測席 (観戦・進行管理) が聴く部屋を選ぶ。`null` は全体の部屋。プレイヤー席で呼んでも何も起きない
338
+ (プレイヤーの部屋は `voicePlan` が決める)。
277
339
 
278
340
  ```ts
279
- import { setMicEnabled } from '@uzuhq/code-sdk';
341
+ import { setListeningRoom } from '@uzuhq/code-sdk';
280
342
 
281
- setMicEnabled(false); // マイクをミュート
282
- setMicEnabled(true); // マイクをオン
343
+ setListeningRoom('secret'); // 密談室を聴く
283
344
  ```
284
345
 
285
- ### `changeRoom(roomId: string | null): void`
346
+ ---
347
+
348
+ ## 接続と通話の状態
286
349
 
287
- ボイスチャットルームを移動する。`null` でデフォルトルーム(全体ルーム)に戻る。
350
+ ### `getPresence(): Presence` / `onPresenceChange(listener): void`
351
+
352
+ 自分の接続と、プレイヤー全員の通話の状態。他の人については声が届くかだけを出す (ゲームの接続は出さない。
353
+ アプリが裏に回るとゲームの画面は止まるが、通話は続くため)。
288
354
 
289
355
  ```ts
290
- import { changeRoom } from '@uzuhq/code-sdk';
356
+ import { getPresence, onPresenceChange } from '@uzuhq/code-sdk';
291
357
 
292
- changeRoom('room_a'); // room_a に移動(同じルームのプレイヤーとのみ音声通話可能)
293
- changeRoom(null); // デフォルトルーム(全員が同じ音声チャンネル)に戻る
358
+ onPresenceChange((presence, changes) => {
359
+ for (const [id, other] of Object.entries(presence.others)) drawSeat(id, other);
360
+ for (const change of changes) {
361
+ if (change.kind === 'voice' && change.to === 'lost')
362
+ toast(`${change.seatId} の声が届いていません`);
363
+ }
364
+ });
294
365
  ```
295
366
 
296
- - 初期状態ではすべてのプレイヤーがデフォルトルーム(`null`)に所属する
297
- - `roomId` に文字列を指定すると、そのプレイヤーは指定ルームへ移動する
367
+ - 誰か (名前・キャラクター) は名簿 (`setup` の `players` / state) から、自分の席 ID と席種は `onState` の引数から引く
368
+ - 切れた側への変化は 5 秒続いたものだけが確定する。瞬断では変わらない
369
+ - `changes` は自分の接続の変化、全員の通話の状態の変化、観測席の人数の変化だけ。発話やミュートの変化では空で、値だけが新しくなる
370
+ - 発話は 1 秒に数回変わるので、描画のフレームごとにまとめて届く
371
+ - アプリも HUD の下に帯 (「〇〇の声が届いていません」など) を出す。シナリオが自前で出しても重ねて出る
372
+ - `roomId` なしのローカル実行では、自分は `online`・通話は `off` に固定される
373
+ - `onPresenceChange()` / `onPauseChange()` の戻り値の関数を呼ぶと、呼ばれなくなる (場面ごとに登録し直すときに使う)
374
+
375
+ 型は [Presence](#presence) を参照。
376
+
377
+ ---
378
+
379
+ ## デバイス連携
298
380
 
299
381
  ### `haptic(kind: 'light' | 'medium' | 'heavy'): void`
300
382
 
@@ -329,21 +411,6 @@ onState(state) {
329
411
  - 再描画のたびに呼んでもよい (ホストは処理中の 2 回目以降を無視する)
330
412
  - 対応していない古いアプリや `uzu dev` では何も起きないので、呼んだあとも画面を操作不能にしない
331
413
 
332
- ### `onPlayersChanged(handler: (players: Record<string, PlayerVoiceState>) => void): void`
333
-
334
- プレイヤーのリアルタイム状態(音声状態)が変化したときのハンドラを登録する。
335
-
336
- ```ts
337
- import { onPlayersChanged } from '@uzuhq/code-sdk';
338
-
339
- onPlayersChanged((players) => {
340
- for (const [id, state] of Object.entries(players)) {
341
- // state.audioStatus: 'speaking' | 'listening' | 'muted' | 'unstable' | null
342
- updatePlayerUI(id, state.audioStatus);
343
- }
344
- });
345
- ```
346
-
347
414
  ---
348
415
 
349
416
  ## SafeArea / HUD 回避
@@ -423,7 +490,7 @@ on('attack', (payload) => {
423
490
  ```
424
491
 
425
492
  :::caution
426
- `on()` は `channel: 'game'` のメッセージのみを受信する。SDK チャネルのメッセージ(`playersChanged` 等)は `onPlayersChanged()` などの専用関数を使用すること。
493
+ `on()` は `channel: 'game'` のメッセージのみを受信する。通話や接続の状態は `onPresenceChange()`、一時停止は `onPauseChange()` などの専用関数を使用すること。
427
494
  :::
428
495
 
429
496
  ### `isHosted`
@@ -438,7 +505,7 @@ on('attack', (payload) => {
438
505
 
439
506
  ```ts
440
507
  interface BridgeMessage {
441
- channel: 'sdk' | 'game';
508
+ channel: 'sdk' | 'game' | 'platform';
442
509
  type: string;
443
510
  payload: Record<string, unknown>;
444
511
  }
@@ -487,33 +554,73 @@ onState(state, myPlayerId, mySeatKind) {
487
554
 
488
555
  ### ConnectionState
489
556
 
557
+ `ReconnectableWebSocket` の接続状態。シナリオが見る自分の接続は `getPresence().me.connection`。
558
+
490
559
  ```ts
491
560
  type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
492
561
  ```
493
562
 
494
- ### ConnectionCallbacks
563
+ ### VoicePlan
564
+
565
+ `voicePlan` が返す値。
495
566
 
496
567
  ```ts
497
- interface ConnectionCallbacks {
498
- onConnectionStateChange?: (state: ConnectionState) => void;
499
- }
568
+ type VoicePlan = {
569
+ room: string | null; // 入る部屋。null は全体の部屋
570
+ mic: 'free' | 'muted' | 'locked';
571
+ };
500
572
  ```
501
573
 
502
- ### PlayerVoiceState
574
+ ### Presence
575
+
576
+ `getPresence()` / `onPresenceChange` が渡す値。`since` は端末の時刻 (ms)。
503
577
 
504
578
  ```ts
505
- interface PlayerVoiceState {
506
- audioStatus: 'speaking' | 'listening' | 'muted' | 'unstable' | null;
507
- }
579
+ type Presence = {
580
+ me: Me; // 観測席でも必ずある
581
+ others: Record<SeatId, Other>; // 自分以外のプレイヤー
582
+ observers: { count: number; speaking: { id: SeatId; nickname: string }[] }; // 観測席は人数と、今話している人だけ
583
+ };
584
+
585
+ type Me = {
586
+ connection:
587
+ { status: 'online' } | { status: 'connecting' | 'reconnecting' | 'offline'; since: number };
588
+ voice:
589
+ | { status: 'off'; reason: 'table' | 'this-device' } // 卓が通話を使わない / この端末は通話に入らない
590
+ | { status: 'connecting' | 'reconnecting'; since: number }
591
+ | { status: 'failed'; reason: 'mic-permission' | 'network'; since: number }
592
+ | {
593
+ status: 'here';
594
+ room: string | null;
595
+ mic: Mic;
596
+ speaking: boolean;
597
+ quality: 'good' | 'poor';
598
+ };
599
+ };
600
+
601
+ type Other = {
602
+ voice:
603
+ | { status: 'off' }
604
+ | { status: 'here'; mic: Mic; speaking: boolean; quality: 'good' | 'poor' } // 自分と同じ部屋で聞こえている
605
+ | { status: 'elsewhere'; room: string | null } // 別の部屋にいる
606
+ | { status: 'lost'; since: number } // 声が届かない (通話が切れた / 同じ部屋のはずなのに聞こえなくなった)
607
+ | { status: 'unknown'; reason: 'me-not-connected' | 'no-report' }; // 自分が通話にいない / 申告が届かず聞こえてもいない
608
+ };
609
+
610
+ type Mic =
611
+ | { status: 'open' }
612
+ | { status: 'muted'; by: 'self' | 'scenario' } // scenario = 場面の頭で切られたまま。本人は戻せる
613
+ | { status: 'locked' } // 場面の間は本人も解除できない
614
+ | { status: 'listen-only' }; // ライブ配信の観戦者。聴くだけ
615
+
616
+ type PresenceChange =
617
+ | { kind: 'connection'; seatId: SeatId; from: ConnectionStatus; to: ConnectionStatus } // 自分の接続だけ
618
+ | { kind: 'voice'; seatId: SeatId; from: VoiceStatus; to: VoiceStatus }
619
+ | { kind: 'observers'; from: number; to: number };
508
620
  ```
509
621
 
510
- | audioStatus | 説明 |
511
- | ----------- | ------------------------ |
512
- | `speaking` | 発話中 |
513
- | `listening` | 音声接続済み・聞いている |
514
- | `muted` | ミュート中 |
515
- | `unstable` | 接続に問題あり |
516
- | `null` | 音声通話に未接続 |
622
+ `others` の通話は本人の申告に自分の端末の見え方 (自分の部屋、声が聞こえるか) を重ねたものなので、
623
+ 端末ごとに違いうる。本人の申告が届かなくても (アプリが裏に回った等)、自分の端末で聞こえていれば `here` になる。
517
624
 
518
625
  ---
519
626
 
@@ -521,15 +628,16 @@ interface PlayerVoiceState {
521
628
 
522
629
  `init()` / `run()` は以下の URL パラメータを読み取る。
523
630
 
524
- | パラメータ | 説明 |
525
- | ------------------------------------------- | --------------------------------------------- |
526
- | `?server=` | WebSocket サーバーの URL |
527
- | `?roomId=` | ルーム ID。指定するとオンラインモードになる |
528
- | `?seatId=` | 自分の席 ID |
529
- | `?players=` | JSON エンコードされたプレイヤーリスト |
530
- | `?__dev=N` | Dev ハーネスモード (N 画面の iframe 並列表示) |
531
- | `?uzuSafeAreaInset{Top,Right,Bottom,Left}=` | デバイスの safe area。ホストが配る |
532
- | `?uzuHudInsetX=` / `?uzuHudInsetY=` | HUD 矩形の右下座標。ホストが配る |
631
+ | パラメータ | 説明 |
632
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------ |
633
+ | `?server=` | WebSocket サーバーの URL |
634
+ | `?roomId=` | ルーム ID。指定するとオンラインモードになる |
635
+ | `?seatId=` | 自分の席 ID |
636
+ | `?players=` | JSON エンコードされたプレイヤーリスト |
637
+ | `?monitor=1` | 運営や作者が監視用に開いた画面。ホストが付ける。サーバーはこの接続を数えず、止める要求も受けない |
638
+ | `?__dev=N` | Dev ハーネスモード (N 画面の iframe 並列表示) |
639
+ | `?uzuSafeAreaInset{Top,Right,Bottom,Left}=` | デバイスの safe area。ホストが配る |
640
+ | `?uzuHudInsetX=` / `?uzuHudInsetY=` | HUD 矩形の右下座標。ホストが配る |
533
641
 
534
642
  ### モードの自動判定
535
643
 
@@ -1,4 +1,4 @@
1
- import { r as UzuDevHooks } from "./dev-hooks-7UDN8Xac.js";
1
+ import { r as UzuDevHooks } from "./dev-hooks-D30dnLtR.js";
2
2
  //#region src/dev-globals.d.ts
3
3
  declare global {
4
4
  interface Window {
@@ -55,6 +55,64 @@ declare const minus: (a: GameTime, b: GameTime) => Duration;
55
55
  */
56
56
  declare const GAME_START: GameTime;
57
57
  //#endregion
58
+ //#region ../engine-core/src/presence.d.ts
59
+ /**
60
+ * @docs
61
+ * - 接続と通話の状態 (Presence): docs/docs/uzu_code/presence.md
62
+ * - ブリッジ仕様: docs/docs/uzu_code/bridge.md
63
+ *
64
+ * 接続と通話の状態に関わる型。ロジックに渡す型と、play-server が全員へ配る
65
+ * presence の型をここに置き、room-core と SDK が同じ定義を読む。
66
+ *
67
+ * 判断はサーバー (room-core) に集める。落ちたか、どの部屋にいるべきか、全員を止めるかは
68
+ * サーバーが決めて配る。SDK はシナリオに焼き込まれて二度と変わらないので、ここにある
69
+ * 値を表どおりに並べるだけにする。
70
+ */
71
+ /** 席 ID。`playerId`・setup の `players` (Seat) の `id` と同じ文字列。 */
72
+ type SeatId = string;
73
+ /**
74
+ * マイクの方針。作品にできるのはマイクを制限することだけで、開くことはできない。
75
+ *
76
+ * - `free`: 何もしない (ミュートも、ミュートの解除もしない)
77
+ * - `muted`: 場面の頭でミュートする。本人は外せる
78
+ * - `locked`: 場面の間は本人も外せない。一時停止の間だけロックが外れる
79
+ *
80
+ * `muted` / `locked` の場面を抜けても、ミュートは自動では外れない。外すのは本人だけ
81
+ * (勝手にマイクを開くと、本人の知らないうちに声が他の人へ届くため)。
82
+ */
83
+ type MicPolicy = 'free' | 'muted' | 'locked';
84
+ /** プレイヤー 1 人の通話の部屋とマイクの方針。 */
85
+ type VoicePlan = {
86
+ /** 入る部屋。null は全体の部屋。 */
87
+ room: string | null;
88
+ mic: MicPolicy;
89
+ };
90
+ /** `voicePlan` を書かない作品の既定。全員が全体の部屋、マイクは本人の自由。 */
91
+ declare const DEFAULT_VOICE_PLAN: VoicePlan;
92
+ /**
93
+ * `voicePlan` の引数。呼ばれるのはプレイヤーだけで、観測席はプラットフォームが一律に扱う
94
+ * (全体の部屋に入り、入ったらミュート。聴く部屋は本人が選ぶ)。
95
+ */
96
+ type VoicePlanArgs<S> = {
97
+ state: S;
98
+ playerId: SeatId;
99
+ };
100
+ /**
101
+ * 一時停止。state ではない。
102
+ *
103
+ * 理由は 1 つだけ持つ。`waiting` (戻らない人を待つ自動中断) はまだ止める側が無いが、
104
+ * 読む側が先に知っておけるよう型に置く。理由は今後増えることがあるので、読む側は知らない
105
+ * `reason` でも止まっているものとして扱う (止める画面はアプリが描く)。
106
+ */
107
+ type Pause = null | {
108
+ reason: 'emergency';
109
+ by: SeatId;
110
+ } | {
111
+ reason: 'waiting';
112
+ for: SeatId[];
113
+ since: number;
114
+ };
115
+ //#endregion
58
116
  //#region ../engine-core/src/types.d.ts
59
117
  /**
60
118
  * roster に載る席。
@@ -276,6 +334,15 @@ type GameLogic<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerAction
276
334
  */
277
335
  deadlines?: Record<string, Deadline<S>>;
278
336
  tickRate?: number;
337
+ /**
338
+ * 通話の部屋とマイクの方針。state が変わるたびに、サーバーがプレイヤー 1 人ずつについて呼ぶ。
339
+ * 結果は presence に載って全員に配られ、各アプリがそれに合わせる。
340
+ *
341
+ * `deadlines` の `at` と同じく、state だけから決まる軽い純関数にすること。実時刻や乱数を
342
+ * 読むと、呼ばれるたびに答えが変わって部屋が暴れる。書かなければ全員が全体の部屋、
343
+ * マイクは本人の自由。投げたら直前の結果を使い続ける。
344
+ */
345
+ voicePlan?(args: VoicePlanArgs<S>): VoicePlan;
279
346
  };
280
347
  /** デフォルトのプレイヤーアイコン URL 一覧(dev / local モード用) */
281
348
  declare const DEFAULT_ICON_URLS: readonly ["https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/4d0da24d-bcf2-4f7b-d1a0-f1bb8c747300/original", "https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/8c75fccb-41d6-429d-e943-06c728a72a00/original", "https://imagedelivery.net/htp-D7B2hJT5XtdWYN9e7Q/43f45d11-da38-4d6e-637d-3df78e583500/original"];
@@ -286,8 +353,11 @@ type PlayScreenMessage = {
286
353
  type: string;
287
354
  [key: string]: unknown;
288
355
  };
289
- /** ブリッジメッセージのチャンネル */
290
- type BridgeChannel = 'sdk' | 'game';
356
+ /**
357
+ * ブリッジメッセージのチャンネル。`platform` はアプリとサーバーのあいだの伝言で、
358
+ * SDK は中身を見ずに WebSocket の `__platform` と行き来させる。
359
+ */
360
+ type BridgeChannel = 'sdk' | 'game' | 'platform';
291
361
  /** ワイヤーフォーマット: { channel, type, payload, playerId? } */
292
362
  type BridgeMessage = {
293
363
  channel: BridgeChannel;
@@ -317,15 +387,6 @@ type BridgeMessage = {
317
387
  * 自己申告なので権限の根拠にはならない。表示の分岐にだけ使うこと。
318
388
  */
319
389
  type SeatKind = 'player' | 'spectator' | 'admin';
320
- /** プレイヤーごとのリアルタイム状態 */
321
- type PlayerVoiceState = {
322
- /** 音声状態。音声通話に未接続の場合は null */
323
- audioStatus: 'speaking' | 'listening' | 'muted' | 'unstable' | null;
324
- };
325
- /** @deprecated 旧形式。新コードでは onPlayersChanged() と PlayerVoiceState を使用 */
326
- type PlayersChangedMessage = {
327
- players: Record<string, PlayerVoiceState>;
328
- };
329
390
  /**
330
391
  * events handler。
331
392
  *
@@ -382,12 +443,9 @@ type GameConfig<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerActio
382
443
  * iframe 内部の `window.innerWidth/Height` を保証する。
383
444
  */
384
445
  devMinIframeShortEdge?: number;
385
- } & ConnectionCallbacks;
386
- type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
387
- type ConnectionCallbacks = {
388
- /** WebSocket 接続状態が変化した時に呼ばれる */
389
- onConnectionStateChange?: (state: ConnectionState) => void;
390
446
  };
447
+ /** `ReconnectableWebSocket` の接続状態。シナリオの接続の状態は `getPresence().me.connection`。 */
448
+ type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
391
449
  //#endregion
392
450
  //#region src/host-globals.d.ts
393
451
  declare global {
@@ -692,4 +750,4 @@ type DevHooksCtx<S = unknown> = {
692
750
  declare const createDevHooks: <S>(ctx: DevHooksCtx<S>) => UzuDevHooks<S>;
693
751
  declare const attachDevHooks: <S>(ctx: DevHooksCtx<S>) => void;
694
752
  //#endregion
695
- export { plus as $, Deadline as A, ServerActionMap as B, SeatKind as C, ActionHandler as D, ActionContext as E, Seat as F, SetupArgs as G, ServerOnlyAction as H, SeededRandom as I, UpdateContext as J, SetupContext as K, ServerActionArgs as L, DeadlineContext as M, Emit as N, ActionMap as O, GameLogic as P, minus as Q, ServerActionContext as R, PlayersChangedMessage as S, ActionArgs as T, ServerOnlyActionContext as U, ServerEvent as V, ServerOnlyActionHandlerFn as W, GAME_START as X, Duration as Y, GameTime as Z, EventSubscription as _, createDevHooks as a, PlayScreenMessage as b, applyJsonMergePatch as c, getPredictionWarnings as d, sub as et, BridgeChannel as f, EventHandler as g, ConnectionState as h, attachDevHooks as i, DeadlineArgs as j, DEFAULT_ICON_URLS as k, applyJsonPatch as l, ConnectionCallbacks as m, RunHandle as n, JsonMergePatch as o, BridgeMessage as p, UpdateArgs as q, UzuDevHooks as r, JsonPatchOp as s, DevHooksCtx as t, PredictionWarning as u, GameConfig as v, SendAction as w, PlayerVoiceState as x, PayloadMap as y, ServerActionHandler as z };
753
+ export { GAME_START as $, Emit as A, ServerOnlyActionContext as B, ActionContext as C, Deadline as D, DEFAULT_ICON_URLS as E, ServerActionContext as F, UpdateContext as G, SetupArgs as H, ServerActionHandler as I, Pause as J, DEFAULT_VOICE_PLAN as K, ServerActionMap as L, Seat as M, SeededRandom as N, DeadlineArgs as O, ServerActionArgs as P, Duration as Q, ServerEvent as R, ActionArgs as S, ActionMap as T, SetupContext as U, ServerOnlyActionHandlerFn as V, UpdateArgs as W, VoicePlan as X, SeatId as Y, VoicePlanArgs as Z, GameConfig as _, createDevHooks as a, SeatKind as b, applyJsonMergePatch as c, getPredictionWarnings as d, GameTime as et, BridgeChannel as f, EventSubscription as g, EventHandler as h, attachDevHooks as i, GameLogic as j, DeadlineContext as k, applyJsonPatch as l, ConnectionState as m, RunHandle as n, plus as nt, JsonMergePatch as o, BridgeMessage as p, MicPolicy as q, UzuDevHooks as r, sub as rt, JsonPatchOp as s, DevHooksCtx as t, minus as tt, PredictionWarning as u, PayloadMap as v, ActionHandler as w, SendAction as x, PlayScreenMessage as y, ServerOnlyAction as z };