@uzuhq/code-cli 0.5.7 → 0.5.9

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.
@@ -1,78 +1,2757 @@
1
- /**
2
- * harness client bundle entry。
3
- *
4
- * `/_uzu_meta.json` を fetch → iframe grid mount + HUD 表示 + admin channel client を
5
- * `window.__uzu_dev` に expose する。 esbuild で IIFE bundle 済み文字列として
6
- * dev-server が `/_uzu_harness.js` から serve する。
7
- */
8
- import { mountIframeGrid, mountSinglePlayer, paneCount } from './mount.js';
9
- import { attachAdminClient } from './admin-client.js';
10
- /**
11
- * meta の URL は CLI 視点 (localhost) で書かれている。 スマホ実機など
12
- * localhost 以外の host で harness を開いたときは、 アクセスに使った
13
- * hostname へ読み替える (port はそのまま)。 harness server 自体は
14
- * 同一 origin なので location.host を使う。
15
- */
16
- function rewriteHostForViewer(meta) {
17
- const isLocal = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
18
- if (isLocal) {
19
- // PC (localhost) は従来どおり scenario dev server (vite) へ直接アクセスする
20
- return { ...meta, serverBaseUrl: `ws://${location.host}` };
21
- }
22
- // 実機 (LAN IP / mDNS 名) からは harness server の scenario proxy を経由する。
23
- // vite には harness から localhost 宛で届くため、 scenario 側の vite 設定
24
- // (--host / allowedHosts) を一切要求しない。
1
+ // ../../node_modules/.pnpm/qrcode-generator@2.0.4/node_modules/qrcode-generator/dist/qrcode.mjs
2
+ var qrcode = function(typeNumber, errorCorrectionLevel) {
3
+ const PAD0 = 236;
4
+ const PAD1 = 17;
5
+ let _typeNumber = typeNumber;
6
+ const _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
7
+ let _modules = null;
8
+ let _moduleCount = 0;
9
+ let _dataCache = null;
10
+ const _dataList = [];
11
+ const _this = {};
12
+ const makeImpl = function(test, maskPattern) {
13
+ _moduleCount = _typeNumber * 4 + 17;
14
+ _modules = (function(moduleCount) {
15
+ const modules = new Array(moduleCount);
16
+ for (let row = 0; row < moduleCount; row += 1) {
17
+ modules[row] = new Array(moduleCount);
18
+ for (let col = 0; col < moduleCount; col += 1) {
19
+ modules[row][col] = null;
20
+ }
21
+ }
22
+ return modules;
23
+ })(_moduleCount);
24
+ setupPositionProbePattern(0, 0);
25
+ setupPositionProbePattern(_moduleCount - 7, 0);
26
+ setupPositionProbePattern(0, _moduleCount - 7);
27
+ setupPositionAdjustPattern();
28
+ setupTimingPattern();
29
+ setupTypeInfo(test, maskPattern);
30
+ if (_typeNumber >= 7) {
31
+ setupTypeNumber(test);
32
+ }
33
+ if (_dataCache == null) {
34
+ _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
35
+ }
36
+ mapData(_dataCache, maskPattern);
37
+ };
38
+ const setupPositionProbePattern = function(row, col) {
39
+ for (let r = -1; r <= 7; r += 1) {
40
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
41
+ for (let c = -1; c <= 7; c += 1) {
42
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
43
+ if (0 <= r && r <= 6 && (c == 0 || c == 6) || 0 <= c && c <= 6 && (r == 0 || r == 6) || 2 <= r && r <= 4 && 2 <= c && c <= 4) {
44
+ _modules[row + r][col + c] = true;
45
+ } else {
46
+ _modules[row + r][col + c] = false;
47
+ }
48
+ }
49
+ }
50
+ };
51
+ const getBestMaskPattern = function() {
52
+ let minLostPoint = 0;
53
+ let pattern = 0;
54
+ for (let i = 0; i < 8; i += 1) {
55
+ makeImpl(true, i);
56
+ const lostPoint = QRUtil.getLostPoint(_this);
57
+ if (i == 0 || minLostPoint > lostPoint) {
58
+ minLostPoint = lostPoint;
59
+ pattern = i;
60
+ }
61
+ }
62
+ return pattern;
63
+ };
64
+ const setupTimingPattern = function() {
65
+ for (let r = 8; r < _moduleCount - 8; r += 1) {
66
+ if (_modules[r][6] != null) {
67
+ continue;
68
+ }
69
+ _modules[r][6] = r % 2 == 0;
70
+ }
71
+ for (let c = 8; c < _moduleCount - 8; c += 1) {
72
+ if (_modules[6][c] != null) {
73
+ continue;
74
+ }
75
+ _modules[6][c] = c % 2 == 0;
76
+ }
77
+ };
78
+ const setupPositionAdjustPattern = function() {
79
+ const pos = QRUtil.getPatternPosition(_typeNumber);
80
+ for (let i = 0; i < pos.length; i += 1) {
81
+ for (let j = 0; j < pos.length; j += 1) {
82
+ const row = pos[i];
83
+ const col = pos[j];
84
+ if (_modules[row][col] != null) {
85
+ continue;
86
+ }
87
+ for (let r = -2; r <= 2; r += 1) {
88
+ for (let c = -2; c <= 2; c += 1) {
89
+ if (r == -2 || r == 2 || c == -2 || c == 2 || r == 0 && c == 0) {
90
+ _modules[row + r][col + c] = true;
91
+ } else {
92
+ _modules[row + r][col + c] = false;
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ };
99
+ const setupTypeNumber = function(test) {
100
+ const bits = QRUtil.getBCHTypeNumber(_typeNumber);
101
+ for (let i = 0; i < 18; i += 1) {
102
+ const mod = !test && (bits >> i & 1) == 1;
103
+ _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
104
+ }
105
+ for (let i = 0; i < 18; i += 1) {
106
+ const mod = !test && (bits >> i & 1) == 1;
107
+ _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
108
+ }
109
+ };
110
+ const setupTypeInfo = function(test, maskPattern) {
111
+ const data = _errorCorrectionLevel << 3 | maskPattern;
112
+ const bits = QRUtil.getBCHTypeInfo(data);
113
+ for (let i = 0; i < 15; i += 1) {
114
+ const mod = !test && (bits >> i & 1) == 1;
115
+ if (i < 6) {
116
+ _modules[i][8] = mod;
117
+ } else if (i < 8) {
118
+ _modules[i + 1][8] = mod;
119
+ } else {
120
+ _modules[_moduleCount - 15 + i][8] = mod;
121
+ }
122
+ }
123
+ for (let i = 0; i < 15; i += 1) {
124
+ const mod = !test && (bits >> i & 1) == 1;
125
+ if (i < 8) {
126
+ _modules[8][_moduleCount - i - 1] = mod;
127
+ } else if (i < 9) {
128
+ _modules[8][15 - i - 1 + 1] = mod;
129
+ } else {
130
+ _modules[8][15 - i - 1] = mod;
131
+ }
132
+ }
133
+ _modules[_moduleCount - 8][8] = !test;
134
+ };
135
+ const mapData = function(data, maskPattern) {
136
+ let inc = -1;
137
+ let row = _moduleCount - 1;
138
+ let bitIndex = 7;
139
+ let byteIndex = 0;
140
+ const maskFunc = QRUtil.getMaskFunction(maskPattern);
141
+ for (let col = _moduleCount - 1; col > 0; col -= 2) {
142
+ if (col == 6) col -= 1;
143
+ while (true) {
144
+ for (let c = 0; c < 2; c += 1) {
145
+ if (_modules[row][col - c] == null) {
146
+ let dark = false;
147
+ if (byteIndex < data.length) {
148
+ dark = (data[byteIndex] >>> bitIndex & 1) == 1;
149
+ }
150
+ const mask = maskFunc(row, col - c);
151
+ if (mask) {
152
+ dark = !dark;
153
+ }
154
+ _modules[row][col - c] = dark;
155
+ bitIndex -= 1;
156
+ if (bitIndex == -1) {
157
+ byteIndex += 1;
158
+ bitIndex = 7;
159
+ }
160
+ }
161
+ }
162
+ row += inc;
163
+ if (row < 0 || _moduleCount <= row) {
164
+ row -= inc;
165
+ inc = -inc;
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ };
171
+ const createBytes = function(buffer, rsBlocks) {
172
+ let offset = 0;
173
+ let maxDcCount = 0;
174
+ let maxEcCount = 0;
175
+ const dcdata = new Array(rsBlocks.length);
176
+ const ecdata = new Array(rsBlocks.length);
177
+ for (let r = 0; r < rsBlocks.length; r += 1) {
178
+ const dcCount = rsBlocks[r].dataCount;
179
+ const ecCount = rsBlocks[r].totalCount - dcCount;
180
+ maxDcCount = Math.max(maxDcCount, dcCount);
181
+ maxEcCount = Math.max(maxEcCount, ecCount);
182
+ dcdata[r] = new Array(dcCount);
183
+ for (let i = 0; i < dcdata[r].length; i += 1) {
184
+ dcdata[r][i] = 255 & buffer.getBuffer()[i + offset];
185
+ }
186
+ offset += dcCount;
187
+ const rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
188
+ const rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
189
+ const modPoly = rawPoly.mod(rsPoly);
190
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
191
+ for (let i = 0; i < ecdata[r].length; i += 1) {
192
+ const modIndex = i + modPoly.getLength() - ecdata[r].length;
193
+ ecdata[r][i] = modIndex >= 0 ? modPoly.getAt(modIndex) : 0;
194
+ }
195
+ }
196
+ let totalCodeCount = 0;
197
+ for (let i = 0; i < rsBlocks.length; i += 1) {
198
+ totalCodeCount += rsBlocks[i].totalCount;
199
+ }
200
+ const data = new Array(totalCodeCount);
201
+ let index = 0;
202
+ for (let i = 0; i < maxDcCount; i += 1) {
203
+ for (let r = 0; r < rsBlocks.length; r += 1) {
204
+ if (i < dcdata[r].length) {
205
+ data[index] = dcdata[r][i];
206
+ index += 1;
207
+ }
208
+ }
209
+ }
210
+ for (let i = 0; i < maxEcCount; i += 1) {
211
+ for (let r = 0; r < rsBlocks.length; r += 1) {
212
+ if (i < ecdata[r].length) {
213
+ data[index] = ecdata[r][i];
214
+ index += 1;
215
+ }
216
+ }
217
+ }
218
+ return data;
219
+ };
220
+ const createData = function(typeNumber2, errorCorrectionLevel2, dataList) {
221
+ const rsBlocks = QRRSBlock.getRSBlocks(typeNumber2, errorCorrectionLevel2);
222
+ const buffer = qrBitBuffer();
223
+ for (let i = 0; i < dataList.length; i += 1) {
224
+ const data = dataList[i];
225
+ buffer.put(data.getMode(), 4);
226
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber2));
227
+ data.write(buffer);
228
+ }
229
+ let totalDataCount = 0;
230
+ for (let i = 0; i < rsBlocks.length; i += 1) {
231
+ totalDataCount += rsBlocks[i].dataCount;
232
+ }
233
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
234
+ throw "code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")";
235
+ }
236
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
237
+ buffer.put(0, 4);
238
+ }
239
+ while (buffer.getLengthInBits() % 8 != 0) {
240
+ buffer.putBit(false);
241
+ }
242
+ while (true) {
243
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
244
+ break;
245
+ }
246
+ buffer.put(PAD0, 8);
247
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
248
+ break;
249
+ }
250
+ buffer.put(PAD1, 8);
251
+ }
252
+ return createBytes(buffer, rsBlocks);
253
+ };
254
+ _this.addData = function(data, mode) {
255
+ mode = mode || "Byte";
256
+ let newData = null;
257
+ switch (mode) {
258
+ case "Numeric":
259
+ newData = qrNumber(data);
260
+ break;
261
+ case "Alphanumeric":
262
+ newData = qrAlphaNum(data);
263
+ break;
264
+ case "Byte":
265
+ newData = qr8BitByte(data);
266
+ break;
267
+ case "Kanji":
268
+ newData = qrKanji(data);
269
+ break;
270
+ default:
271
+ throw "mode:" + mode;
272
+ }
273
+ _dataList.push(newData);
274
+ _dataCache = null;
275
+ };
276
+ _this.isDark = function(row, col) {
277
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
278
+ throw row + "," + col;
279
+ }
280
+ return _modules[row][col];
281
+ };
282
+ _this.getModuleCount = function() {
283
+ return _moduleCount;
284
+ };
285
+ _this.make = function() {
286
+ if (_typeNumber < 1) {
287
+ let typeNumber2 = 1;
288
+ for (; typeNumber2 < 40; typeNumber2++) {
289
+ const rsBlocks = QRRSBlock.getRSBlocks(typeNumber2, _errorCorrectionLevel);
290
+ const buffer = qrBitBuffer();
291
+ for (let i = 0; i < _dataList.length; i++) {
292
+ const data = _dataList[i];
293
+ buffer.put(data.getMode(), 4);
294
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber2));
295
+ data.write(buffer);
296
+ }
297
+ let totalDataCount = 0;
298
+ for (let i = 0; i < rsBlocks.length; i++) {
299
+ totalDataCount += rsBlocks[i].dataCount;
300
+ }
301
+ if (buffer.getLengthInBits() <= totalDataCount * 8) {
302
+ break;
303
+ }
304
+ }
305
+ _typeNumber = typeNumber2;
306
+ }
307
+ makeImpl(false, getBestMaskPattern());
308
+ };
309
+ _this.createTableTag = function(cellSize, margin) {
310
+ cellSize = cellSize || 2;
311
+ margin = typeof margin == "undefined" ? cellSize * 4 : margin;
312
+ let qrHtml = "";
313
+ qrHtml += '<table style="';
314
+ qrHtml += " border-width: 0px; border-style: none;";
315
+ qrHtml += " border-collapse: collapse;";
316
+ qrHtml += " padding: 0px; margin: " + margin + "px;";
317
+ qrHtml += '">';
318
+ qrHtml += "<tbody>";
319
+ for (let r = 0; r < _this.getModuleCount(); r += 1) {
320
+ qrHtml += "<tr>";
321
+ for (let c = 0; c < _this.getModuleCount(); c += 1) {
322
+ qrHtml += '<td style="';
323
+ qrHtml += " border-width: 0px; border-style: none;";
324
+ qrHtml += " border-collapse: collapse;";
325
+ qrHtml += " padding: 0px; margin: 0px;";
326
+ qrHtml += " width: " + cellSize + "px;";
327
+ qrHtml += " height: " + cellSize + "px;";
328
+ qrHtml += " background-color: ";
329
+ qrHtml += _this.isDark(r, c) ? "#000000" : "#ffffff";
330
+ qrHtml += ";";
331
+ qrHtml += '"/>';
332
+ }
333
+ qrHtml += "</tr>";
334
+ }
335
+ qrHtml += "</tbody>";
336
+ qrHtml += "</table>";
337
+ return qrHtml;
338
+ };
339
+ _this.createSvgTag = function(cellSize, margin, alt, title) {
340
+ let opts = {};
341
+ if (typeof arguments[0] == "object") {
342
+ opts = arguments[0];
343
+ cellSize = opts.cellSize;
344
+ margin = opts.margin;
345
+ alt = opts.alt;
346
+ title = opts.title;
347
+ }
348
+ cellSize = cellSize || 2;
349
+ margin = typeof margin == "undefined" ? cellSize * 4 : margin;
350
+ alt = typeof alt === "string" ? { text: alt } : alt || {};
351
+ alt.text = alt.text || null;
352
+ alt.id = alt.text ? alt.id || "qrcode-description" : null;
353
+ title = typeof title === "string" ? { text: title } : title || {};
354
+ title.text = title.text || null;
355
+ title.id = title.text ? title.id || "qrcode-title" : null;
356
+ const size = _this.getModuleCount() * cellSize + margin * 2;
357
+ let c, mc, r, mr, qrSvg = "", rect;
358
+ rect = "l" + cellSize + ",0 0," + cellSize + " -" + cellSize + ",0 0,-" + cellSize + "z ";
359
+ qrSvg += '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"';
360
+ qrSvg += !opts.scalable ? ' width="' + size + 'px" height="' + size + 'px"' : "";
361
+ qrSvg += ' viewBox="0 0 ' + size + " " + size + '" ';
362
+ qrSvg += ' preserveAspectRatio="xMinYMin meet"';
363
+ qrSvg += title.text || alt.text ? ' role="img" aria-labelledby="' + escapeXml([title.id, alt.id].join(" ").trim()) + '"' : "";
364
+ qrSvg += ">";
365
+ qrSvg += title.text ? '<title id="' + escapeXml(title.id) + '">' + escapeXml(title.text) + "</title>" : "";
366
+ qrSvg += alt.text ? '<description id="' + escapeXml(alt.id) + '">' + escapeXml(alt.text) + "</description>" : "";
367
+ qrSvg += '<rect width="100%" height="100%" fill="white" cx="0" cy="0"/>';
368
+ qrSvg += '<path d="';
369
+ for (r = 0; r < _this.getModuleCount(); r += 1) {
370
+ mr = r * cellSize + margin;
371
+ for (c = 0; c < _this.getModuleCount(); c += 1) {
372
+ if (_this.isDark(r, c)) {
373
+ mc = c * cellSize + margin;
374
+ qrSvg += "M" + mc + "," + mr + rect;
375
+ }
376
+ }
377
+ }
378
+ qrSvg += '" stroke="transparent" fill="black"/>';
379
+ qrSvg += "</svg>";
380
+ return qrSvg;
381
+ };
382
+ _this.createDataURL = function(cellSize, margin) {
383
+ cellSize = cellSize || 2;
384
+ margin = typeof margin == "undefined" ? cellSize * 4 : margin;
385
+ const size = _this.getModuleCount() * cellSize + margin * 2;
386
+ const min = margin;
387
+ const max = size - margin;
388
+ return createDataURL(size, size, function(x, y) {
389
+ if (min <= x && x < max && min <= y && y < max) {
390
+ const c = Math.floor((x - min) / cellSize);
391
+ const r = Math.floor((y - min) / cellSize);
392
+ return _this.isDark(r, c) ? 0 : 1;
393
+ } else {
394
+ return 1;
395
+ }
396
+ });
397
+ };
398
+ _this.createImgTag = function(cellSize, margin, alt) {
399
+ cellSize = cellSize || 2;
400
+ margin = typeof margin == "undefined" ? cellSize * 4 : margin;
401
+ const size = _this.getModuleCount() * cellSize + margin * 2;
402
+ let img = "";
403
+ img += "<img";
404
+ img += ' src="';
405
+ img += _this.createDataURL(cellSize, margin);
406
+ img += '"';
407
+ img += ' width="';
408
+ img += size;
409
+ img += '"';
410
+ img += ' height="';
411
+ img += size;
412
+ img += '"';
413
+ if (alt) {
414
+ img += ' alt="';
415
+ img += escapeXml(alt);
416
+ img += '"';
417
+ }
418
+ img += "/>";
419
+ return img;
420
+ };
421
+ const escapeXml = function(s) {
422
+ let escaped = "";
423
+ for (let i = 0; i < s.length; i += 1) {
424
+ const c = s.charAt(i);
425
+ switch (c) {
426
+ case "<":
427
+ escaped += "&lt;";
428
+ break;
429
+ case ">":
430
+ escaped += "&gt;";
431
+ break;
432
+ case "&":
433
+ escaped += "&amp;";
434
+ break;
435
+ case '"':
436
+ escaped += "&quot;";
437
+ break;
438
+ default:
439
+ escaped += c;
440
+ break;
441
+ }
442
+ }
443
+ return escaped;
444
+ };
445
+ const _createHalfASCII = function(margin) {
446
+ const cellSize = 1;
447
+ margin = typeof margin == "undefined" ? cellSize * 2 : margin;
448
+ const size = _this.getModuleCount() * cellSize + margin * 2;
449
+ const min = margin;
450
+ const max = size - margin;
451
+ let y, x, r1, r2, p;
452
+ const blocks = {
453
+ "\u2588\u2588": "\u2588",
454
+ "\u2588 ": "\u2580",
455
+ " \u2588": "\u2584",
456
+ " ": " "
457
+ };
458
+ const blocksLastLineNoMargin = {
459
+ "\u2588\u2588": "\u2580",
460
+ "\u2588 ": "\u2580",
461
+ " \u2588": " ",
462
+ " ": " "
463
+ };
464
+ let ascii = "";
465
+ for (y = 0; y < size; y += 2) {
466
+ r1 = Math.floor((y - min) / cellSize);
467
+ r2 = Math.floor((y + 1 - min) / cellSize);
468
+ for (x = 0; x < size; x += 1) {
469
+ p = "\u2588";
470
+ if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) {
471
+ p = " ";
472
+ }
473
+ if (min <= x && x < max && min <= y + 1 && y + 1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) {
474
+ p += " ";
475
+ } else {
476
+ p += "\u2588";
477
+ }
478
+ ascii += margin < 1 && y + 1 >= max ? blocksLastLineNoMargin[p] : blocks[p];
479
+ }
480
+ ascii += "\n";
481
+ }
482
+ if (size % 2 && margin > 0) {
483
+ return ascii.substring(0, ascii.length - size - 1) + Array(size + 1).join("\u2580");
484
+ }
485
+ return ascii.substring(0, ascii.length - 1);
486
+ };
487
+ _this.createASCII = function(cellSize, margin) {
488
+ cellSize = cellSize || 1;
489
+ if (cellSize < 2) {
490
+ return _createHalfASCII(margin);
491
+ }
492
+ cellSize -= 1;
493
+ margin = typeof margin == "undefined" ? cellSize * 2 : margin;
494
+ const size = _this.getModuleCount() * cellSize + margin * 2;
495
+ const min = margin;
496
+ const max = size - margin;
497
+ let y, x, r, p;
498
+ const white = Array(cellSize + 1).join("\u2588\u2588");
499
+ const black = Array(cellSize + 1).join(" ");
500
+ let ascii = "";
501
+ let line = "";
502
+ for (y = 0; y < size; y += 1) {
503
+ r = Math.floor((y - min) / cellSize);
504
+ line = "";
505
+ for (x = 0; x < size; x += 1) {
506
+ p = 1;
507
+ if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) {
508
+ p = 0;
509
+ }
510
+ line += p ? white : black;
511
+ }
512
+ for (r = 0; r < cellSize; r += 1) {
513
+ ascii += line + "\n";
514
+ }
515
+ }
516
+ return ascii.substring(0, ascii.length - 1);
517
+ };
518
+ _this.renderTo2dContext = function(context, cellSize) {
519
+ cellSize = cellSize || 2;
520
+ const length = _this.getModuleCount();
521
+ for (let row = 0; row < length; row++) {
522
+ for (let col = 0; col < length; col++) {
523
+ context.fillStyle = _this.isDark(row, col) ? "black" : "white";
524
+ context.fillRect(col * cellSize, row * cellSize, cellSize, cellSize);
525
+ }
526
+ }
527
+ };
528
+ return _this;
529
+ };
530
+ qrcode.stringToBytes = function(s) {
531
+ const bytes = [];
532
+ for (let i = 0; i < s.length; i += 1) {
533
+ const c = s.charCodeAt(i);
534
+ bytes.push(c & 255);
535
+ }
536
+ return bytes;
537
+ };
538
+ qrcode.createStringToBytes = function(unicodeData, numChars) {
539
+ const unicodeMap = (function() {
540
+ const bin = base64DecodeInputStream(unicodeData);
541
+ const read = function() {
542
+ const b = bin.read();
543
+ if (b == -1) throw "eof";
544
+ return b;
545
+ };
546
+ let count = 0;
547
+ const unicodeMap2 = {};
548
+ while (true) {
549
+ const b0 = bin.read();
550
+ if (b0 == -1) break;
551
+ const b1 = read();
552
+ const b2 = read();
553
+ const b3 = read();
554
+ const k = String.fromCharCode(b0 << 8 | b1);
555
+ const v = b2 << 8 | b3;
556
+ unicodeMap2[k] = v;
557
+ count += 1;
558
+ }
559
+ if (count != numChars) {
560
+ throw count + " != " + numChars;
561
+ }
562
+ return unicodeMap2;
563
+ })();
564
+ const unknownChar = "?".charCodeAt(0);
565
+ return function(s) {
566
+ const bytes = [];
567
+ for (let i = 0; i < s.length; i += 1) {
568
+ const c = s.charCodeAt(i);
569
+ if (c < 128) {
570
+ bytes.push(c);
571
+ } else {
572
+ const b = unicodeMap[s.charAt(i)];
573
+ if (typeof b == "number") {
574
+ if ((b & 255) == b) {
575
+ bytes.push(b);
576
+ } else {
577
+ bytes.push(b >>> 8);
578
+ bytes.push(b & 255);
579
+ }
580
+ } else {
581
+ bytes.push(unknownChar);
582
+ }
583
+ }
584
+ }
585
+ return bytes;
586
+ };
587
+ };
588
+ var QRMode = {
589
+ MODE_NUMBER: 1 << 0,
590
+ MODE_ALPHA_NUM: 1 << 1,
591
+ MODE_8BIT_BYTE: 1 << 2,
592
+ MODE_KANJI: 1 << 3
593
+ };
594
+ var QRErrorCorrectionLevel = {
595
+ L: 1,
596
+ M: 0,
597
+ Q: 3,
598
+ H: 2
599
+ };
600
+ var QRMaskPattern = {
601
+ PATTERN000: 0,
602
+ PATTERN001: 1,
603
+ PATTERN010: 2,
604
+ PATTERN011: 3,
605
+ PATTERN100: 4,
606
+ PATTERN101: 5,
607
+ PATTERN110: 6,
608
+ PATTERN111: 7
609
+ };
610
+ var QRUtil = (function() {
611
+ const PATTERN_POSITION_TABLE = [
612
+ [],
613
+ [6, 18],
614
+ [6, 22],
615
+ [6, 26],
616
+ [6, 30],
617
+ [6, 34],
618
+ [6, 22, 38],
619
+ [6, 24, 42],
620
+ [6, 26, 46],
621
+ [6, 28, 50],
622
+ [6, 30, 54],
623
+ [6, 32, 58],
624
+ [6, 34, 62],
625
+ [6, 26, 46, 66],
626
+ [6, 26, 48, 70],
627
+ [6, 26, 50, 74],
628
+ [6, 30, 54, 78],
629
+ [6, 30, 56, 82],
630
+ [6, 30, 58, 86],
631
+ [6, 34, 62, 90],
632
+ [6, 28, 50, 72, 94],
633
+ [6, 26, 50, 74, 98],
634
+ [6, 30, 54, 78, 102],
635
+ [6, 28, 54, 80, 106],
636
+ [6, 32, 58, 84, 110],
637
+ [6, 30, 58, 86, 114],
638
+ [6, 34, 62, 90, 118],
639
+ [6, 26, 50, 74, 98, 122],
640
+ [6, 30, 54, 78, 102, 126],
641
+ [6, 26, 52, 78, 104, 130],
642
+ [6, 30, 56, 82, 108, 134],
643
+ [6, 34, 60, 86, 112, 138],
644
+ [6, 30, 58, 86, 114, 142],
645
+ [6, 34, 62, 90, 118, 146],
646
+ [6, 30, 54, 78, 102, 126, 150],
647
+ [6, 24, 50, 76, 102, 128, 154],
648
+ [6, 28, 54, 80, 106, 132, 158],
649
+ [6, 32, 58, 84, 110, 136, 162],
650
+ [6, 26, 54, 82, 110, 138, 166],
651
+ [6, 30, 58, 86, 114, 142, 170]
652
+ ];
653
+ const G15 = 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0;
654
+ const G18 = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0;
655
+ const G15_MASK = 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1;
656
+ const _this = {};
657
+ const getBCHDigit = function(data) {
658
+ let digit = 0;
659
+ while (data != 0) {
660
+ digit += 1;
661
+ data >>>= 1;
662
+ }
663
+ return digit;
664
+ };
665
+ _this.getBCHTypeInfo = function(data) {
666
+ let d = data << 10;
667
+ while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
668
+ d ^= G15 << getBCHDigit(d) - getBCHDigit(G15);
669
+ }
670
+ return (data << 10 | d) ^ G15_MASK;
671
+ };
672
+ _this.getBCHTypeNumber = function(data) {
673
+ let d = data << 12;
674
+ while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
675
+ d ^= G18 << getBCHDigit(d) - getBCHDigit(G18);
676
+ }
677
+ return data << 12 | d;
678
+ };
679
+ _this.getPatternPosition = function(typeNumber) {
680
+ return PATTERN_POSITION_TABLE[typeNumber - 1];
681
+ };
682
+ _this.getMaskFunction = function(maskPattern) {
683
+ switch (maskPattern) {
684
+ case QRMaskPattern.PATTERN000:
685
+ return function(i, j) {
686
+ return (i + j) % 2 == 0;
687
+ };
688
+ case QRMaskPattern.PATTERN001:
689
+ return function(i, j) {
690
+ return i % 2 == 0;
691
+ };
692
+ case QRMaskPattern.PATTERN010:
693
+ return function(i, j) {
694
+ return j % 3 == 0;
695
+ };
696
+ case QRMaskPattern.PATTERN011:
697
+ return function(i, j) {
698
+ return (i + j) % 3 == 0;
699
+ };
700
+ case QRMaskPattern.PATTERN100:
701
+ return function(i, j) {
702
+ return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
703
+ };
704
+ case QRMaskPattern.PATTERN101:
705
+ return function(i, j) {
706
+ return i * j % 2 + i * j % 3 == 0;
707
+ };
708
+ case QRMaskPattern.PATTERN110:
709
+ return function(i, j) {
710
+ return (i * j % 2 + i * j % 3) % 2 == 0;
711
+ };
712
+ case QRMaskPattern.PATTERN111:
713
+ return function(i, j) {
714
+ return (i * j % 3 + (i + j) % 2) % 2 == 0;
715
+ };
716
+ default:
717
+ throw "bad maskPattern:" + maskPattern;
718
+ }
719
+ };
720
+ _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
721
+ let a = qrPolynomial([1], 0);
722
+ for (let i = 0; i < errorCorrectLength; i += 1) {
723
+ a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0));
724
+ }
725
+ return a;
726
+ };
727
+ _this.getLengthInBits = function(mode, type) {
728
+ if (1 <= type && type < 10) {
729
+ switch (mode) {
730
+ case QRMode.MODE_NUMBER:
731
+ return 10;
732
+ case QRMode.MODE_ALPHA_NUM:
733
+ return 9;
734
+ case QRMode.MODE_8BIT_BYTE:
735
+ return 8;
736
+ case QRMode.MODE_KANJI:
737
+ return 8;
738
+ default:
739
+ throw "mode:" + mode;
740
+ }
741
+ } else if (type < 27) {
742
+ switch (mode) {
743
+ case QRMode.MODE_NUMBER:
744
+ return 12;
745
+ case QRMode.MODE_ALPHA_NUM:
746
+ return 11;
747
+ case QRMode.MODE_8BIT_BYTE:
748
+ return 16;
749
+ case QRMode.MODE_KANJI:
750
+ return 10;
751
+ default:
752
+ throw "mode:" + mode;
753
+ }
754
+ } else if (type < 41) {
755
+ switch (mode) {
756
+ case QRMode.MODE_NUMBER:
757
+ return 14;
758
+ case QRMode.MODE_ALPHA_NUM:
759
+ return 13;
760
+ case QRMode.MODE_8BIT_BYTE:
761
+ return 16;
762
+ case QRMode.MODE_KANJI:
763
+ return 12;
764
+ default:
765
+ throw "mode:" + mode;
766
+ }
767
+ } else {
768
+ throw "type:" + type;
769
+ }
770
+ };
771
+ _this.getLostPoint = function(qrcode2) {
772
+ const moduleCount = qrcode2.getModuleCount();
773
+ let lostPoint = 0;
774
+ for (let row = 0; row < moduleCount; row += 1) {
775
+ for (let col = 0; col < moduleCount; col += 1) {
776
+ let sameCount = 0;
777
+ const dark = qrcode2.isDark(row, col);
778
+ for (let r = -1; r <= 1; r += 1) {
779
+ if (row + r < 0 || moduleCount <= row + r) {
780
+ continue;
781
+ }
782
+ for (let c = -1; c <= 1; c += 1) {
783
+ if (col + c < 0 || moduleCount <= col + c) {
784
+ continue;
785
+ }
786
+ if (r == 0 && c == 0) {
787
+ continue;
788
+ }
789
+ if (dark == qrcode2.isDark(row + r, col + c)) {
790
+ sameCount += 1;
791
+ }
792
+ }
793
+ }
794
+ if (sameCount > 5) {
795
+ lostPoint += 3 + sameCount - 5;
796
+ }
797
+ }
798
+ }
799
+ ;
800
+ for (let row = 0; row < moduleCount - 1; row += 1) {
801
+ for (let col = 0; col < moduleCount - 1; col += 1) {
802
+ let count = 0;
803
+ if (qrcode2.isDark(row, col)) count += 1;
804
+ if (qrcode2.isDark(row + 1, col)) count += 1;
805
+ if (qrcode2.isDark(row, col + 1)) count += 1;
806
+ if (qrcode2.isDark(row + 1, col + 1)) count += 1;
807
+ if (count == 0 || count == 4) {
808
+ lostPoint += 3;
809
+ }
810
+ }
811
+ }
812
+ for (let row = 0; row < moduleCount; row += 1) {
813
+ for (let col = 0; col < moduleCount - 6; col += 1) {
814
+ if (qrcode2.isDark(row, col) && !qrcode2.isDark(row, col + 1) && qrcode2.isDark(row, col + 2) && qrcode2.isDark(row, col + 3) && qrcode2.isDark(row, col + 4) && !qrcode2.isDark(row, col + 5) && qrcode2.isDark(row, col + 6)) {
815
+ lostPoint += 40;
816
+ }
817
+ }
818
+ }
819
+ for (let col = 0; col < moduleCount; col += 1) {
820
+ for (let row = 0; row < moduleCount - 6; row += 1) {
821
+ if (qrcode2.isDark(row, col) && !qrcode2.isDark(row + 1, col) && qrcode2.isDark(row + 2, col) && qrcode2.isDark(row + 3, col) && qrcode2.isDark(row + 4, col) && !qrcode2.isDark(row + 5, col) && qrcode2.isDark(row + 6, col)) {
822
+ lostPoint += 40;
823
+ }
824
+ }
825
+ }
826
+ let darkCount = 0;
827
+ for (let col = 0; col < moduleCount; col += 1) {
828
+ for (let row = 0; row < moduleCount; row += 1) {
829
+ if (qrcode2.isDark(row, col)) {
830
+ darkCount += 1;
831
+ }
832
+ }
833
+ }
834
+ const ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
835
+ lostPoint += ratio * 10;
836
+ return lostPoint;
837
+ };
838
+ return _this;
839
+ })();
840
+ var QRMath = (function() {
841
+ const EXP_TABLE = new Array(256);
842
+ const LOG_TABLE = new Array(256);
843
+ for (let i = 0; i < 8; i += 1) {
844
+ EXP_TABLE[i] = 1 << i;
845
+ }
846
+ for (let i = 8; i < 256; i += 1) {
847
+ EXP_TABLE[i] = EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8];
848
+ }
849
+ for (let i = 0; i < 255; i += 1) {
850
+ LOG_TABLE[EXP_TABLE[i]] = i;
851
+ }
852
+ const _this = {};
853
+ _this.glog = function(n) {
854
+ if (n < 1) {
855
+ throw "glog(" + n + ")";
856
+ }
857
+ return LOG_TABLE[n];
858
+ };
859
+ _this.gexp = function(n) {
860
+ while (n < 0) {
861
+ n += 255;
862
+ }
863
+ while (n >= 256) {
864
+ n -= 255;
865
+ }
866
+ return EXP_TABLE[n];
867
+ };
868
+ return _this;
869
+ })();
870
+ var qrPolynomial = function(num, shift) {
871
+ if (typeof num.length == "undefined") {
872
+ throw num.length + "/" + shift;
873
+ }
874
+ const _num = (function() {
875
+ let offset = 0;
876
+ while (offset < num.length && num[offset] == 0) {
877
+ offset += 1;
878
+ }
879
+ const _num2 = new Array(num.length - offset + shift);
880
+ for (let i = 0; i < num.length - offset; i += 1) {
881
+ _num2[i] = num[i + offset];
882
+ }
883
+ return _num2;
884
+ })();
885
+ const _this = {};
886
+ _this.getAt = function(index) {
887
+ return _num[index];
888
+ };
889
+ _this.getLength = function() {
890
+ return _num.length;
891
+ };
892
+ _this.multiply = function(e) {
893
+ const num2 = new Array(_this.getLength() + e.getLength() - 1);
894
+ for (let i = 0; i < _this.getLength(); i += 1) {
895
+ for (let j = 0; j < e.getLength(); j += 1) {
896
+ num2[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i)) + QRMath.glog(e.getAt(j)));
897
+ }
898
+ }
899
+ return qrPolynomial(num2, 0);
900
+ };
901
+ _this.mod = function(e) {
902
+ if (_this.getLength() - e.getLength() < 0) {
903
+ return _this;
904
+ }
905
+ const ratio = QRMath.glog(_this.getAt(0)) - QRMath.glog(e.getAt(0));
906
+ const num2 = new Array(_this.getLength());
907
+ for (let i = 0; i < _this.getLength(); i += 1) {
908
+ num2[i] = _this.getAt(i);
909
+ }
910
+ for (let i = 0; i < e.getLength(); i += 1) {
911
+ num2[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i)) + ratio);
912
+ }
913
+ return qrPolynomial(num2, 0).mod(e);
914
+ };
915
+ return _this;
916
+ };
917
+ var QRRSBlock = (function() {
918
+ const RS_BLOCK_TABLE = [
919
+ // L
920
+ // M
921
+ // Q
922
+ // H
923
+ // 1
924
+ [1, 26, 19],
925
+ [1, 26, 16],
926
+ [1, 26, 13],
927
+ [1, 26, 9],
928
+ // 2
929
+ [1, 44, 34],
930
+ [1, 44, 28],
931
+ [1, 44, 22],
932
+ [1, 44, 16],
933
+ // 3
934
+ [1, 70, 55],
935
+ [1, 70, 44],
936
+ [2, 35, 17],
937
+ [2, 35, 13],
938
+ // 4
939
+ [1, 100, 80],
940
+ [2, 50, 32],
941
+ [2, 50, 24],
942
+ [4, 25, 9],
943
+ // 5
944
+ [1, 134, 108],
945
+ [2, 67, 43],
946
+ [2, 33, 15, 2, 34, 16],
947
+ [2, 33, 11, 2, 34, 12],
948
+ // 6
949
+ [2, 86, 68],
950
+ [4, 43, 27],
951
+ [4, 43, 19],
952
+ [4, 43, 15],
953
+ // 7
954
+ [2, 98, 78],
955
+ [4, 49, 31],
956
+ [2, 32, 14, 4, 33, 15],
957
+ [4, 39, 13, 1, 40, 14],
958
+ // 8
959
+ [2, 121, 97],
960
+ [2, 60, 38, 2, 61, 39],
961
+ [4, 40, 18, 2, 41, 19],
962
+ [4, 40, 14, 2, 41, 15],
963
+ // 9
964
+ [2, 146, 116],
965
+ [3, 58, 36, 2, 59, 37],
966
+ [4, 36, 16, 4, 37, 17],
967
+ [4, 36, 12, 4, 37, 13],
968
+ // 10
969
+ [2, 86, 68, 2, 87, 69],
970
+ [4, 69, 43, 1, 70, 44],
971
+ [6, 43, 19, 2, 44, 20],
972
+ [6, 43, 15, 2, 44, 16],
973
+ // 11
974
+ [4, 101, 81],
975
+ [1, 80, 50, 4, 81, 51],
976
+ [4, 50, 22, 4, 51, 23],
977
+ [3, 36, 12, 8, 37, 13],
978
+ // 12
979
+ [2, 116, 92, 2, 117, 93],
980
+ [6, 58, 36, 2, 59, 37],
981
+ [4, 46, 20, 6, 47, 21],
982
+ [7, 42, 14, 4, 43, 15],
983
+ // 13
984
+ [4, 133, 107],
985
+ [8, 59, 37, 1, 60, 38],
986
+ [8, 44, 20, 4, 45, 21],
987
+ [12, 33, 11, 4, 34, 12],
988
+ // 14
989
+ [3, 145, 115, 1, 146, 116],
990
+ [4, 64, 40, 5, 65, 41],
991
+ [11, 36, 16, 5, 37, 17],
992
+ [11, 36, 12, 5, 37, 13],
993
+ // 15
994
+ [5, 109, 87, 1, 110, 88],
995
+ [5, 65, 41, 5, 66, 42],
996
+ [5, 54, 24, 7, 55, 25],
997
+ [11, 36, 12, 7, 37, 13],
998
+ // 16
999
+ [5, 122, 98, 1, 123, 99],
1000
+ [7, 73, 45, 3, 74, 46],
1001
+ [15, 43, 19, 2, 44, 20],
1002
+ [3, 45, 15, 13, 46, 16],
1003
+ // 17
1004
+ [1, 135, 107, 5, 136, 108],
1005
+ [10, 74, 46, 1, 75, 47],
1006
+ [1, 50, 22, 15, 51, 23],
1007
+ [2, 42, 14, 17, 43, 15],
1008
+ // 18
1009
+ [5, 150, 120, 1, 151, 121],
1010
+ [9, 69, 43, 4, 70, 44],
1011
+ [17, 50, 22, 1, 51, 23],
1012
+ [2, 42, 14, 19, 43, 15],
1013
+ // 19
1014
+ [3, 141, 113, 4, 142, 114],
1015
+ [3, 70, 44, 11, 71, 45],
1016
+ [17, 47, 21, 4, 48, 22],
1017
+ [9, 39, 13, 16, 40, 14],
1018
+ // 20
1019
+ [3, 135, 107, 5, 136, 108],
1020
+ [3, 67, 41, 13, 68, 42],
1021
+ [15, 54, 24, 5, 55, 25],
1022
+ [15, 43, 15, 10, 44, 16],
1023
+ // 21
1024
+ [4, 144, 116, 4, 145, 117],
1025
+ [17, 68, 42],
1026
+ [17, 50, 22, 6, 51, 23],
1027
+ [19, 46, 16, 6, 47, 17],
1028
+ // 22
1029
+ [2, 139, 111, 7, 140, 112],
1030
+ [17, 74, 46],
1031
+ [7, 54, 24, 16, 55, 25],
1032
+ [34, 37, 13],
1033
+ // 23
1034
+ [4, 151, 121, 5, 152, 122],
1035
+ [4, 75, 47, 14, 76, 48],
1036
+ [11, 54, 24, 14, 55, 25],
1037
+ [16, 45, 15, 14, 46, 16],
1038
+ // 24
1039
+ [6, 147, 117, 4, 148, 118],
1040
+ [6, 73, 45, 14, 74, 46],
1041
+ [11, 54, 24, 16, 55, 25],
1042
+ [30, 46, 16, 2, 47, 17],
1043
+ // 25
1044
+ [8, 132, 106, 4, 133, 107],
1045
+ [8, 75, 47, 13, 76, 48],
1046
+ [7, 54, 24, 22, 55, 25],
1047
+ [22, 45, 15, 13, 46, 16],
1048
+ // 26
1049
+ [10, 142, 114, 2, 143, 115],
1050
+ [19, 74, 46, 4, 75, 47],
1051
+ [28, 50, 22, 6, 51, 23],
1052
+ [33, 46, 16, 4, 47, 17],
1053
+ // 27
1054
+ [8, 152, 122, 4, 153, 123],
1055
+ [22, 73, 45, 3, 74, 46],
1056
+ [8, 53, 23, 26, 54, 24],
1057
+ [12, 45, 15, 28, 46, 16],
1058
+ // 28
1059
+ [3, 147, 117, 10, 148, 118],
1060
+ [3, 73, 45, 23, 74, 46],
1061
+ [4, 54, 24, 31, 55, 25],
1062
+ [11, 45, 15, 31, 46, 16],
1063
+ // 29
1064
+ [7, 146, 116, 7, 147, 117],
1065
+ [21, 73, 45, 7, 74, 46],
1066
+ [1, 53, 23, 37, 54, 24],
1067
+ [19, 45, 15, 26, 46, 16],
1068
+ // 30
1069
+ [5, 145, 115, 10, 146, 116],
1070
+ [19, 75, 47, 10, 76, 48],
1071
+ [15, 54, 24, 25, 55, 25],
1072
+ [23, 45, 15, 25, 46, 16],
1073
+ // 31
1074
+ [13, 145, 115, 3, 146, 116],
1075
+ [2, 74, 46, 29, 75, 47],
1076
+ [42, 54, 24, 1, 55, 25],
1077
+ [23, 45, 15, 28, 46, 16],
1078
+ // 32
1079
+ [17, 145, 115],
1080
+ [10, 74, 46, 23, 75, 47],
1081
+ [10, 54, 24, 35, 55, 25],
1082
+ [19, 45, 15, 35, 46, 16],
1083
+ // 33
1084
+ [17, 145, 115, 1, 146, 116],
1085
+ [14, 74, 46, 21, 75, 47],
1086
+ [29, 54, 24, 19, 55, 25],
1087
+ [11, 45, 15, 46, 46, 16],
1088
+ // 34
1089
+ [13, 145, 115, 6, 146, 116],
1090
+ [14, 74, 46, 23, 75, 47],
1091
+ [44, 54, 24, 7, 55, 25],
1092
+ [59, 46, 16, 1, 47, 17],
1093
+ // 35
1094
+ [12, 151, 121, 7, 152, 122],
1095
+ [12, 75, 47, 26, 76, 48],
1096
+ [39, 54, 24, 14, 55, 25],
1097
+ [22, 45, 15, 41, 46, 16],
1098
+ // 36
1099
+ [6, 151, 121, 14, 152, 122],
1100
+ [6, 75, 47, 34, 76, 48],
1101
+ [46, 54, 24, 10, 55, 25],
1102
+ [2, 45, 15, 64, 46, 16],
1103
+ // 37
1104
+ [17, 152, 122, 4, 153, 123],
1105
+ [29, 74, 46, 14, 75, 47],
1106
+ [49, 54, 24, 10, 55, 25],
1107
+ [24, 45, 15, 46, 46, 16],
1108
+ // 38
1109
+ [4, 152, 122, 18, 153, 123],
1110
+ [13, 74, 46, 32, 75, 47],
1111
+ [48, 54, 24, 14, 55, 25],
1112
+ [42, 45, 15, 32, 46, 16],
1113
+ // 39
1114
+ [20, 147, 117, 4, 148, 118],
1115
+ [40, 75, 47, 7, 76, 48],
1116
+ [43, 54, 24, 22, 55, 25],
1117
+ [10, 45, 15, 67, 46, 16],
1118
+ // 40
1119
+ [19, 148, 118, 6, 149, 119],
1120
+ [18, 75, 47, 31, 76, 48],
1121
+ [34, 54, 24, 34, 55, 25],
1122
+ [20, 45, 15, 61, 46, 16]
1123
+ ];
1124
+ const qrRSBlock = function(totalCount, dataCount) {
1125
+ const _this2 = {};
1126
+ _this2.totalCount = totalCount;
1127
+ _this2.dataCount = dataCount;
1128
+ return _this2;
1129
+ };
1130
+ const _this = {};
1131
+ const getRsBlockTable = function(typeNumber, errorCorrectionLevel) {
1132
+ switch (errorCorrectionLevel) {
1133
+ case QRErrorCorrectionLevel.L:
1134
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
1135
+ case QRErrorCorrectionLevel.M:
1136
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
1137
+ case QRErrorCorrectionLevel.Q:
1138
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
1139
+ case QRErrorCorrectionLevel.H:
1140
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
1141
+ default:
1142
+ return void 0;
1143
+ }
1144
+ };
1145
+ _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {
1146
+ const rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);
1147
+ if (typeof rsBlock == "undefined") {
1148
+ throw "bad rs block @ typeNumber:" + typeNumber + "/errorCorrectionLevel:" + errorCorrectionLevel;
1149
+ }
1150
+ const length = rsBlock.length / 3;
1151
+ const list = [];
1152
+ for (let i = 0; i < length; i += 1) {
1153
+ const count = rsBlock[i * 3 + 0];
1154
+ const totalCount = rsBlock[i * 3 + 1];
1155
+ const dataCount = rsBlock[i * 3 + 2];
1156
+ for (let j = 0; j < count; j += 1) {
1157
+ list.push(qrRSBlock(totalCount, dataCount));
1158
+ }
1159
+ }
1160
+ return list;
1161
+ };
1162
+ return _this;
1163
+ })();
1164
+ var qrBitBuffer = function() {
1165
+ const _buffer = [];
1166
+ let _length = 0;
1167
+ const _this = {};
1168
+ _this.getBuffer = function() {
1169
+ return _buffer;
1170
+ };
1171
+ _this.getAt = function(index) {
1172
+ const bufIndex = Math.floor(index / 8);
1173
+ return (_buffer[bufIndex] >>> 7 - index % 8 & 1) == 1;
1174
+ };
1175
+ _this.put = function(num, length) {
1176
+ for (let i = 0; i < length; i += 1) {
1177
+ _this.putBit((num >>> length - i - 1 & 1) == 1);
1178
+ }
1179
+ };
1180
+ _this.getLengthInBits = function() {
1181
+ return _length;
1182
+ };
1183
+ _this.putBit = function(bit) {
1184
+ const bufIndex = Math.floor(_length / 8);
1185
+ if (_buffer.length <= bufIndex) {
1186
+ _buffer.push(0);
1187
+ }
1188
+ if (bit) {
1189
+ _buffer[bufIndex] |= 128 >>> _length % 8;
1190
+ }
1191
+ _length += 1;
1192
+ };
1193
+ return _this;
1194
+ };
1195
+ var qrNumber = function(data) {
1196
+ const _mode = QRMode.MODE_NUMBER;
1197
+ const _data = data;
1198
+ const _this = {};
1199
+ _this.getMode = function() {
1200
+ return _mode;
1201
+ };
1202
+ _this.getLength = function(buffer) {
1203
+ return _data.length;
1204
+ };
1205
+ _this.write = function(buffer) {
1206
+ const data2 = _data;
1207
+ let i = 0;
1208
+ while (i + 2 < data2.length) {
1209
+ buffer.put(strToNum(data2.substring(i, i + 3)), 10);
1210
+ i += 3;
1211
+ }
1212
+ if (i < data2.length) {
1213
+ if (data2.length - i == 1) {
1214
+ buffer.put(strToNum(data2.substring(i, i + 1)), 4);
1215
+ } else if (data2.length - i == 2) {
1216
+ buffer.put(strToNum(data2.substring(i, i + 2)), 7);
1217
+ }
1218
+ }
1219
+ };
1220
+ const strToNum = function(s) {
1221
+ let num = 0;
1222
+ for (let i = 0; i < s.length; i += 1) {
1223
+ num = num * 10 + chatToNum(s.charAt(i));
1224
+ }
1225
+ return num;
1226
+ };
1227
+ const chatToNum = function(c) {
1228
+ if ("0" <= c && c <= "9") {
1229
+ return c.charCodeAt(0) - "0".charCodeAt(0);
1230
+ }
1231
+ throw "illegal char :" + c;
1232
+ };
1233
+ return _this;
1234
+ };
1235
+ var qrAlphaNum = function(data) {
1236
+ const _mode = QRMode.MODE_ALPHA_NUM;
1237
+ const _data = data;
1238
+ const _this = {};
1239
+ _this.getMode = function() {
1240
+ return _mode;
1241
+ };
1242
+ _this.getLength = function(buffer) {
1243
+ return _data.length;
1244
+ };
1245
+ _this.write = function(buffer) {
1246
+ const s = _data;
1247
+ let i = 0;
1248
+ while (i + 1 < s.length) {
1249
+ buffer.put(
1250
+ getCode(s.charAt(i)) * 45 + getCode(s.charAt(i + 1)),
1251
+ 11
1252
+ );
1253
+ i += 2;
1254
+ }
1255
+ if (i < s.length) {
1256
+ buffer.put(getCode(s.charAt(i)), 6);
1257
+ }
1258
+ };
1259
+ const getCode = function(c) {
1260
+ if ("0" <= c && c <= "9") {
1261
+ return c.charCodeAt(0) - "0".charCodeAt(0);
1262
+ } else if ("A" <= c && c <= "Z") {
1263
+ return c.charCodeAt(0) - "A".charCodeAt(0) + 10;
1264
+ } else {
1265
+ switch (c) {
1266
+ case " ":
1267
+ return 36;
1268
+ case "$":
1269
+ return 37;
1270
+ case "%":
1271
+ return 38;
1272
+ case "*":
1273
+ return 39;
1274
+ case "+":
1275
+ return 40;
1276
+ case "-":
1277
+ return 41;
1278
+ case ".":
1279
+ return 42;
1280
+ case "/":
1281
+ return 43;
1282
+ case ":":
1283
+ return 44;
1284
+ default:
1285
+ throw "illegal char :" + c;
1286
+ }
1287
+ }
1288
+ };
1289
+ return _this;
1290
+ };
1291
+ var qr8BitByte = function(data) {
1292
+ const _mode = QRMode.MODE_8BIT_BYTE;
1293
+ const _data = data;
1294
+ const _bytes = qrcode.stringToBytes(data);
1295
+ const _this = {};
1296
+ _this.getMode = function() {
1297
+ return _mode;
1298
+ };
1299
+ _this.getLength = function(buffer) {
1300
+ return _bytes.length;
1301
+ };
1302
+ _this.write = function(buffer) {
1303
+ for (let i = 0; i < _bytes.length; i += 1) {
1304
+ buffer.put(_bytes[i], 8);
1305
+ }
1306
+ };
1307
+ return _this;
1308
+ };
1309
+ var qrKanji = function(data) {
1310
+ const _mode = QRMode.MODE_KANJI;
1311
+ const _data = data;
1312
+ const stringToBytes2 = qrcode.stringToBytes;
1313
+ !(function(c, code) {
1314
+ const test = stringToBytes2(c);
1315
+ if (test.length != 2 || (test[0] << 8 | test[1]) != code) {
1316
+ throw "sjis not supported.";
1317
+ }
1318
+ })("\u53CB", 38726);
1319
+ const _bytes = stringToBytes2(data);
1320
+ const _this = {};
1321
+ _this.getMode = function() {
1322
+ return _mode;
1323
+ };
1324
+ _this.getLength = function(buffer) {
1325
+ return ~~(_bytes.length / 2);
1326
+ };
1327
+ _this.write = function(buffer) {
1328
+ const data2 = _bytes;
1329
+ let i = 0;
1330
+ while (i + 1 < data2.length) {
1331
+ let c = (255 & data2[i]) << 8 | 255 & data2[i + 1];
1332
+ if (33088 <= c && c <= 40956) {
1333
+ c -= 33088;
1334
+ } else if (57408 <= c && c <= 60351) {
1335
+ c -= 49472;
1336
+ } else {
1337
+ throw "illegal char at " + (i + 1) + "/" + c;
1338
+ }
1339
+ c = (c >>> 8 & 255) * 192 + (c & 255);
1340
+ buffer.put(c, 13);
1341
+ i += 2;
1342
+ }
1343
+ if (i < data2.length) {
1344
+ throw "illegal char at " + (i + 1);
1345
+ }
1346
+ };
1347
+ return _this;
1348
+ };
1349
+ var byteArrayOutputStream = function() {
1350
+ const _bytes = [];
1351
+ const _this = {};
1352
+ _this.writeByte = function(b) {
1353
+ _bytes.push(b & 255);
1354
+ };
1355
+ _this.writeShort = function(i) {
1356
+ _this.writeByte(i);
1357
+ _this.writeByte(i >>> 8);
1358
+ };
1359
+ _this.writeBytes = function(b, off, len) {
1360
+ off = off || 0;
1361
+ len = len || b.length;
1362
+ for (let i = 0; i < len; i += 1) {
1363
+ _this.writeByte(b[i + off]);
1364
+ }
1365
+ };
1366
+ _this.writeString = function(s) {
1367
+ for (let i = 0; i < s.length; i += 1) {
1368
+ _this.writeByte(s.charCodeAt(i));
1369
+ }
1370
+ };
1371
+ _this.toByteArray = function() {
1372
+ return _bytes;
1373
+ };
1374
+ _this.toString = function() {
1375
+ let s = "";
1376
+ s += "[";
1377
+ for (let i = 0; i < _bytes.length; i += 1) {
1378
+ if (i > 0) {
1379
+ s += ",";
1380
+ }
1381
+ s += _bytes[i];
1382
+ }
1383
+ s += "]";
1384
+ return s;
1385
+ };
1386
+ return _this;
1387
+ };
1388
+ var base64EncodeOutputStream = function() {
1389
+ let _buffer = 0;
1390
+ let _buflen = 0;
1391
+ let _length = 0;
1392
+ let _base64 = "";
1393
+ const _this = {};
1394
+ const writeEncoded = function(b) {
1395
+ _base64 += String.fromCharCode(encode(b & 63));
1396
+ };
1397
+ const encode = function(n) {
1398
+ if (n < 0) {
1399
+ throw "n:" + n;
1400
+ } else if (n < 26) {
1401
+ return 65 + n;
1402
+ } else if (n < 52) {
1403
+ return 97 + (n - 26);
1404
+ } else if (n < 62) {
1405
+ return 48 + (n - 52);
1406
+ } else if (n == 62) {
1407
+ return 43;
1408
+ } else if (n == 63) {
1409
+ return 47;
1410
+ } else {
1411
+ throw "n:" + n;
1412
+ }
1413
+ };
1414
+ _this.writeByte = function(n) {
1415
+ _buffer = _buffer << 8 | n & 255;
1416
+ _buflen += 8;
1417
+ _length += 1;
1418
+ while (_buflen >= 6) {
1419
+ writeEncoded(_buffer >>> _buflen - 6);
1420
+ _buflen -= 6;
1421
+ }
1422
+ };
1423
+ _this.flush = function() {
1424
+ if (_buflen > 0) {
1425
+ writeEncoded(_buffer << 6 - _buflen);
1426
+ _buffer = 0;
1427
+ _buflen = 0;
1428
+ }
1429
+ if (_length % 3 != 0) {
1430
+ const padlen = 3 - _length % 3;
1431
+ for (let i = 0; i < padlen; i += 1) {
1432
+ _base64 += "=";
1433
+ }
1434
+ }
1435
+ };
1436
+ _this.toString = function() {
1437
+ return _base64;
1438
+ };
1439
+ return _this;
1440
+ };
1441
+ var base64DecodeInputStream = function(str) {
1442
+ const _str = str;
1443
+ let _pos = 0;
1444
+ let _buffer = 0;
1445
+ let _buflen = 0;
1446
+ const _this = {};
1447
+ _this.read = function() {
1448
+ while (_buflen < 8) {
1449
+ if (_pos >= _str.length) {
1450
+ if (_buflen == 0) {
1451
+ return -1;
1452
+ }
1453
+ throw "unexpected end of file./" + _buflen;
1454
+ }
1455
+ const c = _str.charAt(_pos);
1456
+ _pos += 1;
1457
+ if (c == "=") {
1458
+ _buflen = 0;
1459
+ return -1;
1460
+ } else if (c.match(/^\s$/)) {
1461
+ continue;
1462
+ }
1463
+ _buffer = _buffer << 6 | decode(c.charCodeAt(0));
1464
+ _buflen += 6;
1465
+ }
1466
+ const n = _buffer >>> _buflen - 8 & 255;
1467
+ _buflen -= 8;
1468
+ return n;
1469
+ };
1470
+ const decode = function(c) {
1471
+ if (65 <= c && c <= 90) {
1472
+ return c - 65;
1473
+ } else if (97 <= c && c <= 122) {
1474
+ return c - 97 + 26;
1475
+ } else if (48 <= c && c <= 57) {
1476
+ return c - 48 + 52;
1477
+ } else if (c == 43) {
1478
+ return 62;
1479
+ } else if (c == 47) {
1480
+ return 63;
1481
+ } else {
1482
+ throw "c:" + c;
1483
+ }
1484
+ };
1485
+ return _this;
1486
+ };
1487
+ var gifImage = function(width, height) {
1488
+ const _width = width;
1489
+ const _height = height;
1490
+ const _data = new Array(width * height);
1491
+ const _this = {};
1492
+ _this.setPixel = function(x, y, pixel) {
1493
+ _data[y * _width + x] = pixel;
1494
+ };
1495
+ _this.write = function(out) {
1496
+ out.writeString("GIF87a");
1497
+ out.writeShort(_width);
1498
+ out.writeShort(_height);
1499
+ out.writeByte(128);
1500
+ out.writeByte(0);
1501
+ out.writeByte(0);
1502
+ out.writeByte(0);
1503
+ out.writeByte(0);
1504
+ out.writeByte(0);
1505
+ out.writeByte(255);
1506
+ out.writeByte(255);
1507
+ out.writeByte(255);
1508
+ out.writeString(",");
1509
+ out.writeShort(0);
1510
+ out.writeShort(0);
1511
+ out.writeShort(_width);
1512
+ out.writeShort(_height);
1513
+ out.writeByte(0);
1514
+ const lzwMinCodeSize = 2;
1515
+ const raster = getLZWRaster(lzwMinCodeSize);
1516
+ out.writeByte(lzwMinCodeSize);
1517
+ let offset = 0;
1518
+ while (raster.length - offset > 255) {
1519
+ out.writeByte(255);
1520
+ out.writeBytes(raster, offset, 255);
1521
+ offset += 255;
1522
+ }
1523
+ out.writeByte(raster.length - offset);
1524
+ out.writeBytes(raster, offset, raster.length - offset);
1525
+ out.writeByte(0);
1526
+ out.writeString(";");
1527
+ };
1528
+ const bitOutputStream = function(out) {
1529
+ const _out = out;
1530
+ let _bitLength = 0;
1531
+ let _bitBuffer = 0;
1532
+ const _this2 = {};
1533
+ _this2.write = function(data, length) {
1534
+ if (data >>> length != 0) {
1535
+ throw "length over";
1536
+ }
1537
+ while (_bitLength + length >= 8) {
1538
+ _out.writeByte(255 & (data << _bitLength | _bitBuffer));
1539
+ length -= 8 - _bitLength;
1540
+ data >>>= 8 - _bitLength;
1541
+ _bitBuffer = 0;
1542
+ _bitLength = 0;
1543
+ }
1544
+ _bitBuffer = data << _bitLength | _bitBuffer;
1545
+ _bitLength = _bitLength + length;
1546
+ };
1547
+ _this2.flush = function() {
1548
+ if (_bitLength > 0) {
1549
+ _out.writeByte(_bitBuffer);
1550
+ }
1551
+ };
1552
+ return _this2;
1553
+ };
1554
+ const getLZWRaster = function(lzwMinCodeSize) {
1555
+ const clearCode = 1 << lzwMinCodeSize;
1556
+ const endCode = (1 << lzwMinCodeSize) + 1;
1557
+ let bitLength = lzwMinCodeSize + 1;
1558
+ const table = lzwTable();
1559
+ for (let i = 0; i < clearCode; i += 1) {
1560
+ table.add(String.fromCharCode(i));
1561
+ }
1562
+ table.add(String.fromCharCode(clearCode));
1563
+ table.add(String.fromCharCode(endCode));
1564
+ const byteOut = byteArrayOutputStream();
1565
+ const bitOut = bitOutputStream(byteOut);
1566
+ bitOut.write(clearCode, bitLength);
1567
+ let dataIndex = 0;
1568
+ let s = String.fromCharCode(_data[dataIndex]);
1569
+ dataIndex += 1;
1570
+ while (dataIndex < _data.length) {
1571
+ const c = String.fromCharCode(_data[dataIndex]);
1572
+ dataIndex += 1;
1573
+ if (table.contains(s + c)) {
1574
+ s = s + c;
1575
+ } else {
1576
+ bitOut.write(table.indexOf(s), bitLength);
1577
+ if (table.size() < 4095) {
1578
+ if (table.size() == 1 << bitLength) {
1579
+ bitLength += 1;
1580
+ }
1581
+ table.add(s + c);
1582
+ }
1583
+ s = c;
1584
+ }
1585
+ }
1586
+ bitOut.write(table.indexOf(s), bitLength);
1587
+ bitOut.write(endCode, bitLength);
1588
+ bitOut.flush();
1589
+ return byteOut.toByteArray();
1590
+ };
1591
+ const lzwTable = function() {
1592
+ const _map = {};
1593
+ let _size = 0;
1594
+ const _this2 = {};
1595
+ _this2.add = function(key) {
1596
+ if (_this2.contains(key)) {
1597
+ throw "dup key:" + key;
1598
+ }
1599
+ _map[key] = _size;
1600
+ _size += 1;
1601
+ };
1602
+ _this2.size = function() {
1603
+ return _size;
1604
+ };
1605
+ _this2.indexOf = function(key) {
1606
+ return _map[key];
1607
+ };
1608
+ _this2.contains = function(key) {
1609
+ return typeof _map[key] != "undefined";
1610
+ };
1611
+ return _this2;
1612
+ };
1613
+ return _this;
1614
+ };
1615
+ var createDataURL = function(width, height, getPixel) {
1616
+ const gif = gifImage(width, height);
1617
+ for (let y = 0; y < height; y += 1) {
1618
+ for (let x = 0; x < width; x += 1) {
1619
+ gif.setPixel(x, y, getPixel(x, y));
1620
+ }
1621
+ }
1622
+ const b = byteArrayOutputStream();
1623
+ gif.write(b);
1624
+ const base64 = base64EncodeOutputStream();
1625
+ const bytes = b.toByteArray();
1626
+ for (let i = 0; i < bytes.length; i += 1) {
1627
+ base64.writeByte(bytes[i]);
1628
+ }
1629
+ base64.flush();
1630
+ return "data:image/gif;base64," + base64;
1631
+ };
1632
+ var qrcode_default = qrcode;
1633
+ var stringToBytes = qrcode.stringToBytes;
1634
+
1635
+ // src/harness/dev-button.ts
1636
+ var inspectorEl = null;
1637
+ var inspectorTimer = null;
1638
+ function isInspectorOpen() {
1639
+ return inspectorEl != null;
1640
+ }
1641
+ function attachUzuMenu(button, opts = {}) {
1642
+ const { stateGetter, playerCount, adminEnabled, resetGame, onOpenMobile, onOpenSingleView } = opts;
1643
+ let popup = null;
1644
+ const closePopup = () => {
1645
+ popup?.remove();
1646
+ popup = null;
1647
+ };
1648
+ button.addEventListener("click", (ev) => {
1649
+ ev.stopPropagation();
1650
+ if (popup) {
1651
+ closePopup();
1652
+ return;
1653
+ }
1654
+ const el = document.createElement("div");
1655
+ popup = el;
1656
+ el.style.cssText = [
1657
+ "position:fixed",
1658
+ "z-index:100001",
1659
+ "background:#2d3436",
1660
+ "border-radius:8px",
1661
+ "padding:12px",
1662
+ "box-shadow:0 4px 16px rgba(0,0,0,0.4)",
1663
+ "font-family:system-ui,sans-serif",
1664
+ "min-width:200px",
1665
+ "display:flex",
1666
+ "flex-direction:column",
1667
+ "gap:8px"
1668
+ ].join(";");
1669
+ const addSeparator = () => {
1670
+ if (!el.children.length) return;
1671
+ const sep = document.createElement("div");
1672
+ sep.style.cssText = "border-top:1px solid #44475a;";
1673
+ el.appendChild(sep);
1674
+ };
1675
+ const addAction = (label, bg, hoverBg, onClick) => {
1676
+ const b = document.createElement("button");
1677
+ b.textContent = label;
1678
+ b.style.cssText = [
1679
+ "display:block",
1680
+ "width:100%",
1681
+ "padding:8px 24px",
1682
+ "border:none",
1683
+ "border-radius:4px",
1684
+ `background:${bg}`,
1685
+ "color:#fff",
1686
+ "font-size:13px",
1687
+ "cursor:pointer",
1688
+ "font-family:system-ui,sans-serif"
1689
+ ].join(";");
1690
+ b.onmouseenter = () => {
1691
+ b.style.background = hoverBg;
1692
+ b.style.color = "#2d3436";
1693
+ };
1694
+ b.onmouseleave = () => {
1695
+ b.style.background = bg;
1696
+ b.style.color = "#fff";
1697
+ };
1698
+ b.onclick = (e) => {
1699
+ e.stopPropagation();
1700
+ onClick();
1701
+ };
1702
+ el.appendChild(b);
1703
+ return b;
1704
+ };
1705
+ if (onOpenSingleView) {
1706
+ addAction("\u{1F5A5} \u3053\u306E\u753B\u9762\u3060\u3051\u958B\u304F", "#0984e3", "#74b9ff", () => {
1707
+ onOpenSingleView();
1708
+ closePopup();
1709
+ });
1710
+ }
1711
+ if (onOpenMobile) {
1712
+ addAction("\u{1F4F1} \u30B9\u30DE\u30DB\u3067\u958B\u304F", "#0984e3", "#74b9ff", () => {
1713
+ onOpenMobile();
1714
+ closePopup();
1715
+ });
1716
+ }
1717
+ addSeparator();
1718
+ addAction("Emulator", "#e17055", "#fab1a0", () => {
1719
+ const currentUrl = new URL(window.location.href);
1720
+ const endpoint = currentUrl.origin + currentUrl.pathname;
1721
+ const emulatorUrl = new URL("https://emulator.code.uzu-app.com/");
1722
+ emulatorUrl.searchParams.set("endpoint", endpoint);
1723
+ if (playerCount) {
1724
+ emulatorUrl.searchParams.set("player_count", String(playerCount));
1725
+ }
1726
+ if (adminEnabled) {
1727
+ emulatorUrl.searchParams.set("admin", "1");
1728
+ }
1729
+ window.open(emulatorUrl.toString(), "_blank");
1730
+ });
1731
+ if (stateGetter) {
1732
+ addSeparator();
1733
+ const stateBtn = addAction(
1734
+ isInspectorOpen() ? "Hide State" : "State",
1735
+ "#00b894",
1736
+ "#55efc4",
1737
+ () => {
1738
+ if (isInspectorOpen()) {
1739
+ closeInspector();
1740
+ stateBtn.textContent = "State";
1741
+ } else {
1742
+ openInspector(stateGetter);
1743
+ stateBtn.textContent = "Hide State";
1744
+ }
1745
+ }
1746
+ );
1747
+ }
1748
+ if (resetGame) {
1749
+ addSeparator();
1750
+ addAction("Reset State", "#d63031", "#ff7675", () => {
1751
+ resetGame();
1752
+ closePopup();
1753
+ });
1754
+ }
1755
+ document.body.appendChild(el);
1756
+ const rect = button.getBoundingClientRect();
1757
+ el.style.top = `${Math.round(rect.bottom + 6)}px`;
1758
+ el.style.left = `${Math.round(rect.left)}px`;
1759
+ const close = (e) => {
1760
+ if (popup && !popup.contains(e.target) && !button.contains(e.target)) {
1761
+ closePopup();
1762
+ document.removeEventListener("click", close);
1763
+ }
1764
+ };
1765
+ setTimeout(() => document.addEventListener("click", close), 0);
1766
+ });
1767
+ }
1768
+ function openInspector(getter) {
1769
+ if (inspectorEl) return;
1770
+ inspectorEl = document.createElement("div");
1771
+ inspectorEl.style.cssText = [
1772
+ "position:fixed",
1773
+ "bottom:8px",
1774
+ "right:8px",
1775
+ "z-index:99998",
1776
+ "width:360px",
1777
+ "max-height:80vh",
1778
+ "overflow:auto",
1779
+ "background:#1e272e",
1780
+ "border:1px solid #44475a",
1781
+ "border-radius:8px",
1782
+ "padding:0",
1783
+ "box-shadow:0 4px 16px rgba(0,0,0,0.5)",
1784
+ "font-family:monospace",
1785
+ "font-size:11px",
1786
+ "color:#dfe6e9",
1787
+ "resize:both",
1788
+ "min-width:200px",
1789
+ "min-height:120px"
1790
+ ].join(";");
1791
+ const header = document.createElement("div");
1792
+ header.style.cssText = [
1793
+ "display:flex",
1794
+ "justify-content:space-between",
1795
+ "align-items:center",
1796
+ "padding:6px 10px",
1797
+ "background:#2d3436",
1798
+ "border-bottom:1px solid #44475a",
1799
+ "border-radius:8px 8px 0 0",
1800
+ "position:sticky",
1801
+ "top:0",
1802
+ "cursor:grab",
1803
+ "user-select:none"
1804
+ ].join(";");
1805
+ const headerTitle = document.createElement("span");
1806
+ headerTitle.textContent = "State Inspector";
1807
+ headerTitle.style.cssText = "color:#00b894;font-weight:bold;font-size:12px;";
1808
+ const closeBtn = document.createElement("button");
1809
+ closeBtn.textContent = "\xD7";
1810
+ closeBtn.style.cssText = "background:none;border:none;color:#dfe6e9;font-size:18px;cursor:pointer;padding:0 4px;";
1811
+ closeBtn.onclick = () => closeInspector();
1812
+ header.appendChild(headerTitle);
1813
+ header.appendChild(closeBtn);
1814
+ inspectorEl.appendChild(header);
1815
+ let dragOffsetX = 0;
1816
+ let dragOffsetY = 0;
1817
+ let overlay = null;
1818
+ header.addEventListener("mousedown", (e) => {
1819
+ if (e.target === closeBtn) return;
1820
+ const rect = inspectorEl.getBoundingClientRect();
1821
+ dragOffsetX = e.clientX - rect.left;
1822
+ dragOffsetY = e.clientY - rect.top;
1823
+ header.style.cursor = "grabbing";
1824
+ e.preventDefault();
1825
+ overlay = document.createElement("div");
1826
+ overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;z-index:99997;cursor:grabbing;";
1827
+ document.body.appendChild(overlay);
1828
+ const onMouseMove = (ev) => {
1829
+ if (!inspectorEl) return;
1830
+ inspectorEl.style.left = ev.clientX - dragOffsetX + "px";
1831
+ inspectorEl.style.top = ev.clientY - dragOffsetY + "px";
1832
+ inspectorEl.style.right = "auto";
1833
+ inspectorEl.style.bottom = "auto";
1834
+ };
1835
+ const onMouseUp = () => {
1836
+ document.removeEventListener("mousemove", onMouseMove);
1837
+ document.removeEventListener("mouseup", onMouseUp);
1838
+ header.style.cursor = "grab";
1839
+ overlay?.remove();
1840
+ overlay = null;
1841
+ };
1842
+ document.addEventListener("mousemove", onMouseMove);
1843
+ document.addEventListener("mouseup", onMouseUp);
1844
+ });
1845
+ const pre = document.createElement("pre");
1846
+ pre.style.cssText = "margin:0;padding:10px;white-space:pre-wrap;word-break:break-all;";
1847
+ inspectorEl.appendChild(pre);
1848
+ document.body.appendChild(inspectorEl);
1849
+ const update = () => {
1850
+ const snap = getter();
1851
+ if (!snap) {
1852
+ pre.textContent = "(no state)";
1853
+ return;
1854
+ }
1855
+ pre.innerHTML = syntaxHighlight(snap.state);
1856
+ };
1857
+ update();
1858
+ inspectorTimer = setInterval(update, 200);
1859
+ }
1860
+ function closeInspector() {
1861
+ if (inspectorTimer) {
1862
+ clearInterval(inspectorTimer);
1863
+ inspectorTimer = null;
1864
+ }
1865
+ if (inspectorEl) {
1866
+ inspectorEl.remove();
1867
+ inspectorEl = null;
1868
+ }
1869
+ }
1870
+ function syntaxHighlight(obj) {
1871
+ const json = JSON.stringify(obj, null, 2);
1872
+ return json.replace(/("(?:\\.|[^"\\])*")\s*:/g, '<span style="color:#81ecec">$1</span>:').replace(/:\s*("(?:\\.|[^"\\])*")/g, ': <span style="color:#ffeaa7">$1</span>').replace(/:\s*(\d+\.?\d*)/g, ': <span style="color:#fd79a8">$1</span>').replace(/:\s*(true|false)/g, ': <span style="color:#a29bfe">$1</span>').replace(/:\s*(null)/g, ': <span style="color:#636e72">$1</span>');
1873
+ }
1874
+
1875
+ // src/harness/mount.ts
1876
+ var HUD_TOP = 8;
1877
+ var HUD_LEFT = 12;
1878
+ var HUD_UZU_SIZE = 44;
1879
+ var HUD_GAP = 6;
1880
+ var HUD_ACTION_ITEM_WIDTH = 38;
1881
+ var UZU_LOGO_SVG = `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1882
+ <path d="M5.58178 17.2748C4.86125 13.8314 7.60805 8.50894 11.366 7.67694C12.0626 8.32582 12.2856 10.153 12.1701 11.0726C10.7171 11.2637 7.6797 14.1578 7.97429 17.4022C8.02604 17.9595 8.70677 18.278 8.70677 18.278C7.36522 19.3807 5.91618 18.8672 5.58576 17.2748" fill="#BEBEFF"/>
1883
+ <path d="M18.6032 12.4103C15.8843 12.9557 14.28 15.6826 14.288 17.8044C14.2919 19.2096 15.7489 19.7231 16.7442 18.8832C16.143 18.927 15.9838 18.5687 15.9838 18.3259C15.9679 16.9326 17.5921 15.0615 18.8102 15.0456C19.065 14.3649 18.9098 12.9039 18.6032 12.4143" fill="#BEBEFF"/>
1884
+ <path d="M0.872407 19.5798C-0.684116 15.8935 -0.29399 9.47636 3.15345 5.77415C6.91538 1.73754 13.038 2.19534 14.686 6.56634C16.2983 10.8378 13.9894 17.0321 10.749 19.54C8.83018 21.0249 5.89627 20.376 5.57382 17.2032C5.90025 18.302 7.28162 18.6443 8.25295 17.8243C9.76568 16.5424 11.4536 13.915 11.6168 11.2757C11.9034 6.74946 7.92254 5.59501 4.87319 8.38958C0.75298 12.1674 1.75218 18.2462 1.89549 18.9827C2.0388 19.7192 2.14629 20.1332 1.76412 20.2765C1.44565 20.3999 1.11126 20.1531 0.872407 19.5838" fill="white"/>
1885
+ <path d="M22.5562 13.2024C22.5602 14.2175 22.393 15.2366 22.0467 16.196C22.0188 16.2716 21.991 16.3433 21.9631 16.4189C22.3532 17.6212 21.5929 19.0622 20.9878 19.739C20.5777 20.1968 20.9758 20.4237 21.5093 20.2087C22.2378 19.9102 23.9257 18.6084 23.9973 16.7255C24.0451 15.4675 23.4519 13.8911 22.5562 13.2024Z" fill="white"/>
1886
+ <path d="M16.2505 6.82111C16.2505 6.82111 16.2505 6.82509 16.2505 6.82907C16.2426 6.82907 16.2306 6.82907 16.2227 6.82907C16.5491 7.99944 16.6645 9.30119 16.561 10.6666C18.3126 11.2558 19.0212 13.4254 18.5355 15.3521C18.0578 17.259 16.7163 18.4891 15.8843 18.5726C14.8692 18.6762 14.284 17.876 14.3397 17.1714C14.1128 18.505 14.4552 20.2526 16.1032 20.3043C18.3843 20.376 19.9328 18.7956 21.0077 15.8298C22.3811 12.044 20.5539 7.29483 16.2465 6.82907" fill="white"/>
1887
+ </svg>`;
1888
+ var MIC_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1889
+ <path d="M12 1C10.34 1 9 2.34 9 4V12C9 13.66 10.34 15 12 15C13.66 15 15 13.66 15 12V4C15 2.34 13.66 1 12 1Z" fill="white"/>
1890
+ <path d="M17 12C17 14.76 14.76 17 12 17C9.24 17 7 14.76 7 12H5C5 15.53 7.61 18.43 11 18.93V22H13V18.93C16.39 18.43 19 15.53 19 12H17Z" fill="white"/>
1891
+ </svg>`;
1892
+ var PHONE_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1893
+ <rect x="6.5" y="2" width="11" height="20" rx="2.5" stroke="white" stroke-width="2"/>
1894
+ <line x1="10" y1="18.5" x2="14" y2="18.5" stroke="white" stroke-width="2" stroke-linecap="round"/>
1895
+ </svg>`;
1896
+ var NOTCH_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1897
+ <rect x="2" y="4.5" width="20" height="15" rx="3.5" stroke="white" stroke-width="1.8"/>
1898
+ <rect x="4.8" y="8.5" width="2.2" height="7" rx="1.1" fill="white"/>
1899
+ <line x1="11" y1="17" x2="17" y2="17" stroke="white" stroke-width="1.6" stroke-linecap="round"/>
1900
+ </svg>`;
1901
+ function hudInsetParams(hasMenu, actionCount) {
1902
+ let x = HUD_LEFT;
1903
+ if (hasMenu) x += HUD_UZU_SIZE;
1904
+ if (actionCount > 0) {
1905
+ if (hasMenu) x += HUD_GAP;
1906
+ x += HUD_ACTION_ITEM_WIDTH * actionCount;
1907
+ }
1908
+ return { x, y: HUD_TOP + HUD_UZU_SIZE };
1909
+ }
1910
+ var SIM_NOTCH = { top: 0, right: 59, bottom: 21, left: 59, radius: 44 };
1911
+ function createNotchDecor() {
1912
+ const decor = document.createElement("div");
1913
+ decor.style.cssText = "position:absolute;inset:0;pointer-events:none;z-index:7;";
1914
+ const island = document.createElement("div");
1915
+ island.style.cssText = [
1916
+ "position:absolute;left:16px;top:50%;transform:translateY(-50%)",
1917
+ "width:26px;height:88px;border-radius:13px",
1918
+ "background:#000;border:1px solid rgba(255,255,255,0.12)"
1919
+ ].join(";");
1920
+ const home = document.createElement("div");
1921
+ home.style.cssText = [
1922
+ "position:absolute;bottom:7px;left:50%;transform:translateX(-50%)",
1923
+ "width:32%;max-width:180px;height:4px;border-radius:2px",
1924
+ "background:rgba(255,255,255,0.5)"
1925
+ ].join(";");
1926
+ decor.append(island, home);
1927
+ return decor;
1928
+ }
1929
+ function mountFullscreenButton(target) {
1930
+ const doc = document;
1931
+ const root = document.documentElement;
1932
+ const supported = Boolean(root.requestFullscreen || root.webkitRequestFullscreen);
1933
+ const standalone = matchMedia("(display-mode: standalone)").matches;
1934
+ if (!supported || standalone) return;
1935
+ const btn = document.createElement("button");
1936
+ btn.textContent = "\u26F6";
1937
+ btn.title = "\u5168\u753B\u9762";
1938
+ btn.style.cssText = [
1939
+ "position:absolute",
1940
+ "right:calc(10px + env(safe-area-inset-right))",
1941
+ "top:calc(10px + env(safe-area-inset-top))",
1942
+ "z-index:10",
1943
+ "width:40px",
1944
+ "height:40px",
1945
+ "border:none",
1946
+ "border-radius:10px",
1947
+ "background:rgba(0,0,0,0.45)",
1948
+ "color:#cfd8ea",
1949
+ "font-size:20px",
1950
+ "line-height:1",
1951
+ "cursor:pointer",
1952
+ "backdrop-filter:blur(4px)"
1953
+ ].join(";");
1954
+ const isFullscreen = () => Boolean(document.fullscreenElement ?? doc.webkitFullscreenElement);
1955
+ const sync = () => {
1956
+ btn.textContent = isFullscreen() ? "\u2715" : "\u26F6";
1957
+ btn.style.opacity = isFullscreen() ? "0.35" : "1";
1958
+ };
1959
+ btn.onclick = () => {
1960
+ if (isFullscreen()) {
1961
+ (document.exitFullscreen ?? doc.webkitExitFullscreen)?.call(document);
1962
+ return;
1963
+ }
1964
+ (root.requestFullscreen ?? root.webkitRequestFullscreen)?.call(root);
1965
+ };
1966
+ document.addEventListener("fullscreenchange", sync);
1967
+ document.addEventListener("webkitfullscreenchange", sync);
1968
+ target.appendChild(btn);
1969
+ }
1970
+ function mountSinglePlayer(opts, playerIndex) {
1971
+ document.documentElement.style.cssText = "margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#0b0b16;";
1972
+ opts.target.style.cssText = "margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#0b0b16;position:relative;";
1973
+ const iframe = document.createElement("iframe");
1974
+ iframe.src = buildIframeUrl(opts, playerIndex);
1975
+ iframe.style.cssText = [
1976
+ "border:none;display:block;position:absolute",
1977
+ "top:env(safe-area-inset-top, 0px)",
1978
+ "left:env(safe-area-inset-left, 0px)",
1979
+ "width:calc(100% - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px))",
1980
+ "height:calc(100% - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))"
1981
+ ].join(";");
1982
+ iframe.allow = "autoplay";
1983
+ const splash = document.createElement("div");
1984
+ splash.style.cssText = [
1985
+ "position:absolute;inset:0;z-index:5",
1986
+ "display:flex;align-items:center;justify-content:center",
1987
+ "background:#0b0b16;color:#3a4a6a",
1988
+ "font-family:monospace;font-size:12px;letter-spacing:0.08em",
1989
+ "transition:opacity 0.3s"
1990
+ ].join(";");
1991
+ splash.textContent = "waiting firstFrameReady\u2026";
1992
+ opts.target.appendChild(iframe);
1993
+ opts.target.appendChild(splash);
1994
+ if (paneAt(opts, playerIndex).seatKind !== "admin") {
1995
+ const chrome = createCellChrome(opts, playerIndex, true, void 0, () => {
1996
+ const uzuBtn = chrome.querySelector("button");
1997
+ if (uzuBtn) openPlayerSwitcher(opts, playerIndex, uzuBtn);
1998
+ });
1999
+ opts.target.appendChild(chrome);
2000
+ }
2001
+ mountFullscreenButton(opts.target);
2002
+ window.addEventListener("message", (event) => {
2003
+ if (event.source !== iframe.contentWindow) return;
2004
+ let data = null;
2005
+ try {
2006
+ data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
2007
+ } catch {
2008
+ return;
2009
+ }
2010
+ if (!data || data.channel !== "sdk") return;
2011
+ if (data.type === "firstFrameReady") {
2012
+ splash.style.opacity = "0";
2013
+ splash.style.pointerEvents = "none";
2014
+ setTimeout(() => splash.remove(), 350);
2015
+ }
2016
+ });
2017
+ }
2018
+ var CELL_ASPECT_RATIO = { portrait: 9 / 19.5, landscape: 19.5 / 9 };
2019
+ var GAP = 4;
2020
+ var optimalGrid = (areaWidth, areaHeight, count, cellAspectRatio) => {
2021
+ let bestCols = 1;
2022
+ let bestRows = count;
2023
+ let bestCellArea = 0;
2024
+ for (let cols = 1; cols <= count; cols++) {
2025
+ const rows = Math.ceil(count / cols);
2026
+ const slotW = (areaWidth - GAP * (cols - 1)) / cols;
2027
+ const slotH = (areaHeight - GAP * (rows - 1)) / rows;
2028
+ let cellW;
2029
+ let cellH;
2030
+ if (slotW / slotH > cellAspectRatio) {
2031
+ cellH = slotH;
2032
+ cellW = slotH * cellAspectRatio;
2033
+ } else {
2034
+ cellW = slotW;
2035
+ cellH = slotW / cellAspectRatio;
2036
+ }
2037
+ const area = cellW * cellH;
2038
+ if (area > bestCellArea) {
2039
+ bestCellArea = area;
2040
+ bestCols = cols;
2041
+ bestRows = rows;
2042
+ }
2043
+ }
2044
+ return { cols: bestCols, rows: bestRows };
2045
+ };
2046
+ var computeGridCellSize = (areaWidth, areaHeight, count, cellAspectRatio) => {
2047
+ const { cols, rows } = optimalGrid(areaWidth, areaHeight, count, cellAspectRatio);
2048
+ const slotW = (areaWidth - GAP * (cols - 1)) / cols;
2049
+ const slotH = (areaHeight - GAP * (rows - 1)) / rows;
2050
+ let cellW;
2051
+ let cellH;
2052
+ if (slotW / slotH > cellAspectRatio) {
2053
+ cellH = slotH;
2054
+ cellW = slotH * cellAspectRatio;
2055
+ } else {
2056
+ cellW = slotW;
2057
+ cellH = slotW / cellAspectRatio;
2058
+ }
2059
+ return { cols, rows, cellW, cellH };
2060
+ };
2061
+ function createLifecycleStatusRow() {
2062
+ const row = document.createElement("div");
2063
+ row.style.cssText = "display:flex;justify-content:center;gap:8px;font-size:9px;padding-top:1px;font-family:monospace;color:#3a4a6a;";
2064
+ const entries = [
2065
+ { key: "ready", label: "init" },
2066
+ { key: "firstFrameReady", label: "firstFrame" },
2067
+ { key: "gameReady", label: "game" }
2068
+ ];
2069
+ const dots = {
2070
+ ready: document.createElement("span"),
2071
+ firstFrameReady: document.createElement("span"),
2072
+ gameReady: document.createElement("span")
2073
+ };
2074
+ for (const { key, label } of entries) {
2075
+ const item = document.createElement("span");
2076
+ item.style.cssText = "display:inline-flex;align-items:center;gap:2px;";
2077
+ const dot = dots[key];
2078
+ dot.textContent = "\u25CF";
2079
+ dot.style.cssText = "color:#3a4a6a;transition:color 0.15s;";
2080
+ const text = document.createElement("span");
2081
+ text.textContent = label;
2082
+ item.appendChild(dot);
2083
+ item.appendChild(text);
2084
+ row.appendChild(item);
2085
+ }
2086
+ const predictionWarn = document.createElement("span");
2087
+ predictionWarn.style.cssText = [
2088
+ "display:none;align-items:center;gap:3px",
2089
+ "color:#1a1a2e;background:#ffb86c;border-radius:6px",
2090
+ "padding:0 5px;cursor:pointer;user-select:none;font-weight:bold"
2091
+ ].join(";");
2092
+ row.appendChild(predictionWarn);
2093
+ return { el: row, dots, predictionWarn };
2094
+ }
2095
+ function observerPanes(opts) {
2096
+ const pane = (id, nickname, seatKind) => ({
2097
+ id,
2098
+ nickname,
2099
+ seatKind,
2100
+ playerNo: null
2101
+ });
2102
+ return [
2103
+ ...opts.spectator ? [pane("spec_0", "\u89B3\u6226", "spectator")] : [],
2104
+ ...opts.admin ? [pane("admin_0", "Admin", "admin")] : []
2105
+ ];
2106
+ }
2107
+ function paneCount(opts) {
2108
+ return opts.players.length + observerPanes(opts).length;
2109
+ }
2110
+ function paneAt(opts, index) {
2111
+ const player = opts.players[index];
2112
+ if (player) {
25
2113
  return {
26
- ...meta,
27
- scenarioUrl: `${location.protocol}//${location.host}/`,
28
- serverBaseUrl: `ws://${location.host}`,
2114
+ id: player.id,
2115
+ nickname: player.nickname,
2116
+ seatKind: "player",
2117
+ playerNo: index + 1
29
2118
  };
2119
+ }
2120
+ const observer = observerPanes(opts)[index - opts.players.length];
2121
+ if (!observer) throw new Error(`pane index out of range: ${index}`);
2122
+ return observer;
30
2123
  }
31
- async function main() {
32
- const res = await fetch('/_uzu_meta.json');
33
- if (!res.ok) {
34
- console.error('[uzu-harness] failed to fetch meta:', res.status);
2124
+ function toWireSeat(seat) {
2125
+ return {
2126
+ id: seat.id,
2127
+ name: seat.nickname,
2128
+ iconUrl: seat.iconUrl,
2129
+ characterId: seat.characterId,
2130
+ kind: "player"
2131
+ };
2132
+ }
2133
+ function buildIframeUrl(opts, playerIndex) {
2134
+ const url = new URL(opts.scenarioUrl);
2135
+ const pane = paneAt(opts, playerIndex);
2136
+ url.searchParams.set("server", opts.serverBaseUrl);
2137
+ url.searchParams.set("roomId", opts.roomKey);
2138
+ url.searchParams.set("seatId", pane.id);
2139
+ const roster = JSON.stringify(opts.players.map(toWireSeat));
2140
+ url.searchParams.set("players", roster);
2141
+ url.searchParams.set("seats", roster);
2142
+ url.searchParams.set("seatKind", pane.seatKind);
2143
+ url.searchParams.set("revisionId", opts.revisionId);
2144
+ url.searchParams.set("__dev_player", String(playerIndex));
2145
+ url.searchParams.set("__dev_room", opts.roomKey);
2146
+ if (pane.seatKind === "admin") {
2147
+ url.searchParams.set("uzuHudInsetX", "0");
2148
+ url.searchParams.set("uzuHudInsetY", "0");
2149
+ return url.toString();
2150
+ }
2151
+ const hud = hudInsetParams(true, 2);
2152
+ url.searchParams.set("uzuHudInsetX", String(hud.x));
2153
+ url.searchParams.set("uzuHudInsetY", String(hud.y));
2154
+ return url.toString();
2155
+ }
2156
+ function resolveMobileUrl(opts, playerIndex) {
2157
+ const isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
2158
+ const host = isLocal ? opts.lanHosts?.[0] : location.hostname;
2159
+ if (!host) return null;
2160
+ const port = location.port ? `:${location.port}` : "";
2161
+ return `http://${host}${port}/?player=${playerIndex}`;
2162
+ }
2163
+ function singleViewUrl(playerIndex) {
2164
+ const url = new URL(location.href);
2165
+ url.searchParams.set("player", String(playerIndex));
2166
+ return url.toString();
2167
+ }
2168
+ function openMobileQrPopup(url, label) {
2169
+ const backdrop = document.createElement("div");
2170
+ backdrop.style.cssText = [
2171
+ "position:fixed;inset:0;z-index:100000",
2172
+ "background:rgba(0,0,0,0.6)",
2173
+ "display:flex;align-items:center;justify-content:center"
2174
+ ].join(";");
2175
+ const panel = document.createElement("div");
2176
+ panel.style.cssText = [
2177
+ "background:#2d3436;border-radius:12px;padding:20px",
2178
+ "display:flex;flex-direction:column;align-items:center;gap:10px",
2179
+ "font-family:system-ui,sans-serif;color:#fff;max-width:80vw"
2180
+ ].join(";");
2181
+ const heading = document.createElement("div");
2182
+ heading.textContent = `\u{1F4F1} ${label}`;
2183
+ heading.style.cssText = "font-size:14px;font-weight:bold;";
2184
+ const note = document.createElement("div");
2185
+ note.textContent = "QR \u3092\u30AB\u30E1\u30E9\u3067\u8AAD\u3080\u3068\u3001 \u3053\u306E\u30AD\u30E3\u30E9\u306E\u753B\u9762\u304C\u30B9\u30DE\u30DB\u3067\u958B\u304D\u307E\u3059";
2186
+ note.style.cssText = "font-size:11px;color:#a0a8b8;";
2187
+ const qrWrap = document.createElement("div");
2188
+ qrWrap.style.cssText = "background:#fff;padding:10px;border-radius:8px;line-height:0;";
2189
+ const qr = qrcode_default(0, "M");
2190
+ qr.addData(url);
2191
+ qr.make();
2192
+ qrWrap.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 0, scalable: true });
2193
+ const svg = qrWrap.querySelector("svg");
2194
+ if (svg) {
2195
+ svg.style.width = "200px";
2196
+ svg.style.height = "200px";
2197
+ }
2198
+ const urlEl = document.createElement("a");
2199
+ urlEl.textContent = url;
2200
+ urlEl.href = url;
2201
+ urlEl.target = "_blank";
2202
+ urlEl.style.cssText = "font-size:12px;color:#8be9fd;word-break:break-all;text-align:center;";
2203
+ panel.append(heading, note, qrWrap, urlEl);
2204
+ backdrop.appendChild(panel);
2205
+ backdrop.onclick = () => backdrop.remove();
2206
+ panel.onclick = (e) => e.stopPropagation();
2207
+ document.body.appendChild(backdrop);
2208
+ }
2209
+ function openPlayerSwitcher(opts, currentIndex, anchor) {
2210
+ const existing = document.getElementById("__uzu_player_switcher");
2211
+ if (existing) {
2212
+ existing.remove();
2213
+ return;
2214
+ }
2215
+ const panel = document.createElement("div");
2216
+ panel.id = "__uzu_player_switcher";
2217
+ panel.style.cssText = [
2218
+ "position:fixed;z-index:100001",
2219
+ "background:#2d3436;border-radius:10px;padding:8px",
2220
+ "box-shadow:0 4px 16px rgba(0,0,0,0.5)",
2221
+ "font-family:system-ui,sans-serif;color:#fff",
2222
+ "display:flex;flex-direction:column;gap:4px;min-width:180px",
2223
+ "max-height:70vh;overflow-y:auto"
2224
+ ].join(";");
2225
+ const heading = document.createElement("div");
2226
+ heading.textContent = "\u753B\u9762\u3092\u5207\u308A\u66FF\u3048";
2227
+ heading.style.cssText = "font-size:11px;color:#a0a8b8;padding:2px 8px 6px;";
2228
+ panel.appendChild(heading);
2229
+ for (let i = 0; i < paneCount(opts); i++) {
2230
+ const pane = paneAt(opts, i);
2231
+ const isCurrent = i === currentIndex;
2232
+ const row = document.createElement("button");
2233
+ row.textContent = `${isCurrent ? "\u25CF " : ""}${pane.nickname} (${pane.id})`;
2234
+ row.style.cssText = [
2235
+ "text-align:left;border:none;border-radius:6px",
2236
+ "padding:10px 12px;font-size:14px;cursor:pointer",
2237
+ `background:${isCurrent ? "#0984e3" : "#3a4553"};color:#fff`
2238
+ ].join(";");
2239
+ row.onclick = (e) => {
2240
+ e.stopPropagation();
2241
+ if (isCurrent) {
2242
+ panel.remove();
35
2243
  return;
2244
+ }
2245
+ const url = new URL(location.href);
2246
+ url.searchParams.set("player", String(i));
2247
+ location.href = url.toString();
2248
+ };
2249
+ panel.appendChild(row);
2250
+ }
2251
+ document.body.appendChild(panel);
2252
+ const rect = anchor.getBoundingClientRect();
2253
+ panel.style.top = `${Math.round(rect.bottom + 6)}px`;
2254
+ panel.style.left = `${Math.round(rect.left)}px`;
2255
+ const close = (e) => {
2256
+ if (!panel.contains(e.target) && !anchor.contains(e.target)) {
2257
+ panel.remove();
2258
+ document.removeEventListener("click", close);
36
2259
  }
37
- const rawMeta = (await res.json());
38
- const meta = rewriteHostForViewer(rawMeta);
39
- // `?player=N` (0-based) 付きは 1 プレイヤー全画面 mount (スマホ実機用)。
40
- // admin channel grid (親frame) だけが張るので、 単体表示では dev メニューを出さない。
41
- const playerParam = new URLSearchParams(location.search).get('player');
42
- const playerIndex = playerParam == null ? null : Number(playerParam);
43
- const panes = paneCount(meta);
44
- if (playerIndex != null && Number.isInteger(playerIndex)) {
45
- if (playerIndex < 0 || playerIndex >= panes) {
46
- document.body.textContent = `player 0〜${panes - 1} で指定してください`;
47
- return;
2260
+ };
2261
+ setTimeout(() => document.addEventListener("click", close), 0);
2262
+ }
2263
+ function createCellChrome(opts, playerIndex, facade = false, notchToggle, onUzuClick) {
2264
+ const wrap = document.createElement("div");
2265
+ wrap.style.cssText = [
2266
+ "position:absolute",
2267
+ // notch / status bar を避けるため safe-area を足す (PC harness では env=0)
2268
+ `top:calc(${HUD_TOP}px + env(safe-area-inset-top, 0px))`,
2269
+ `left:calc(${HUD_LEFT}px + env(safe-area-inset-left, 0px))`,
2270
+ "z-index:6",
2271
+ "display:flex;align-items:center;gap:6px"
2272
+ ].join(";");
2273
+ const uzu = document.createElement("button");
2274
+ const uzuInteractive = Boolean(onUzuClick) || !facade;
2275
+ uzu.title = onUzuClick ? "\u753B\u9762\u3092\u5207\u308A\u66FF\u3048" : "UZU \u30E1\u30CB\u30E5\u30FC";
2276
+ uzu.innerHTML = UZU_LOGO_SVG;
2277
+ uzu.style.cssText = [
2278
+ `width:${HUD_UZU_SIZE}px;height:${HUD_UZU_SIZE}px`,
2279
+ "border:none;border-radius:12px",
2280
+ "background:rgba(0,0,0,0.6)",
2281
+ `cursor:${uzuInteractive ? "pointer" : "default"};padding:0`,
2282
+ "display:flex;align-items:center;justify-content:center",
2283
+ "flex-shrink:0"
2284
+ ].join(";");
2285
+ const mobileUrl = resolveMobileUrl(opts, playerIndex);
2286
+ const label = paneAt(opts, playerIndex).nickname;
2287
+ if (onUzuClick) {
2288
+ uzu.onclick = onUzuClick;
2289
+ } else if (!facade) {
2290
+ attachUzuMenu(uzu, {
2291
+ stateGetter: opts.stateGetter,
2292
+ resetGame: opts.resetGame,
2293
+ playerCount: opts.playerCount,
2294
+ adminEnabled: opts.admin,
2295
+ onOpenSingleView: () => {
2296
+ window.open(singleViewUrl(playerIndex), "_blank", "noopener");
2297
+ }
2298
+ });
2299
+ }
2300
+ wrap.appendChild(uzu);
2301
+ const bar = document.createElement("div");
2302
+ bar.style.cssText = [
2303
+ "height:36px",
2304
+ "background:rgba(0,0,0,0.6)",
2305
+ "border-radius:18px",
2306
+ "display:flex;align-items:center"
2307
+ ].join(";");
2308
+ const micSlot = document.createElement(notchToggle ? "button" : "div");
2309
+ micSlot.innerHTML = notchToggle ? NOTCH_SVG : MIC_SVG;
2310
+ micSlot.style.cssText = notchToggle ? "padding:0 10px;height:36px;background:none;border:none;cursor:pointer;display:flex;align-items:center;" : "padding:0 10px;display:flex;align-items:center;";
2311
+ if (notchToggle) {
2312
+ micSlot.onclick = notchToggle.toggle;
2313
+ notchToggle.register((on) => {
2314
+ micSlot.style.opacity = on ? "1" : "0.4";
2315
+ micSlot.title = `\u64EC\u4F3C\u30CE\u30C3\u30C1\u8868\u793A: ${on ? "ON" : "OFF"}`;
2316
+ });
2317
+ } else {
2318
+ micSlot.title = "\u30DE\u30A4\u30AF (dev \u8868\u793A\u306E\u307F)";
2319
+ }
2320
+ bar.appendChild(micSlot);
2321
+ const phone = document.createElement("button");
2322
+ phone.innerHTML = PHONE_SVG;
2323
+ phone.title = facade ? "" : "\u30B9\u30DE\u30DB\u5B9F\u6A5F\u3067\u958B\u304F (QR)";
2324
+ phone.style.cssText = [
2325
+ "padding:0 10px;height:36px",
2326
+ `background:none;border:none;cursor:${facade ? "default" : "pointer"}`,
2327
+ "display:flex;align-items:center"
2328
+ ].join(";");
2329
+ if (!facade) {
2330
+ phone.onclick = () => {
2331
+ if (mobileUrl) openMobileQrPopup(mobileUrl, label);
2332
+ };
2333
+ }
2334
+ bar.appendChild(phone);
2335
+ wrap.appendChild(bar);
2336
+ return wrap;
2337
+ }
2338
+ function mountIframeGrid(opts) {
2339
+ const { target, playerCount, orientation, devMinIframeShortEdge } = opts;
2340
+ document.documentElement.style.cssText = "margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#1a1a2e;";
2341
+ const cells = [];
2342
+ const iframeStatusDots = /* @__PURE__ */ new Map();
2343
+ const iframeSplashes = /* @__PURE__ */ new Map();
2344
+ const iframePredictionWarns = /* @__PURE__ */ new Map();
2345
+ let notchSimOn = false;
2346
+ const notchAppliers = [];
2347
+ const toggleNotchSim = () => {
2348
+ notchSimOn = !notchSimOn;
2349
+ for (const apply of notchAppliers) apply(notchSimOn);
2350
+ };
2351
+ const layout = () => {
2352
+ const n = cells.length;
2353
+ if (n === 0) return;
2354
+ const cellAR = CELL_ASPECT_RATIO[orientation];
2355
+ const targetW = target === document.body ? window.innerWidth : target.clientWidth;
2356
+ const targetH = target === document.body ? window.innerHeight : target.clientHeight;
2357
+ const naive = computeGridCellSize(targetW, targetH, n, cellAR);
2358
+ const naiveShortEdge = Math.min(naive.cellW, naive.cellH);
2359
+ const zoom = Math.min(1, naiveShortEdge / devMinIframeShortEdge);
2360
+ const areaW = targetW / zoom;
2361
+ const areaH = targetH / zoom;
2362
+ const { cols, rows, cellW, cellH } = computeGridCellSize(areaW, areaH, n, cellAR);
2363
+ target.style.cssText = [
2364
+ `margin:0;padding:0;width:${areaW}px;height:${areaH}px;overflow:hidden`,
2365
+ "display:grid",
2366
+ `grid-template-columns:repeat(${cols},${cellW}px)`,
2367
+ `grid-template-rows:repeat(${rows},${cellH}px)`,
2368
+ "justify-content:center",
2369
+ "align-content:center",
2370
+ `gap:${GAP}px`,
2371
+ "font-family:system-ui,sans-serif",
2372
+ "background:#1a1a2e",
2373
+ // 縮小表示は zoom ではなく transform: scale で行う。 zoom は layout プロパティ
2374
+ // なので WebKit では iframe 内部の percentage height / vh の解決に物理 rect
2375
+ // (zoom 適用後) が混入し、 iframe 内 body が viewport より小さくレンダリング
2376
+ // される (Safari で下部に白い letterbox が出る)。 transform は視覚のみで
2377
+ // iframe 内 layout に波及しないため engine 差が出ない。
2378
+ ...zoom < 1 ? [`transform:scale(${zoom})`, "transform-origin:top left"] : []
2379
+ ].join(";");
2380
+ for (const cell of cells) {
2381
+ cell.style.cssText = `width:${cellW}px;height:${cellH}px;display:flex;flex-direction:column;overflow:hidden;`;
2382
+ }
2383
+ };
2384
+ const render = () => {
2385
+ target.innerHTML = "";
2386
+ cells.length = 0;
2387
+ iframeStatusDots.clear();
2388
+ iframeSplashes.clear();
2389
+ iframePredictionWarns.clear();
2390
+ notchAppliers.length = 0;
2391
+ for (let i = 0; i < paneCount(opts); i++) {
2392
+ const pane = paneAt(opts, i);
2393
+ const cell = document.createElement("div");
2394
+ const label = document.createElement("div");
2395
+ const labelColor = pane.seatKind === "admin" ? "#ffb86c" : "#8be9fd";
2396
+ label.style.cssText = `color:${labelColor};font-size:12px;padding:2px 0;flex-shrink:0;background:#16213e;text-align:center;`;
2397
+ const nameRow = document.createElement("div");
2398
+ nameRow.textContent = pane.seatKind === "player" ? `Player ${pane.playerNo} (${pane.id})` : `${pane.seatKind === "admin" ? "\u2699" : "\u{1F441}"} ${pane.nickname} (${pane.id})`;
2399
+ label.appendChild(nameRow);
2400
+ const statusRow = createLifecycleStatusRow();
2401
+ label.appendChild(statusRow.el);
2402
+ const iframe = document.createElement("iframe");
2403
+ iframe.src = buildIframeUrl(opts, i);
2404
+ iframe.style.cssText = "border:none;display:block;position:absolute;inset:0;width:100%;height:100%;";
2405
+ const frameWrap = document.createElement("div");
2406
+ frameWrap.style.cssText = "flex:1;position:relative;min-height:0;overflow:hidden;background:#000;";
2407
+ const screen = document.createElement("div");
2408
+ screen.style.cssText = "position:absolute;overflow:hidden;background:#000;";
2409
+ const splash = document.createElement("div");
2410
+ splash.style.cssText = [
2411
+ "position:absolute;inset:0;z-index:5",
2412
+ "display:flex;align-items:center;justify-content:center",
2413
+ "background:#0b0b16;color:#3a4a6a",
2414
+ "font-family:monospace;font-size:11px;letter-spacing:0.08em",
2415
+ "transition:opacity 0.3s"
2416
+ ].join(";");
2417
+ splash.textContent = "waiting firstFrameReady\u2026";
2418
+ screen.appendChild(iframe);
2419
+ screen.appendChild(splash);
2420
+ if (pane.seatKind !== "admin") {
2421
+ screen.appendChild(
2422
+ createCellChrome(opts, i, false, {
2423
+ toggle: toggleNotchSim,
2424
+ register: (apply) => {
2425
+ apply(notchSimOn);
2426
+ notchAppliers.push(apply);
2427
+ }
2428
+ })
2429
+ );
2430
+ }
2431
+ const predictionPanel = document.createElement("div");
2432
+ predictionPanel.style.cssText = [
2433
+ "display:none;position:absolute;z-index:20;left:6px;right:6px;top:6px",
2434
+ "background:#16213e;border:1px solid #ffb86c;border-radius:6px",
2435
+ "padding:8px 10px;font-family:monospace;font-size:10px;line-height:1.6",
2436
+ "color:#ffb86c;text-align:left;white-space:pre-wrap;cursor:pointer"
2437
+ ].join(";");
2438
+ predictionPanel.addEventListener("click", () => {
2439
+ predictionPanel.style.display = "none";
2440
+ });
2441
+ const notchDecor = createNotchDecor();
2442
+ frameWrap.appendChild(screen);
2443
+ frameWrap.appendChild(notchDecor);
2444
+ frameWrap.appendChild(predictionPanel);
2445
+ statusRow.predictionWarn.addEventListener("click", () => {
2446
+ predictionPanel.style.display = predictionPanel.style.display === "none" ? "block" : "none";
2447
+ });
2448
+ const applyNotch = (on) => {
2449
+ if (pane.seatKind === "admin") on = false;
2450
+ if (on) {
2451
+ screen.style.top = `${SIM_NOTCH.top}px`;
2452
+ screen.style.left = `${SIM_NOTCH.left}px`;
2453
+ screen.style.width = `calc(100% - ${SIM_NOTCH.left + SIM_NOTCH.right}px)`;
2454
+ screen.style.height = `calc(100% - ${SIM_NOTCH.top + SIM_NOTCH.bottom}px)`;
2455
+ screen.style.borderRadius = `${SIM_NOTCH.radius}px`;
2456
+ notchDecor.style.display = "block";
2457
+ } else {
2458
+ screen.style.top = "0";
2459
+ screen.style.left = "0";
2460
+ screen.style.width = "100%";
2461
+ screen.style.height = "100%";
2462
+ screen.style.borderRadius = "0";
2463
+ notchDecor.style.display = "none";
2464
+ }
2465
+ };
2466
+ applyNotch(notchSimOn);
2467
+ notchAppliers.push(applyNotch);
2468
+ cell.appendChild(label);
2469
+ cell.appendChild(frameWrap);
2470
+ cells.push(cell);
2471
+ target.appendChild(cell);
2472
+ if (iframe.contentWindow) {
2473
+ iframeStatusDots.set(iframe.contentWindow, statusRow.dots);
2474
+ iframeSplashes.set(iframe.contentWindow, splash);
2475
+ iframePredictionWarns.set(iframe.contentWindow, {
2476
+ badge: statusRow.predictionWarn,
2477
+ panel: predictionPanel,
2478
+ entries: []
2479
+ });
2480
+ }
2481
+ }
2482
+ layout();
2483
+ };
2484
+ const onParentMessage = (event) => {
2485
+ const dots = iframeStatusDots.get(event.source);
2486
+ if (!dots) return;
2487
+ let data = null;
2488
+ try {
2489
+ data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
2490
+ } catch {
2491
+ return;
2492
+ }
2493
+ if (!data || data.channel !== "sdk") return;
2494
+ const type = data.type;
2495
+ if (type === "ready" || type === "firstFrameReady" || type === "gameReady") {
2496
+ dots[type].style.color = "#5cd6a0";
2497
+ }
2498
+ if (type === "firstFrameReady") {
2499
+ const splash = iframeSplashes.get(event.source);
2500
+ if (splash) {
2501
+ splash.style.opacity = "0";
2502
+ splash.style.pointerEvents = "none";
2503
+ setTimeout(() => splash.remove(), 350);
2504
+ iframeSplashes.delete(event.source);
2505
+ }
2506
+ }
2507
+ if (type === "predictionWarning") {
2508
+ const slot = iframePredictionWarns.get(event.source);
2509
+ const payload = data.payload;
2510
+ if (!slot || typeof payload !== "object" || payload === null) return;
2511
+ if (!("action" in payload) || !("api" in payload)) return;
2512
+ const { action, api } = payload;
2513
+ if (typeof action !== "string" || typeof api !== "string") return;
2514
+ slot.entries.push(`${action} \u2192 ${api}`);
2515
+ const body = [
2516
+ "\u26A0 \u5148\u8AAD\u307F\u4E2D\u306B\u3001\u30B5\u30FC\u30D0\u30FC\u3068\u7D50\u679C\u304C\u4E00\u81F4\u3057\u306A\u3044 API \u304C\u547C\u3070\u308C\u307E\u3057\u305F",
2517
+ "",
2518
+ ...slot.entries.map((e) => ` ${e}`),
2519
+ "",
2520
+ "\u3053\u306E action \u3092 serverOnly() \u3067 wrap \u3059\u308B\u304B\u3001\u5024\u3092 state \u306B\u66F8\u304B\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002",
2521
+ "\u76F4\u3057\u65B9\u306E\u8A73\u7D30\u306F console \u3092\u53C2\u7167\u3002",
2522
+ "",
2523
+ "(\u30AF\u30EA\u30C3\u30AF\u3067\u9589\u3058\u308B)"
2524
+ ].join("\n");
2525
+ slot.badge.textContent = `\u26A0 ${slot.entries.length}`;
2526
+ slot.badge.title = body;
2527
+ slot.badge.style.display = "inline-flex";
2528
+ slot.panel.textContent = body;
2529
+ }
2530
+ };
2531
+ window.addEventListener("message", onParentMessage);
2532
+ window.addEventListener("resize", layout);
2533
+ render();
2534
+ }
2535
+
2536
+ // src/harness/admin-client.ts
2537
+ function attachAdminClient(adminUrl) {
2538
+ const pending = /* @__PURE__ */ new Map();
2539
+ const subscribers = /* @__PURE__ */ new Map();
2540
+ let idCounter = 0;
2541
+ let ws = null;
2542
+ let ready = null;
2543
+ let latestSnapshot = null;
2544
+ const localSnapshotSubs = /* @__PURE__ */ new Set();
2545
+ const localEventSubs = /* @__PURE__ */ new Set();
2546
+ const connect = () => {
2547
+ if (ready) return ready;
2548
+ ready = new Promise((resolve, reject) => {
2549
+ const socket = new WebSocket(adminUrl);
2550
+ ws = socket;
2551
+ socket.addEventListener("open", () => {
2552
+ void call("snapshot_subscribe", "subscribe", {
2553
+ kind: "snapshot"
2554
+ }).catch(() => {
2555
+ });
2556
+ subscribers.set("snapshot_subscribe", (value) => {
2557
+ latestSnapshot = value;
2558
+ localSnapshotSubs.forEach((cb) => {
2559
+ try {
2560
+ cb(value);
2561
+ } catch (err) {
2562
+ console.warn("[__uzu_dev] snapshot listener threw:", err);
2563
+ }
2564
+ });
2565
+ });
2566
+ void call("events_subscribe", "subscribe", {
2567
+ kind: "events"
2568
+ }).catch(() => {
2569
+ });
2570
+ subscribers.set("events_subscribe", (value) => {
2571
+ localEventSubs.forEach((cb) => {
2572
+ try {
2573
+ cb(value);
2574
+ } catch (err) {
2575
+ console.warn("[__uzu_dev] events listener threw:", err);
2576
+ }
2577
+ });
2578
+ });
2579
+ resolve();
2580
+ });
2581
+ socket.addEventListener("message", (ev) => {
2582
+ let msg;
2583
+ try {
2584
+ msg = JSON.parse(ev.data);
2585
+ } catch {
2586
+ return;
48
2587
  }
49
- mountSinglePlayer({
50
- target: document.body,
51
- playerCount: meta.playerCount,
52
- orientation: meta.orientation,
53
- devMinIframeShortEdge: meta.devMinIframeShortEdge,
54
- scenarioUrl: meta.scenarioUrl,
55
- serverBaseUrl: meta.serverBaseUrl,
56
- revisionId: meta.revisionId,
57
- roomKey: meta.roomKey,
58
- players: meta.players,
59
- admin: meta.admin,
60
- spectator: meta.spectator,
61
- }, playerIndex);
2588
+ const id = msg.id;
2589
+ if (msg.type === "result") {
2590
+ const p = pending.get(id);
2591
+ if (p) {
2592
+ p.resolve(msg.value);
2593
+ pending.delete(id);
2594
+ }
2595
+ } else if (msg.type === "error") {
2596
+ const p = pending.get(id);
2597
+ if (p) {
2598
+ p.reject(new Error(msg.error));
2599
+ pending.delete(id);
2600
+ }
2601
+ } else if (msg.type === "missing") {
2602
+ const p = pending.get(id);
2603
+ if (p) {
2604
+ p.resolve(void 0);
2605
+ pending.delete(id);
2606
+ }
2607
+ } else if (msg.type === "event") {
2608
+ const sub = subscribers.get(id);
2609
+ if (sub) sub(msg.value);
2610
+ }
2611
+ });
2612
+ socket.addEventListener("error", () => reject(new Error("admin channel error")));
2613
+ socket.addEventListener("close", () => {
2614
+ ws = null;
2615
+ ready = null;
2616
+ pending.forEach((p) => p.reject(new Error("admin channel closed")));
2617
+ pending.clear();
2618
+ });
2619
+ });
2620
+ return ready;
2621
+ };
2622
+ const nextId = (prefix) => {
2623
+ idCounter++;
2624
+ return `${prefix}-${idCounter}`;
2625
+ };
2626
+ const call = (id, type, body) => {
2627
+ return new Promise((resolve, reject) => {
2628
+ if (!ws) {
2629
+ reject(new Error("admin channel not connected"));
62
2630
  return;
2631
+ }
2632
+ pending.set(id, { resolve, reject });
2633
+ ws.send(JSON.stringify({ type, id, ...body }));
2634
+ });
2635
+ };
2636
+ const callMethod = async (method, args = []) => {
2637
+ await connect();
2638
+ const id = nextId("call");
2639
+ return call(id, "call", { method, args });
2640
+ };
2641
+ const hooks = {
2642
+ getSnapshot: () => latestSnapshot,
2643
+ getRawState: () => latestSnapshot,
2644
+ playerId: () => "host",
2645
+ subscribeSnapshot: (cb) => {
2646
+ localSnapshotSubs.add(cb);
2647
+ return () => {
2648
+ localSnapshotSubs.delete(cb);
2649
+ };
2650
+ },
2651
+ waitForSnapshot: (predicate, options) => {
2652
+ const timeoutMs = options?.timeoutMs ?? 1e4;
2653
+ return new Promise((resolve, reject) => {
2654
+ if (latestSnapshot != null) {
2655
+ try {
2656
+ if (predicate(latestSnapshot)) {
2657
+ resolve(latestSnapshot);
2658
+ return;
2659
+ }
2660
+ } catch (err) {
2661
+ reject(err);
2662
+ return;
2663
+ }
2664
+ }
2665
+ const timer = setTimeout(() => {
2666
+ localSnapshotSubs.delete(listener);
2667
+ reject(new Error(`[__uzu_dev] waitForSnapshot timed out after ${timeoutMs}ms`));
2668
+ }, timeoutMs);
2669
+ const listener = (snap) => {
2670
+ try {
2671
+ if (predicate(snap)) {
2672
+ clearTimeout(timer);
2673
+ localSnapshotSubs.delete(listener);
2674
+ resolve(snap);
2675
+ }
2676
+ } catch (err) {
2677
+ clearTimeout(timer);
2678
+ localSnapshotSubs.delete(listener);
2679
+ reject(err);
2680
+ }
2681
+ };
2682
+ localSnapshotSubs.add(listener);
2683
+ });
2684
+ },
2685
+ setRawState: async (state) => {
2686
+ await callMethod("setRawState", [state]);
2687
+ },
2688
+ mergeRawState: async (patch) => {
2689
+ await callMethod("mergeRawState", [patch]);
2690
+ },
2691
+ patchRawState: async (ops) => {
2692
+ await callMethod("patchRawState", [ops]);
2693
+ },
2694
+ send: async (args) => {
2695
+ await callMethod("sendAction", [args]);
2696
+ },
2697
+ subscribeEvents: (cb) => {
2698
+ localEventSubs.add(cb);
2699
+ return () => {
2700
+ localEventSubs.delete(cb);
2701
+ };
2702
+ },
2703
+ pauseTick: () => {
2704
+ void callMethod("pauseTick");
2705
+ },
2706
+ resumeTick: () => {
2707
+ void callMethod("resumeTick");
2708
+ },
2709
+ stepTick: (n) => {
2710
+ void callMethod("stepTick", [n ?? 1]);
2711
+ },
2712
+ // getSeed / getCurrentTick / isTickPaused は admin channel 越しだと非同期取得になり、
2713
+ // 同期シグネチャでは値が届く前に初期値を返す嘘 API になるため remote proxy では公開しない。
2714
+ // 状態追跡は subscribeSnapshot / waitForSnapshot を使う。
2715
+ reset: (opts) => {
2716
+ void callMethod("reset", [opts]);
63
2717
  }
64
- // admin channel client を接続。 window.__uzu_dev として parent frame に expose する。
65
- const adminUrl = `${meta.serverBaseUrl}/dev/admin`;
66
- const hooks = attachAdminClient(adminUrl);
67
- window.__uzu_dev = hooks;
68
- const stateGetter = () => {
69
- const snap = hooks.getSnapshot();
70
- if (snap == null)
71
- return null;
72
- return { state: snap, serverTime: 0, myId: 'host' };
73
- };
74
- const resetGame = hooks.reset ? () => hooks.reset?.({ seed: 'random' }) : undefined;
75
- mountIframeGrid({
2718
+ };
2719
+ void connect().catch((err) => {
2720
+ console.warn("[__uzu_dev] admin channel connect failed:", err);
2721
+ });
2722
+ return hooks;
2723
+ }
2724
+
2725
+ // src/harness/client-entry.ts
2726
+ function rewriteHostForViewer(meta) {
2727
+ const isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
2728
+ if (isLocal) {
2729
+ return { ...meta, serverBaseUrl: `ws://${location.host}` };
2730
+ }
2731
+ return {
2732
+ ...meta,
2733
+ scenarioUrl: `${location.protocol}//${location.host}/`,
2734
+ serverBaseUrl: `ws://${location.host}`
2735
+ };
2736
+ }
2737
+ async function main() {
2738
+ const res = await fetch("/_uzu_meta.json");
2739
+ if (!res.ok) {
2740
+ console.error("[uzu-harness] failed to fetch meta:", res.status);
2741
+ return;
2742
+ }
2743
+ const rawMeta = await res.json();
2744
+ const meta = rewriteHostForViewer(rawMeta);
2745
+ const playerParam = new URLSearchParams(location.search).get("player");
2746
+ const playerIndex = playerParam == null ? null : Number(playerParam);
2747
+ const panes = paneCount(meta);
2748
+ if (playerIndex != null && Number.isInteger(playerIndex)) {
2749
+ if (playerIndex < 0 || playerIndex >= panes) {
2750
+ document.body.textContent = `player \u306F 0\u301C${panes - 1} \u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`;
2751
+ return;
2752
+ }
2753
+ mountSinglePlayer(
2754
+ {
76
2755
  target: document.body,
77
2756
  playerCount: meta.playerCount,
78
2757
  orientation: meta.orientation,
@@ -83,12 +2762,38 @@ async function main() {
83
2762
  roomKey: meta.roomKey,
84
2763
  players: meta.players,
85
2764
  admin: meta.admin,
86
- spectator: meta.spectator,
87
- // 実機リンク用 host は rewrite 前の meta から取る (localhost 視点の LAN/mDNS 一覧)。
88
- lanHosts: rawMeta.lanHosts,
89
- // 各 cell の UZU ボタンに配線する dev メニュー。
90
- stateGetter,
91
- resetGame,
92
- });
2765
+ spectator: meta.spectator
2766
+ },
2767
+ playerIndex
2768
+ );
2769
+ return;
2770
+ }
2771
+ const adminUrl = `${meta.serverBaseUrl}/dev/admin`;
2772
+ const hooks = attachAdminClient(adminUrl);
2773
+ window.__uzu_dev = hooks;
2774
+ const stateGetter = () => {
2775
+ const snap = hooks.getSnapshot();
2776
+ if (snap == null) return null;
2777
+ return { state: snap, serverTime: 0, myId: "host" };
2778
+ };
2779
+ const resetGame = hooks.reset ? () => hooks.reset?.({ seed: "random" }) : void 0;
2780
+ mountIframeGrid({
2781
+ target: document.body,
2782
+ playerCount: meta.playerCount,
2783
+ orientation: meta.orientation,
2784
+ devMinIframeShortEdge: meta.devMinIframeShortEdge,
2785
+ scenarioUrl: meta.scenarioUrl,
2786
+ serverBaseUrl: meta.serverBaseUrl,
2787
+ revisionId: meta.revisionId,
2788
+ roomKey: meta.roomKey,
2789
+ players: meta.players,
2790
+ admin: meta.admin,
2791
+ spectator: meta.spectator,
2792
+ // 実機リンク用 host は rewrite 前の meta から取る (localhost 視点の LAN/mDNS 一覧)。
2793
+ lanHosts: rawMeta.lanHosts,
2794
+ // 各 cell の UZU ボタンに配線する dev メニュー。
2795
+ stateGetter,
2796
+ resetGame
2797
+ });
93
2798
  }
94
2799
  void main();