@uzuhq/code-cli 0.5.5 → 0.5.6
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/dev-server/game-room.js +57 -39
- package/dist/dev-server/server.js +1 -1
- package/dist/dev.js +17 -20
- package/dist/harness/client-entry.js +10 -6
- package/dist/harness/mount.js +66 -31
- package/package.json +1 -1
|
@@ -14,6 +14,33 @@
|
|
|
14
14
|
import { randomUUID } from 'crypto';
|
|
15
15
|
import { compare, applyJsonMergePatch, applyJsonPatch } from './json-patch.js';
|
|
16
16
|
import { SeededRandomImpl } from './random.js';
|
|
17
|
+
/**
|
|
18
|
+
* `?seats=` を player だけの roster に落とす。 壊れた申告は null。
|
|
19
|
+
*
|
|
20
|
+
* 観測者は roster に載らない。 旧ホストは観測席も載せてくるので、 配役に混ざらないよう
|
|
21
|
+
* ここで落とす。
|
|
22
|
+
*/
|
|
23
|
+
function parseRoster(seatsParam) {
|
|
24
|
+
if (!seatsParam)
|
|
25
|
+
return null;
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(seatsParam);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(raw))
|
|
34
|
+
return null;
|
|
35
|
+
return raw
|
|
36
|
+
.filter((p) => (p.kind ?? 'player') === 'player')
|
|
37
|
+
.map((p) => ({
|
|
38
|
+
id: p.id,
|
|
39
|
+
nickname: p.name ?? 'Guest',
|
|
40
|
+
iconUrl: p.iconUrl ?? '',
|
|
41
|
+
characterId: p.characterId,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
17
44
|
/**
|
|
18
45
|
* serverOnly() で wrap された handler かを判定する (SDK の isServerOnlyAction と同じ brand)。
|
|
19
46
|
* dev-server は SDK を import できないため実装を持つ。
|
|
@@ -36,14 +63,14 @@ export class GameRoom {
|
|
|
36
63
|
tickPaused = false;
|
|
37
64
|
playerInputs = {};
|
|
38
65
|
/**
|
|
39
|
-
* roster
|
|
66
|
+
* roster。 配役を受け取る player 席のみで、 観測席 (GM 席・観戦席) は載らない。
|
|
40
67
|
* dev harness では manifest から組んだ roster を constructor で注入する
|
|
41
68
|
* (server 権威)。 その場合、 接続クエリの roster 申告は一切採用しないので、
|
|
42
69
|
* 旧世代 harness page の残タブが reconnect しても roster を汚染できない。
|
|
43
70
|
* 本番 DO は backend 由来の roster を全 client が同一申告するため接続時
|
|
44
71
|
* 登録で成立している — 権威が platform 側にある点は同じ。
|
|
45
72
|
*/
|
|
46
|
-
|
|
73
|
+
players = [];
|
|
47
74
|
seq = 0;
|
|
48
75
|
prevBroadcastState = null;
|
|
49
76
|
static SNAPSHOT_INTERVAL = 20;
|
|
@@ -51,14 +78,12 @@ export class GameRoom {
|
|
|
51
78
|
attachments = new WeakMap();
|
|
52
79
|
snapshotSubscribers = new Set();
|
|
53
80
|
eventSubscribers = new Set();
|
|
54
|
-
constructor(logic,
|
|
81
|
+
constructor(logic, players) {
|
|
55
82
|
this.logic = logic;
|
|
56
83
|
this.tickRate = logic.tickRate ?? 0;
|
|
57
|
-
if (
|
|
58
|
-
this.
|
|
59
|
-
console.log(`[GameRoom] 📋
|
|
60
|
-
.map((p) => `${p.id}(${p.kind ?? 'player'})`)
|
|
61
|
-
.join(', ')}`);
|
|
84
|
+
if (players && players.length > 0) {
|
|
85
|
+
this.players = players;
|
|
86
|
+
console.log(`[GameRoom] 📋 Roster (server-authoritative): ${players.map((p) => p.id).join(', ')}`);
|
|
62
87
|
}
|
|
63
88
|
}
|
|
64
89
|
// ─── Broadcast ─────────────────────────────────────────
|
|
@@ -153,22 +178,32 @@ export class GameRoom {
|
|
|
153
178
|
});
|
|
154
179
|
}
|
|
155
180
|
// ─── Game Lifecycle ────────────────────────────────────
|
|
181
|
+
/**
|
|
182
|
+
* 移行期: 手元のシナリオが `setup({ seats })` のままでも `uzu dev` を動かせるよう、
|
|
183
|
+
* 同じ配列を旧名でも渡す。 本番 (play-server) が publish 済み logic.js のために
|
|
184
|
+
* 同じことをしているのと揃えている。 `SetupArgs` に `seats` を宣言しないのは、
|
|
185
|
+
* 新規シナリオに旧名を選ばせないため。
|
|
186
|
+
*/
|
|
187
|
+
setupArgs(random) {
|
|
188
|
+
return {
|
|
189
|
+
players: this.players,
|
|
190
|
+
seats: this.players,
|
|
191
|
+
ctx: { random, now: Date.now() },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
156
194
|
maybeStartGame() {
|
|
157
195
|
if (this.gameState !== null)
|
|
158
196
|
return;
|
|
159
|
-
if (this.
|
|
197
|
+
if (this.players.length === 0)
|
|
160
198
|
return;
|
|
161
199
|
this.seed = Date.now() & 0xffffffff;
|
|
162
200
|
this.random = new SeededRandomImpl(this.seed);
|
|
163
|
-
this.gameState = this.logic.setup(
|
|
164
|
-
seats: this.seats,
|
|
165
|
-
ctx: { random: this.random, now: Date.now() },
|
|
166
|
-
});
|
|
201
|
+
this.gameState = this.logic.setup(this.setupArgs(this.random));
|
|
167
202
|
this.stateInitialized = true;
|
|
168
203
|
this.tickCount = 0;
|
|
169
204
|
this.seq = 0;
|
|
170
205
|
this.prevBroadcastState = structuredClone(this.gameState);
|
|
171
|
-
console.log(`[GameRoom] ✅ Game started with ${this.
|
|
206
|
+
console.log(`[GameRoom] ✅ Game started with ${this.players.length} players`);
|
|
172
207
|
this.broadcastAll({
|
|
173
208
|
type: '__game_start',
|
|
174
209
|
state: this.gameState,
|
|
@@ -237,30 +272,16 @@ export class GameRoom {
|
|
|
237
272
|
}
|
|
238
273
|
const nickname = url.searchParams.get('nickname') ?? 'Guest';
|
|
239
274
|
const connectionId = randomUUID();
|
|
240
|
-
|
|
275
|
+
// roster に自席が居なくても弾かない。 観測席 (GM 席・観戦席) は roster に載らない
|
|
276
|
+
// まま接続してくる。 シナリオ側は state.players の空振りで観測者ビューを返す。
|
|
277
|
+
if (this.players.length === 0) {
|
|
241
278
|
// constructor 注入 (server 権威) が無い場合のみ、 接続クエリの申告 roster に
|
|
242
279
|
// fallback する (本番 DO と同じ経路)。 dev harness では常に注入済みなので
|
|
243
280
|
// ここは通らない。
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (Array.isArray(raw)) {
|
|
249
|
-
this.seats = raw.map((p) => ({
|
|
250
|
-
id: p.id,
|
|
251
|
-
nickname: p.name ?? 'Guest',
|
|
252
|
-
iconUrl: p.iconUrl ?? '',
|
|
253
|
-
characterId: p.characterId,
|
|
254
|
-
kind: p.kind,
|
|
255
|
-
}));
|
|
256
|
-
console.log(`[GameRoom] 📋 Seats registered: ${this.seats
|
|
257
|
-
.map((p) => `${p.id}(${p.kind})`)
|
|
258
|
-
.join(', ')}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
/* ignore */
|
|
263
|
-
}
|
|
281
|
+
const declared = parseRoster(url.searchParams.get('seats'));
|
|
282
|
+
if (declared && declared.length > 0) {
|
|
283
|
+
this.players = declared;
|
|
284
|
+
console.log(`[GameRoom] 📋 Roster pinned: ${this.players.map((p) => p.id).join(', ')}`);
|
|
264
285
|
}
|
|
265
286
|
}
|
|
266
287
|
console.log(`[GameRoom] 🔗 New connection: connectionId=${connectionId} playerId=${playerId} nickname=${nickname}`);
|
|
@@ -576,10 +597,7 @@ export class GameRoom {
|
|
|
576
597
|
this.seed = opts.seed;
|
|
577
598
|
}
|
|
578
599
|
this.random = new SeededRandomImpl(this.seed);
|
|
579
|
-
this.gameState = this.logic.setup(
|
|
580
|
-
seats: this.seats,
|
|
581
|
-
ctx: { random: this.random, now: Date.now() },
|
|
582
|
-
});
|
|
600
|
+
this.gameState = this.logic.setup(this.setupArgs(this.random));
|
|
583
601
|
this.tickCount = 0;
|
|
584
602
|
// reset は新規ゲーム = 実行可能状態を意味するので pause も解除する。
|
|
585
603
|
this.tickPaused = false;
|
|
@@ -55,7 +55,7 @@ export function startHarnessServer(opts) {
|
|
|
55
55
|
const key = `${revisionId}/${roomId}`;
|
|
56
56
|
let room = gameRooms.get(key);
|
|
57
57
|
if (!room) {
|
|
58
|
-
room = new GameRoom(opts.logic, opts.meta.
|
|
58
|
+
room = new GameRoom(opts.logic, opts.meta.players);
|
|
59
59
|
gameRooms.set(key, room);
|
|
60
60
|
}
|
|
61
61
|
room.handleConnection(ws, url);
|
package/dist/dev.js
CHANGED
|
@@ -77,27 +77,24 @@ export async function runDevCommand() {
|
|
|
77
77
|
const roomKey = 'devroom';
|
|
78
78
|
const harnessPort = await findFreePort();
|
|
79
79
|
const lanHosts = currentLanHosts();
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
: []),
|
|
92
|
-
...(manifest.admin
|
|
93
|
-
? [{ id: 'admin_0', nickname: 'Admin', iconUrl: '', kind: 'admin' }]
|
|
94
|
-
: []),
|
|
95
|
-
];
|
|
80
|
+
// roster をここで一度だけ組む。 grid の player セル・iframe URL・GameRoom 注入の単一の源。
|
|
81
|
+
const players = Array.from({ length: playerCount }, (_, i) => ({
|
|
82
|
+
id: `dev_${i}`,
|
|
83
|
+
nickname: manifest.characters?.[i]?.name ?? `Player ${i + 1}`,
|
|
84
|
+
iconUrl: manifest.characters?.[i]?.icon ?? '',
|
|
85
|
+
characterId: manifest.characters?.[i]?.id,
|
|
86
|
+
}));
|
|
87
|
+
// 観測席は roster に載せない。 harness が pane を出すだけで、 scenario からは
|
|
88
|
+
// state.players に居ない席として見える。
|
|
89
|
+
const admin = manifest.admin ?? false;
|
|
90
|
+
const spectator = manifest.spectator ?? false;
|
|
96
91
|
const meta = {
|
|
97
92
|
scenarioUrl,
|
|
98
93
|
playerCount,
|
|
99
94
|
orientation,
|
|
100
|
-
|
|
95
|
+
players,
|
|
96
|
+
admin,
|
|
97
|
+
spectator,
|
|
101
98
|
roomKey,
|
|
102
99
|
serverBaseUrl: `ws://localhost:${harnessPort}`,
|
|
103
100
|
revisionId: 'dev',
|
|
@@ -117,9 +114,9 @@ export async function runDevCommand() {
|
|
|
117
114
|
console.log(` 🎮 UZU dev harness: ${harnessUrl}`);
|
|
118
115
|
console.log(` scenario: ${scenarioUrl}`);
|
|
119
116
|
console.log(` players: ${playerCount} orientation: ${orientation} roomKey: ${roomKey}`);
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
console.log(`
|
|
117
|
+
const observerIds = [...(spectator ? ['spec_0'] : []), ...(admin ? ['admin_0'] : [])];
|
|
118
|
+
if (observerIds.length > 0) {
|
|
119
|
+
console.log(` observer seats (roster 外): ${observerIds.join(', ')}`);
|
|
123
120
|
}
|
|
124
121
|
if (lanHosts.length) {
|
|
125
122
|
console.log('');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `window.__uzu_dev` に expose する。 esbuild で IIFE bundle 済み文字列として
|
|
6
6
|
* dev-server が `/_uzu_harness.js` から serve する。
|
|
7
7
|
*/
|
|
8
|
-
import { mountIframeGrid, mountSinglePlayer } from './mount.js';
|
|
8
|
+
import { mountIframeGrid, mountSinglePlayer, paneCount } from './mount.js';
|
|
9
9
|
import { attachAdminClient } from './admin-client.js';
|
|
10
10
|
/**
|
|
11
11
|
* meta の URL は CLI 視点 (localhost) で書かれている。 スマホ実機など
|
|
@@ -40,10 +40,10 @@ async function main() {
|
|
|
40
40
|
// admin channel は grid (親frame) だけが張るので、 単体表示では dev メニューを出さない。
|
|
41
41
|
const playerParam = new URLSearchParams(location.search).get('player');
|
|
42
42
|
const playerIndex = playerParam == null ? null : Number(playerParam);
|
|
43
|
-
const
|
|
43
|
+
const panes = paneCount(meta);
|
|
44
44
|
if (playerIndex != null && Number.isInteger(playerIndex)) {
|
|
45
|
-
if (playerIndex < 0 || playerIndex >=
|
|
46
|
-
document.body.textContent = `player は 0〜${
|
|
45
|
+
if (playerIndex < 0 || playerIndex >= panes) {
|
|
46
|
+
document.body.textContent = `player は 0〜${panes - 1} で指定してください`;
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
mountSinglePlayer({
|
|
@@ -55,7 +55,9 @@ async function main() {
|
|
|
55
55
|
serverBaseUrl: meta.serverBaseUrl,
|
|
56
56
|
revisionId: meta.revisionId,
|
|
57
57
|
roomKey: meta.roomKey,
|
|
58
|
-
|
|
58
|
+
players: meta.players,
|
|
59
|
+
admin: meta.admin,
|
|
60
|
+
spectator: meta.spectator,
|
|
59
61
|
}, playerIndex);
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
@@ -79,7 +81,9 @@ async function main() {
|
|
|
79
81
|
serverBaseUrl: meta.serverBaseUrl,
|
|
80
82
|
revisionId: meta.revisionId,
|
|
81
83
|
roomKey: meta.roomKey,
|
|
82
|
-
|
|
84
|
+
players: meta.players,
|
|
85
|
+
admin: meta.admin,
|
|
86
|
+
spectator: meta.spectator,
|
|
83
87
|
// 実機リンク用 host は rewrite 前の meta から取る (localhost 視点の LAN/mDNS 一覧)。
|
|
84
88
|
lanHosts: rawMeta.lanHosts,
|
|
85
89
|
// 各 cell の UZU ボタンに配線する dev メニュー。
|
package/dist/harness/mount.js
CHANGED
|
@@ -161,9 +161,9 @@ export function mountSinglePlayer(opts, playerIndex) {
|
|
|
161
161
|
splash.textContent = 'waiting firstFrameReady…';
|
|
162
162
|
opts.target.appendChild(iframe);
|
|
163
163
|
opts.target.appendChild(splash);
|
|
164
|
-
// 実機でも本番アプリと同じ overlay chrome を出す (ハリボテ)。
|
|
164
|
+
// 実機でも本番アプリと同じ overlay chrome を出す (ハリボテ)。 GM 席は端末を
|
|
165
165
|
// 模す必要が無いので載せない。 UZU ボタンだけは「他プレイヤー画面へ切替」に配線する。
|
|
166
|
-
if (
|
|
166
|
+
if (paneAt(opts, playerIndex).seatKind !== 'admin') {
|
|
167
167
|
const chrome = createCellChrome(opts, playerIndex, true, undefined, () => {
|
|
168
168
|
const uzuBtn = chrome.querySelector('button');
|
|
169
169
|
if (uzuBtn)
|
|
@@ -273,39 +273,73 @@ function createLifecycleStatusRow() {
|
|
|
273
273
|
row.appendChild(predictionWarn);
|
|
274
274
|
return { el: row, dots, predictionWarn };
|
|
275
275
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
/**
|
|
277
|
+
* roster に載らない観測席の pane。 席の中身は固定なので manifest の宣言から起こす。
|
|
278
|
+
* grid の中だけの存在で、 GameRoom にも iframe の `?seats=` にも渡らない。
|
|
279
|
+
*/
|
|
280
|
+
function observerPanes(opts) {
|
|
281
|
+
const pane = (id, nickname, seatKind) => ({
|
|
282
|
+
id,
|
|
283
|
+
nickname,
|
|
284
|
+
seatKind,
|
|
285
|
+
playerNo: null,
|
|
286
|
+
});
|
|
287
|
+
return [
|
|
288
|
+
...(opts.spectator ? [pane('spec_0', '観戦', 'spectator')] : []),
|
|
289
|
+
...(opts.admin ? [pane('admin_0', 'Admin', 'admin')] : []),
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
export function paneCount(opts) {
|
|
293
|
+
return opts.players.length + observerPanes(opts).length;
|
|
281
294
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
295
|
+
function paneAt(opts, index) {
|
|
296
|
+
const player = opts.players[index];
|
|
297
|
+
if (player) {
|
|
298
|
+
return {
|
|
299
|
+
id: player.id,
|
|
300
|
+
nickname: player.nickname,
|
|
301
|
+
seatKind: 'player',
|
|
302
|
+
playerNo: index + 1,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const observer = observerPanes(opts)[index - opts.players.length];
|
|
306
|
+
if (!observer)
|
|
307
|
+
throw new Error(`pane index out of range: ${index}`);
|
|
308
|
+
return observer;
|
|
284
309
|
}
|
|
285
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* SDK が読む wire 形式 (nickname → name)。
|
|
312
|
+
*
|
|
313
|
+
* `kind` は移行期の互換措置。 旧 SDK (0.7.3 等) は `kind` の無い席を受け取ると
|
|
314
|
+
* throw する。 roster は player だけになったので値は常に 'player'。
|
|
315
|
+
*/
|
|
286
316
|
function toWireSeat(seat) {
|
|
287
317
|
return {
|
|
288
318
|
id: seat.id,
|
|
289
319
|
name: seat.nickname,
|
|
290
320
|
iconUrl: seat.iconUrl,
|
|
291
321
|
characterId: seat.characterId,
|
|
292
|
-
kind:
|
|
322
|
+
kind: 'player',
|
|
293
323
|
};
|
|
294
324
|
}
|
|
295
325
|
function buildIframeUrl(opts, playerIndex) {
|
|
296
326
|
const url = new URL(opts.scenarioUrl);
|
|
297
|
-
const
|
|
327
|
+
const pane = paneAt(opts, playerIndex);
|
|
298
328
|
url.searchParams.set('server', opts.serverBaseUrl);
|
|
299
329
|
url.searchParams.set('roomId', opts.roomKey);
|
|
300
|
-
url.searchParams.set('seatId',
|
|
301
|
-
|
|
330
|
+
url.searchParams.set('seatId', pane.id);
|
|
331
|
+
// roster は player 席だけ。 観測席の iframe も同じ roster を送る (自分は載らない)。
|
|
332
|
+
url.searchParams.set('seats', JSON.stringify(opts.players.map(toWireSeat)));
|
|
333
|
+
// 自分の席種別。 観測席で GM ビューと観戦ビューを出し分けるのに要る (state からは
|
|
334
|
+
// 導けない)。 iframe URL 限定で WebSocket には出さない。
|
|
335
|
+
url.searchParams.set('seatKind', pane.seatKind);
|
|
302
336
|
url.searchParams.set('revisionId', opts.revisionId);
|
|
303
337
|
// SDK は解釈しないが、 scenario 側で「parent は harness page (audio consumer 無し) か」を
|
|
304
338
|
// 判定するマーカーとして残す (tenbo の __uzuAudio bridge スキップ判定など)。
|
|
305
339
|
url.searchParams.set('__dev_player', String(playerIndex));
|
|
306
340
|
url.searchParams.set('__dev_room', opts.roomKey);
|
|
307
|
-
if (
|
|
308
|
-
//
|
|
341
|
+
if (pane.seatKind === 'admin') {
|
|
342
|
+
// GM 席は本番 overlay chrome を載せないため避け領域も不要。
|
|
309
343
|
url.searchParams.set('uzuHudInsetX', '0');
|
|
310
344
|
url.searchParams.set('uzuHudInsetY', '0');
|
|
311
345
|
return url.toString();
|
|
@@ -403,11 +437,11 @@ function openPlayerSwitcher(opts, currentIndex, anchor) {
|
|
|
403
437
|
heading.textContent = '画面を切り替え';
|
|
404
438
|
heading.style.cssText = 'font-size:11px;color:#a0a8b8;padding:2px 8px 6px;';
|
|
405
439
|
panel.appendChild(heading);
|
|
406
|
-
for (let i = 0; i < opts
|
|
407
|
-
const
|
|
440
|
+
for (let i = 0; i < paneCount(opts); i++) {
|
|
441
|
+
const pane = paneAt(opts, i);
|
|
408
442
|
const isCurrent = i === currentIndex;
|
|
409
443
|
const row = document.createElement('button');
|
|
410
|
-
row.textContent = `${isCurrent ? '● ' : ''}${
|
|
444
|
+
row.textContent = `${isCurrent ? '● ' : ''}${pane.nickname} (${pane.id})`;
|
|
411
445
|
row.style.cssText = [
|
|
412
446
|
'text-align:left;border:none;border-radius:6px',
|
|
413
447
|
'padding:10px 12px;font-size:14px;cursor:pointer',
|
|
@@ -467,7 +501,7 @@ onUzuClick) {
|
|
|
467
501
|
'flex-shrink:0',
|
|
468
502
|
].join(';');
|
|
469
503
|
const mobileUrl = resolveMobileUrl(opts, playerIndex);
|
|
470
|
-
const label =
|
|
504
|
+
const label = paneAt(opts, playerIndex).nickname;
|
|
471
505
|
if (onUzuClick) {
|
|
472
506
|
uzu.onclick = onUzuClick;
|
|
473
507
|
}
|
|
@@ -476,7 +510,7 @@ onUzuClick) {
|
|
|
476
510
|
stateGetter: opts.stateGetter,
|
|
477
511
|
resetGame: opts.resetGame,
|
|
478
512
|
playerCount: opts.playerCount,
|
|
479
|
-
adminEnabled: opts.
|
|
513
|
+
adminEnabled: opts.admin,
|
|
480
514
|
onOpenSingleView: () => {
|
|
481
515
|
window.open(singleViewUrl(playerIndex), '_blank', 'noopener');
|
|
482
516
|
},
|
|
@@ -585,17 +619,18 @@ export function mountIframeGrid(opts) {
|
|
|
585
619
|
iframeSplashes.clear();
|
|
586
620
|
iframePredictionWarns.clear();
|
|
587
621
|
notchAppliers.length = 0;
|
|
588
|
-
for (let i = 0; i < opts
|
|
589
|
-
const
|
|
622
|
+
for (let i = 0; i < paneCount(opts); i++) {
|
|
623
|
+
const pane = paneAt(opts, i);
|
|
590
624
|
const cell = document.createElement('div');
|
|
591
625
|
const label = document.createElement('div');
|
|
592
|
-
//
|
|
593
|
-
const labelColor =
|
|
626
|
+
// GM 席は本番に存在しない dev 専用席なので、 player 席と見分く別配色にする。
|
|
627
|
+
const labelColor = pane.seatKind === 'admin' ? '#ffb86c' : '#8be9fd';
|
|
594
628
|
label.style.cssText = `color:${labelColor};font-size:12px;padding:2px 0;flex-shrink:0;background:#16213e;text-align:center;`;
|
|
595
629
|
const nameRow = document.createElement('div');
|
|
596
|
-
nameRow.textContent =
|
|
597
|
-
|
|
598
|
-
|
|
630
|
+
nameRow.textContent =
|
|
631
|
+
pane.seatKind === 'player'
|
|
632
|
+
? `Player ${pane.playerNo} (${pane.id})`
|
|
633
|
+
: `${pane.seatKind === 'admin' ? '⚙' : '👁'} ${pane.nickname} (${pane.id})`;
|
|
599
634
|
label.appendChild(nameRow);
|
|
600
635
|
const statusRow = createLifecycleStatusRow();
|
|
601
636
|
label.appendChild(statusRow.el);
|
|
@@ -624,8 +659,8 @@ export function mountIframeGrid(opts) {
|
|
|
624
659
|
splash.textContent = 'waiting firstFrameReady…';
|
|
625
660
|
screen.appendChild(iframe);
|
|
626
661
|
screen.appendChild(splash);
|
|
627
|
-
//
|
|
628
|
-
if (
|
|
662
|
+
// GM 席は端末を模す必要が無いので overlay chrome / 擬似ノッチを載せず素の画面にする。
|
|
663
|
+
if (pane.seatKind !== 'admin') {
|
|
629
664
|
screen.appendChild(createCellChrome(opts, i, false, {
|
|
630
665
|
toggle: toggleNotchSim,
|
|
631
666
|
register: (apply) => {
|
|
@@ -655,7 +690,7 @@ export function mountIframeGrid(opts) {
|
|
|
655
690
|
});
|
|
656
691
|
// 擬似ノッチ ON/OFF を screen の inset だけで切り替える (iframe は再生成しない)。
|
|
657
692
|
const applyNotch = (on) => {
|
|
658
|
-
if (
|
|
693
|
+
if (pane.seatKind === 'admin')
|
|
659
694
|
on = false;
|
|
660
695
|
if (on) {
|
|
661
696
|
screen.style.top = `${SIM_NOTCH.top}px`;
|