@uzuhq/code-cli 0.5.5 → 0.5.7
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 +58 -39
- package/dist/dev-server/server.js +1 -1
- package/dist/dev.js +18 -21
- package/dist/harness/client-entry.js +10 -6
- package/dist/harness/mount.js +71 -32
- 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
|
+
* roster の申告を player だけに落とす。 壊れた申告は null。
|
|
19
|
+
*
|
|
20
|
+
* 観測者は roster に載らない。 旧ホストは観測席も載せてくるので、 配役に混ざらないよう
|
|
21
|
+
* ここで落とす。
|
|
22
|
+
*/
|
|
23
|
+
function parseRoster(rosterParam) {
|
|
24
|
+
if (!rosterParam)
|
|
25
|
+
return null;
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(rosterParam);
|
|
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,17 @@ 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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
// 移行期: 旧 SDK を同梱したシナリオは `seats=` を送り続ける。
|
|
282
|
+
const declared = parseRoster(url.searchParams.get('players') ?? url.searchParams.get('seats'));
|
|
283
|
+
if (declared && declared.length > 0) {
|
|
284
|
+
this.players = declared;
|
|
285
|
+
console.log(`[GameRoom] 📋 Roster pinned: ${this.players.map((p) => p.id).join(', ')}`);
|
|
264
286
|
}
|
|
265
287
|
}
|
|
266
288
|
console.log(`[GameRoom] 🔗 New connection: connectionId=${connectionId} playerId=${playerId} nickname=${nickname}`);
|
|
@@ -576,10 +598,7 @@ export class GameRoom {
|
|
|
576
598
|
this.seed = opts.seed;
|
|
577
599
|
}
|
|
578
600
|
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
|
-
});
|
|
601
|
+
this.gameState = this.logic.setup(this.setupArgs(this.random));
|
|
583
602
|
this.tickCount = 0;
|
|
584
603
|
// reset は新規ゲーム = 実行可能状態を意味するので pause も解除する。
|
|
585
604
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. child stdout/stderr を line-by-line で監視し、 `dev.readyPattern` (default vite 向け)
|
|
7
7
|
* の regex match をトリガに scenario dev server の URL を確定
|
|
8
8
|
* 3. harness http+WebSocket server を立て、 iframe grid + HUD を親 frame に描画
|
|
9
|
-
* 4. `?server=ws://localhost:<port>&roomId=<key>&seatId=dev_N&
|
|
9
|
+
* 4. `?server=ws://localhost:<port>&roomId=<key>&seatId=dev_N&players=<json>&revisionId=dev`
|
|
10
10
|
* を子 iframe URL に組み立てて mount。 SDK は既存 online mode でそのまま接続する
|
|
11
11
|
* 5. `window.__uzu_dev` は harness page が dev-server の管理 channel と WS で会話して expose
|
|
12
12
|
*/
|
|
@@ -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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dev harness の iframe grid を parent frame に描画する。
|
|
3
3
|
*
|
|
4
4
|
* 子 iframe URL には `?server=ws://localhost:<port>&roomId=<key>&seatId=dev_N&
|
|
5
|
-
*
|
|
5
|
+
* players=<json>&revisionId=dev` を付ける。 SDK は既存 online mode で dev-server に
|
|
6
6
|
* 接続する (本番 Cloudflare Worker と同じ shape)。
|
|
7
7
|
*/
|
|
8
8
|
import qrcode from 'qrcode-generator';
|
|
@@ -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,77 @@ 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 の `?players=` にも渡らない。
|
|
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
|
+
// 移行期: 手元の scenario が同梱する SDK が旧版 (0.7.3 等) だと `?seats=` が無いだけで
|
|
333
|
+
// run() の入口が throw する。 旧名も併せて載せる。
|
|
334
|
+
const roster = JSON.stringify(opts.players.map(toWireSeat));
|
|
335
|
+
url.searchParams.set('players', roster);
|
|
336
|
+
url.searchParams.set('seats', roster);
|
|
337
|
+
// 自分の席種別。 観測席で GM ビューと観戦ビューを出し分けるのに要る (state からは
|
|
338
|
+
// 導けない)。 iframe URL 限定で WebSocket には出さない。
|
|
339
|
+
url.searchParams.set('seatKind', pane.seatKind);
|
|
302
340
|
url.searchParams.set('revisionId', opts.revisionId);
|
|
303
341
|
// SDK は解釈しないが、 scenario 側で「parent は harness page (audio consumer 無し) か」を
|
|
304
342
|
// 判定するマーカーとして残す (tenbo の __uzuAudio bridge スキップ判定など)。
|
|
305
343
|
url.searchParams.set('__dev_player', String(playerIndex));
|
|
306
344
|
url.searchParams.set('__dev_room', opts.roomKey);
|
|
307
|
-
if (
|
|
308
|
-
//
|
|
345
|
+
if (pane.seatKind === 'admin') {
|
|
346
|
+
// GM 席は本番 overlay chrome を載せないため避け領域も不要。
|
|
309
347
|
url.searchParams.set('uzuHudInsetX', '0');
|
|
310
348
|
url.searchParams.set('uzuHudInsetY', '0');
|
|
311
349
|
return url.toString();
|
|
@@ -403,11 +441,11 @@ function openPlayerSwitcher(opts, currentIndex, anchor) {
|
|
|
403
441
|
heading.textContent = '画面を切り替え';
|
|
404
442
|
heading.style.cssText = 'font-size:11px;color:#a0a8b8;padding:2px 8px 6px;';
|
|
405
443
|
panel.appendChild(heading);
|
|
406
|
-
for (let i = 0; i < opts
|
|
407
|
-
const
|
|
444
|
+
for (let i = 0; i < paneCount(opts); i++) {
|
|
445
|
+
const pane = paneAt(opts, i);
|
|
408
446
|
const isCurrent = i === currentIndex;
|
|
409
447
|
const row = document.createElement('button');
|
|
410
|
-
row.textContent = `${isCurrent ? '● ' : ''}${
|
|
448
|
+
row.textContent = `${isCurrent ? '● ' : ''}${pane.nickname} (${pane.id})`;
|
|
411
449
|
row.style.cssText = [
|
|
412
450
|
'text-align:left;border:none;border-radius:6px',
|
|
413
451
|
'padding:10px 12px;font-size:14px;cursor:pointer',
|
|
@@ -467,7 +505,7 @@ onUzuClick) {
|
|
|
467
505
|
'flex-shrink:0',
|
|
468
506
|
].join(';');
|
|
469
507
|
const mobileUrl = resolveMobileUrl(opts, playerIndex);
|
|
470
|
-
const label =
|
|
508
|
+
const label = paneAt(opts, playerIndex).nickname;
|
|
471
509
|
if (onUzuClick) {
|
|
472
510
|
uzu.onclick = onUzuClick;
|
|
473
511
|
}
|
|
@@ -476,7 +514,7 @@ onUzuClick) {
|
|
|
476
514
|
stateGetter: opts.stateGetter,
|
|
477
515
|
resetGame: opts.resetGame,
|
|
478
516
|
playerCount: opts.playerCount,
|
|
479
|
-
adminEnabled: opts.
|
|
517
|
+
adminEnabled: opts.admin,
|
|
480
518
|
onOpenSingleView: () => {
|
|
481
519
|
window.open(singleViewUrl(playerIndex), '_blank', 'noopener');
|
|
482
520
|
},
|
|
@@ -585,17 +623,18 @@ export function mountIframeGrid(opts) {
|
|
|
585
623
|
iframeSplashes.clear();
|
|
586
624
|
iframePredictionWarns.clear();
|
|
587
625
|
notchAppliers.length = 0;
|
|
588
|
-
for (let i = 0; i < opts
|
|
589
|
-
const
|
|
626
|
+
for (let i = 0; i < paneCount(opts); i++) {
|
|
627
|
+
const pane = paneAt(opts, i);
|
|
590
628
|
const cell = document.createElement('div');
|
|
591
629
|
const label = document.createElement('div');
|
|
592
|
-
//
|
|
593
|
-
const labelColor =
|
|
630
|
+
// GM 席は本番に存在しない dev 専用席なので、 player 席と見分く別配色にする。
|
|
631
|
+
const labelColor = pane.seatKind === 'admin' ? '#ffb86c' : '#8be9fd';
|
|
594
632
|
label.style.cssText = `color:${labelColor};font-size:12px;padding:2px 0;flex-shrink:0;background:#16213e;text-align:center;`;
|
|
595
633
|
const nameRow = document.createElement('div');
|
|
596
|
-
nameRow.textContent =
|
|
597
|
-
|
|
598
|
-
|
|
634
|
+
nameRow.textContent =
|
|
635
|
+
pane.seatKind === 'player'
|
|
636
|
+
? `Player ${pane.playerNo} (${pane.id})`
|
|
637
|
+
: `${pane.seatKind === 'admin' ? '⚙' : '👁'} ${pane.nickname} (${pane.id})`;
|
|
599
638
|
label.appendChild(nameRow);
|
|
600
639
|
const statusRow = createLifecycleStatusRow();
|
|
601
640
|
label.appendChild(statusRow.el);
|
|
@@ -624,8 +663,8 @@ export function mountIframeGrid(opts) {
|
|
|
624
663
|
splash.textContent = 'waiting firstFrameReady…';
|
|
625
664
|
screen.appendChild(iframe);
|
|
626
665
|
screen.appendChild(splash);
|
|
627
|
-
//
|
|
628
|
-
if (
|
|
666
|
+
// GM 席は端末を模す必要が無いので overlay chrome / 擬似ノッチを載せず素の画面にする。
|
|
667
|
+
if (pane.seatKind !== 'admin') {
|
|
629
668
|
screen.appendChild(createCellChrome(opts, i, false, {
|
|
630
669
|
toggle: toggleNotchSim,
|
|
631
670
|
register: (apply) => {
|
|
@@ -655,7 +694,7 @@ export function mountIframeGrid(opts) {
|
|
|
655
694
|
});
|
|
656
695
|
// 擬似ノッチ ON/OFF を screen の inset だけで切り替える (iframe は再生成しない)。
|
|
657
696
|
const applyNotch = (on) => {
|
|
658
|
-
if (
|
|
697
|
+
if (pane.seatKind === 'admin')
|
|
659
698
|
on = false;
|
|
660
699
|
if (on) {
|
|
661
700
|
screen.style.top = `${SIM_NOTCH.top}px`;
|