@caisual/cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/caisual.mjs +504 -229
  2. package/package.json +1 -1
package/dist/caisual.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/i18n.ts
4
4
  import { promises as fs2 } from "node:fs";
5
- import { join as join2, sep } from "node:path";
5
+ import { join as join2, sep as sep2 } from "node:path";
6
6
 
7
7
  // ../contracts/src/slug.ts
8
8
  var NOMI_RISERVATI = [
@@ -99,6 +99,10 @@ function richiedeServer(manifest) {
99
99
  function modalitaLocale(manifest, mode) {
100
100
  return mode !== null && manifest.modes.some((voce) => voce.id === mode && voce.execution === "local");
101
101
  }
102
+ var AVVISO_ISOLATED = "isolated has no effect: games run inside the portal page, which is not cross-origin isolated";
103
+ function avvisiManifest(value) {
104
+ return typeof value === "object" && value !== null && Object.hasOwn(value, "isolated") ? [AVVISO_ISOLATED] : [];
105
+ }
102
106
  var TETTO_GIOCATORI = 24;
103
107
  var RITARDO_SPETTATORI_MS = 3e3;
104
108
  var MASSIMO_CLASSIFICHE = 32;
@@ -125,6 +129,7 @@ var CAMPI = /* @__PURE__ */ new Set([
125
129
  "players",
126
130
  "lobby",
127
131
  "persistent",
132
+ "replays",
128
133
  "spectators",
129
134
  "boards",
130
135
  "roles",
@@ -328,10 +333,8 @@ function validaManifest(valore) {
328
333
  else network.push(value);
329
334
  }
330
335
  }
331
- let isolated = false;
332
336
  if (dati.isolated !== void 0) {
333
337
  if (typeof dati.isolated !== "boolean") errori.push("isolated: must be a boolean.");
334
- else isolated = dati.isolated;
335
338
  }
336
339
  const requires = {
337
340
  webgl2: false,
@@ -365,7 +368,6 @@ function validaManifest(valore) {
365
368
  errori.push("requires.performance: must be light, medium, or heavy.");
366
369
  } else requires.performance = value.performance;
367
370
  }
368
- if (requires.threads && !isolated) errori.push("requires.threads: needs isolated: true.");
369
371
  }
370
372
  }
371
373
  let players = { min: 1, max: 1 };
@@ -394,6 +396,8 @@ function validaManifest(valore) {
394
396
  if (typeof dati.persistent !== "boolean") errori.push("persistent: must be a boolean.");
395
397
  else persistent = dati.persistent;
396
398
  }
399
+ const replays = dati.replays === true;
400
+ if (dati.replays !== void 0 && typeof dati.replays !== "boolean") errori.push("replays: must be a boolean.");
397
401
  let spectators = { delayMs: RITARDO_SPETTATORI_MS };
398
402
  if (dati.spectators === false || dati.spectators === null) spectators = null;
399
403
  else if (dati.spectators !== void 0 && dati.spectators !== true) {
@@ -668,11 +672,11 @@ function validaManifest(valore) {
668
672
  input,
669
673
  visibility,
670
674
  network,
671
- isolated,
672
675
  requires,
673
676
  players,
674
677
  lobby,
675
678
  persistent,
679
+ replays,
676
680
  spectators,
677
681
  boards,
678
682
  roles,
@@ -682,14 +686,120 @@ function validaManifest(valore) {
682
686
  } };
683
687
  }
684
688
 
689
+ // ../contracts/src/server-wasm.ts
690
+ var MASSIMO_FILE_WASM_SERVER = 8;
691
+ var MASSIMO_BYTE_WASM_SERVER_FILE = 8e6;
692
+ var MASSIMO_BYTE_WASM_SERVER_TOTALI = 16e6;
693
+ function validaBinarioWasmServer(buffer) {
694
+ const wasm = globalThis.WebAssembly;
695
+ if (!wasm.validate(buffer)) return "invalid WebAssembly module.";
696
+ const bytes = new Uint8Array(buffer);
697
+ let indice = 8;
698
+ function numero() {
699
+ let valore = 0;
700
+ let fattore = 1;
701
+ for (let n = 0; n < 10; n++) {
702
+ const byte = bytes[indice++];
703
+ if (byte === void 0) throw new Error("invalid WebAssembly module.");
704
+ valore += (byte & 127) * fattore;
705
+ if (byte < 128) return valore;
706
+ fattore *= 128;
707
+ }
708
+ throw new Error("invalid WebAssembly module.");
709
+ }
710
+ function tipo() {
711
+ const byte = bytes[indice++];
712
+ if (byte === 99 || byte === 100) numero();
713
+ }
714
+ function limiti(memoria) {
715
+ const flags = numero();
716
+ if (memoria && (flags & 2) !== 0) throw new Error("shared memory and threads are not supported by the room server.");
717
+ numero();
718
+ if (flags & 1) numero();
719
+ }
720
+ try {
721
+ while (indice < bytes.length) {
722
+ const sezione = bytes[indice++];
723
+ const lunghezza = numero();
724
+ const fine = indice + lunghezza;
725
+ if (sezione === 2 || sezione === 5) {
726
+ const quanti = numero();
727
+ for (let n = 0; n < quanti; n++) {
728
+ if (sezione === 5) {
729
+ limiti(true);
730
+ continue;
731
+ }
732
+ const modulo = numero();
733
+ indice += modulo;
734
+ const nome = numero();
735
+ indice += nome;
736
+ const genere = bytes[indice++];
737
+ if (genere === 0) numero();
738
+ else if (genere === 1) {
739
+ tipo();
740
+ limiti(false);
741
+ } else if (genere === 2) limiti(true);
742
+ else if (genere === 3) {
743
+ tipo();
744
+ indice++;
745
+ } else if (genere === 4) {
746
+ indice++;
747
+ numero();
748
+ } else throw new Error("unsupported WebAssembly import.");
749
+ }
750
+ }
751
+ indice = fine;
752
+ }
753
+ return null;
754
+ } catch (errore) {
755
+ return errore instanceof Error ? errore.message : "invalid WebAssembly module.";
756
+ }
757
+ }
758
+ function percorsoWasmServerValido(path) {
759
+ return path.endsWith(".wasm") && path.split("/").every((segmento) => segmento !== "." && segmento !== ".." && /^[^\\/%?#\s\u0000-\u001f"'`]+$/.test(segmento));
760
+ }
761
+ function validaFileWasmServer(value) {
762
+ if (!Array.isArray(value)) return { ok: false, errori: ["server.wasm must be an array."] };
763
+ const errori = [];
764
+ if (value.length > MASSIMO_FILE_WASM_SERVER) errori.push("The room server may include at most 8 .wasm files.");
765
+ const files = [];
766
+ const percorsi = /* @__PURE__ */ new Set();
767
+ let totale = 0;
768
+ for (const [indice, raw] of value.entries()) {
769
+ const file = typeof raw === "object" && raw !== null ? raw : {};
770
+ const nome = `server.wasm[${indice}]`;
771
+ if (typeof file.path !== "string" || !percorsoWasmServerValido(file.path)) {
772
+ errori.push(`${nome}.path must be a relative .wasm path inside the game folder, without '..'.`);
773
+ continue;
774
+ }
775
+ if (percorsi.has(file.path)) errori.push(`${file.path}: duplicate server .wasm path.`);
776
+ percorsi.add(file.path);
777
+ if (typeof file.bytes !== "number" || !Number.isSafeInteger(file.bytes) || file.bytes < 0) {
778
+ errori.push(`${nome}.bytes must be a non-negative integer.`);
779
+ } else {
780
+ totale += file.bytes;
781
+ if (file.bytes > MASSIMO_BYTE_WASM_SERVER_FILE) errori.push(`${file.path}: server .wasm files must be at most 8 MB each.`);
782
+ }
783
+ if (typeof file.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(file.sha256)) {
784
+ errori.push(`${nome}.sha256 must be a lowercase SHA-256 hex digest.`);
785
+ }
786
+ if (Object.keys(file).some((campo) => !["path", "bytes", "sha256"].includes(campo))) {
787
+ errori.push(`${nome} may only contain path, bytes and sha256.`);
788
+ }
789
+ files.push(file);
790
+ }
791
+ if (totale > MASSIMO_BYTE_WASM_SERVER_TOTALI) errori.push("The room server may include at most 16 MB of .wasm files in total.");
792
+ return errori.length === 0 ? { ok: true, files } : { ok: false, errori };
793
+ }
794
+
685
795
  // ../contracts/src/server-js.ts
686
- var MASSIMO_BYTE_SERVER_JS = 1e6;
796
+ var MASSIMO_BYTE_SERVER_JS = 4e6;
687
797
  var IMPORT_KIT = /\bimport\s+(?:(?:[$A-Z_a-z][$\w]*\s*,\s*)?(?:\*\s+as\s+[$A-Z_a-z][$\w]*|\{[^{}]*\})|[$A-Z_a-z][$\w]*)\s+from\s+(['"])@caisual\/kit\/server\1\s*;?/g;
688
798
  function spaziCome(value) {
689
799
  return value.replace(/[^\n]/g, " ");
690
800
  }
691
801
  function mascheraTestiECommenti(sorgente) {
692
- const risultato = [...sorgente].map((carattere) => carattere === "\n" ? "\n" : " ");
802
+ const risultato = sorgente.split("").map((carattere) => carattere === "\n" ? "\n" : " ");
693
803
  function copiaCodice(inizio, chiudiSuGraffa) {
694
804
  let profonditaGraffe = chiudiSuGraffa ? 1 : 0;
695
805
  for (let indice = inizio; indice < sorgente.length; indice += 1) {
@@ -749,14 +859,36 @@ function mascheraTestiECommenti(sorgente) {
749
859
  copiaCodice(0, false);
750
860
  return risultato.join("");
751
861
  }
862
+ function importWasmServer(sorgente) {
863
+ const codice = mascheraTestiECommenti(sorgente);
864
+ const imports = [];
865
+ for (const match of codice.matchAll(/\bimport\b/g)) {
866
+ const importazione = /^import\s+([$A-Z_a-z][$\w]*)\s+from\s+(['"])(\.\/[^'"\r\n]+)\2/.exec(sorgente.slice(match.index));
867
+ if (importazione === null || !percorsoWasmServerValido(importazione[3].slice(2))) continue;
868
+ if (/^\s*(?:with|assert)\s*\{/.test(codice.slice(match.index + importazione[0].length))) continue;
869
+ imports.push({ inizio: match.index, fine: match.index + importazione[0].length, nome: importazione[1], path: importazione[3].slice(2) });
870
+ }
871
+ return imports;
872
+ }
873
+ function percorsiWasmServer(sorgente) {
874
+ return [...new Set(importWasmServer(sorgente).map(({ path }) => path))];
875
+ }
752
876
  function validaServerJs(sorgente) {
753
877
  const errori = [];
754
878
  if (new TextEncoder().encode(sorgente).byteLength > MASSIMO_BYTE_SERVER_JS) {
755
- errori.push("server.js must be at most 1 MB.");
879
+ errori.push("server.js must be at most 4 MB.");
880
+ }
881
+ let senzaWasm = sorgente;
882
+ const importsWasm = importWasmServer(sorgente);
883
+ for (const { inizio, fine } of [...importsWasm].reverse()) {
884
+ senzaWasm = senzaWasm.slice(0, inizio) + spaziCome(senzaWasm.slice(inizio, fine)) + senzaWasm.slice(fine);
756
885
  }
757
- const senzaImportKit = sorgente.replace(IMPORT_KIT, (importazione) => spaziCome(importazione));
886
+ if (new Set(importsWasm.map(({ path }) => path)).size > MASSIMO_FILE_WASM_SERVER) {
887
+ errori.push("The room server may include at most 8 .wasm files.");
888
+ }
889
+ const senzaImportKit = senzaWasm.replace(IMPORT_KIT, (importazione) => spaziCome(importazione));
758
890
  const codice = mascheraTestiECommenti(senzaImportKit);
759
- if (/\bmodule\s*\.\s*exports\b|\bexports\s*\./.test(codice)) {
891
+ if (/\bmodule\s*\.\s*exports\b/.test(codice) || /\bexports\s*\./.test(codice.replace(/\.\s*exports\b/g, "."))) {
760
892
  errori.push("server.js must use ESM and cannot use CommonJS exports.");
761
893
  }
762
894
  if (/\brequire\s*\(/.test(codice)) {
@@ -768,7 +900,7 @@ function validaServerJs(sorgente) {
768
900
  const senzaImportMeta = codice.replace(/\bimport\s*\.\s*meta\b/g, "");
769
901
  const riesportaDipendenza = /\bexport\s+(?:\*\s*(?:as\s+[$A-Z_a-z][$\w]*\s*)?|\{[^}]*\})\s+from\b/.test(codice);
770
902
  if (/\bimport\b/.test(senzaImportMeta) || riesportaDipendenza) {
771
- errori.push("server.js may only import from '@caisual/kit/server'.");
903
+ errori.push("server.js may only import from '@caisual/kit/server' or default-import relative .wasm files inside the game folder, without '..'.");
772
904
  }
773
905
  if (!/\bexport\s+default\b/.test(codice)) {
774
906
  errori.push("server.js must have an export default.");
@@ -776,6 +908,12 @@ function validaServerJs(sorgente) {
776
908
  return errori.length === 0 ? { ok: true } : { ok: false, errori };
777
909
  }
778
910
 
911
+ // ../contracts/src/replay.ts
912
+ var REPLAY_MAX_BYTES = 10 * 1024 * 1024;
913
+ var REPLAY_MAX_DURATION_MS = 30 * 60 * 1e3;
914
+ var REPLAY_CHUNK_BYTES = 512 * 1024;
915
+ var REPLAY_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
916
+
779
917
  // ../contracts/src/overlay.ts
780
918
  function validBoardDay(value) {
781
919
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
@@ -802,6 +940,9 @@ function overlayReadOrigin(origin, site, expected) {
802
940
  return site === null || site === "same-origin" || site === "none";
803
941
  }
804
942
 
943
+ // ../contracts/src/room-limits.ts
944
+ var MASSIMO_BYTE_FRAME_STANZA = 64 * 1024;
945
+
805
946
  // ../contracts/src/player.ts
806
947
  var GUEST_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
807
948
  function guestName(id) {
@@ -882,10 +1023,16 @@ function validaImmagine(campo, bytes) {
882
1023
  return null;
883
1024
  }
884
1025
 
1026
+ // ../contracts/src/pubblicazione-limiti.ts
1027
+ var MASSIMO_FILE = 3e3;
1028
+ var MASSIMO_BYTE_TOTALI = 5e8;
1029
+ var MASSIMO_BYTE_FILE = 1e8;
1030
+
885
1031
  // src/bundle.ts
886
1032
  import { promises as fs } from "node:fs";
1033
+ import { createHash } from "node:crypto";
887
1034
  import { builtinModules } from "node:module";
888
- import { isAbsolute, join, relative } from "node:path";
1035
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
889
1036
  import { build } from "esbuild";
890
1037
  var CliError = class extends Error {
891
1038
  exitCode;
@@ -936,7 +1083,31 @@ ${remaining.join("\n")}
936
1083
  export default ${defaultName};
937
1084
  `;
938
1085
  }
1086
+ async function readWasmFiles(root, source) {
1087
+ const realRoot = await fs.realpath(root);
1088
+ const files = [];
1089
+ for (const path of percorsiWasmServer(source)) {
1090
+ try {
1091
+ const absolutePath = await fs.realpath(join(root, path));
1092
+ if (!absolutePath.startsWith(realRoot + sep)) throw new Error("must stay inside the game folder.");
1093
+ const stat = await fs.stat(absolutePath);
1094
+ if (!stat.isFile()) throw new Error("must be a regular file.");
1095
+ const declared = { path, bytes: stat.size, sha256: "0".repeat(64) };
1096
+ const validation = validaFileWasmServer([...files, declared].map(({ path: path2, bytes, sha256: sha2562 }) => ({ path: path2, bytes, sha256: sha2562 })));
1097
+ if (!validation.ok) throw new Error(validation.errori.join("\n"));
1098
+ const content = await fs.readFile(absolutePath);
1099
+ if (content.byteLength !== stat.size) throw new Error("changed while being read. Retry the command.");
1100
+ const binaryError = validaBinarioWasmServer(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
1101
+ if (binaryError !== null) throw new Error(binaryError);
1102
+ files.push({ path, bytes: content.byteLength, sha256: createHash("sha256").update(content).digest("hex"), content });
1103
+ } catch (error) {
1104
+ throw new CliError(2, `${path}: ${error instanceof Error ? error.message : "file not readable."}`);
1105
+ }
1106
+ }
1107
+ return files;
1108
+ }
939
1109
  async function bundleServer(root) {
1110
+ root = await fs.realpath(root);
940
1111
  const serverPath = join(root, "server.js");
941
1112
  let source;
942
1113
  try {
@@ -945,7 +1116,7 @@ async function bundleServer(root) {
945
1116
  throw new CliError(2, "server.js: file not readable.");
946
1117
  }
947
1118
  const directValidation = validaServerJs(source);
948
- if (directValidation.ok) return { source, bundled: false };
1119
+ if (directValidation.ok) return { source, bundled: false, wasm: await readWasmFiles(root, source) };
949
1120
  let output;
950
1121
  try {
951
1122
  const result = await build({
@@ -955,6 +1126,19 @@ async function bundleServer(root) {
955
1126
  platform: "neutral",
956
1127
  target: "es2022",
957
1128
  external: ["@caisual/kit/server"],
1129
+ plugins: [{
1130
+ name: "server-wasm",
1131
+ setup(builder) {
1132
+ builder.onResolve({ filter: /\.wasm$/ }, (args) => {
1133
+ if (!args.path.startsWith("./") || !percorsoWasmServerValido(args.path.slice(2))) {
1134
+ return { errors: [{ text: "Server .wasm imports must use a relative path inside the game folder, without '..'." }] };
1135
+ }
1136
+ const path = relative(root, resolve(args.resolveDir, args.path)).split(sep).join("/");
1137
+ if (!percorsoWasmServerValido(path)) return { errors: [{ text: "Server .wasm files must stay inside the game folder." }] };
1138
+ return { path: `./${path}`, external: true };
1139
+ });
1140
+ }
1141
+ }],
958
1142
  mainFields: ["module", "main"],
959
1143
  conditions: ["workerd", "worker", "import", "default"],
960
1144
  minify: false,
@@ -990,7 +1174,7 @@ ${lines.join("\n")}`);
990
1174
  ${bundledValidation.errori.map((error) => `- ${error}`).join("\n")}`
991
1175
  );
992
1176
  }
993
- return { source: output, bundled: true };
1177
+ return { source: output, bundled: true, wasm: await readWasmFiles(root, output) };
994
1178
  }
995
1179
 
996
1180
  // src/i18n.ts
@@ -999,7 +1183,7 @@ var defaultWarn = (message) => process.stderr.write(`Warning: ${message}
999
1183
  async function readLocalDictionary(clientRoot, language) {
1000
1184
  const root = await fs2.realpath(clientRoot);
1001
1185
  const path = await fs2.realpath(join2(root, "i18n", `${language}.json`));
1002
- if (!path.startsWith(`${root}${sep}`)) throw new Error("The dictionary must be inside client/.");
1186
+ if (!path.startsWith(`${root}${sep2}`)) throw new Error("The dictionary must be inside client/.");
1003
1187
  return JSON.parse(await fs2.readFile(path, "utf8"));
1004
1188
  }
1005
1189
  async function checkGameTexts(clientRoot, manifest, warn = defaultWarn) {
@@ -1044,29 +1228,29 @@ async function checkGameTexts(clientRoot, manifest, warn = defaultWarn) {
1044
1228
  }
1045
1229
 
1046
1230
  // src/caisual.ts
1047
- import { createHash as createHash3 } from "node:crypto";
1231
+ import { createHash as createHash4 } from "node:crypto";
1048
1232
  import { createReadStream, promises as fs4 } from "node:fs";
1049
1233
  import { tmpdir } from "node:os";
1050
- import { basename as basename2, dirname as dirname3, extname as extname2, join as join4, resolve as resolve2 } from "node:path";
1234
+ import { basename as basename2, dirname as dirname3, extname as extname2, join as join4, resolve as resolve3 } from "node:path";
1051
1235
 
1052
1236
  // ../../docs/publish.md
1053
- var publish_default = '# Publish a game on Caisual\n\nCaisual hosts browser games supplied as folders. Each publish creates an immutable version. It becomes current unless another version was activated after the upload opened.\nThe publishing flow supports both single-player and multiplayer games and does not require changes in the Caisual dashboard. Player identity, rooms, cloud saves, leaderboards, and the daily challenge come from the game kit, documented in [kit.md](./kit.md).\n\nA game published with `"overlay": { "version": 1 }` is a standard game: it runs full screen and Caisual draws the menu, the lobby, invitations, friends, matchmaking, spectators, leaderboards, voice, the end of a match and Play again on top of it. Write the field, the HUD and the settings; declare the rest in the manifest. See [Sessions and the standard overlay](./kit.md#sessions-and-the-standard-overlay).\n\n## Game folder\n\nUse this structure:\n\n```text\nmy-game/\n caisual.json\n server.js # optional, required only for multiplayer rooms\n client/\n index.html\n i18n/\n en.json\n it.json\n ...\n```\n\n`caisual.json` and `client/index.html` are required. Put every file used by the game under `client/`.\n\nRun `npx @caisual/cli init my-game` to create a minimal single-player folder with the standard overlay and one local mode. Run `npx @caisual/cli init --multiplayer my-game` to add a room mode with matchmaking and a `server.js`. Both templates include `client/i18n/en.json` used by the example client through `const t = await c.text()`. They are full screen and use `c.session` and `c.overlay`; neither draws a menu or a lobby of its own.\n\nThe whole CLI is:\n\n```text\ncaisual init [--multiplayer | --arcade] [folder]\ncaisual dev [folder] [--port 8790] [--day YYYY-MM-DD] [--latency ms [--jitter ms] [--loss percent]]\ncaisual check [folder] [--json]\ncaisual publish [folder]\ncaisual versions [folder|id]\ncaisual rollback [folder|id] --to <n>\ncaisual unlist [folder|id]\ncaisual relist [folder|id]\ncaisual delete [folder|id] --yes\ncaisual skill\ncaisual --help\ncaisual --version\n```\n\n`caisual init --arcade my-arena` creates a complete online action starter in English and Italian. It includes a shared fixed-step simulation, an authoritative `server.js`, numbered and acknowledged controls, local prediction with reconciliation, remote interpolation, safe HUD placement, standings and fast rematches. All `init` variants also generate `package.json` with `dev`, `test` and optional `test:browser` scripts. See [Responsive action games](./kit.md#responsive-action-games) and [Testing with a browser](/docs/local-development#testing-with-a-browser).\n\n`caisual check [folder] [--json]` runs every local check used by publish: the manifest, game texts, client files, the `server.js` bundle, the three required images, and screenshots. It needs no key and uploads nothing. It validates files without launching or playing the game in a browser. With `--json` it prints a report for tools and agents. It exits with code 2 when the report has errors.\n\n`caisual skill` writes this guide and [kit.md](./kit.md) into `.claude/skills/caisual/SKILL.md` in the current folder and adds a `## Caisual` section to `AGENTS.md`, so an agent working in that repository reads the rules before it starts.\n\n## caisual.json\n\nThe file must contain one JSON object. Unknown fields are rejected. This is a complete single-player example:\n\n```json\n{\n "manifest": 1,\n "id": "my-game",\n "name": "My Game",\n "description": { "en": "A short description of the game." },\n "cover": "cover.png",\n "card": "card.png",\n "icon": "icon.png",\n "screenshots": ["screenshots/level-one.png"],\n "tags": ["puzzle"],\n "languages": ["en", "it"],\n "platform": "both",\n "overlay": { "version": 1, "accent": "#397e83" },\n "orientation": "landscape",\n "input": ["keyboard", "mouse", "touch"],\n "visibility": "public",\n "network": [],\n "isolated": false,\n "requires": { "webgl2": false, "webgpu": false, "wasm": false, "threads": false, "memoryMb": null, "performance": "light" },\n "players": { "min": 1, "max": 1 },\n "lobby": false,\n "persistent": false,\n "spectators": true,\n "boards": { "main": { "source": "client", "label": "Best run", "periods": ["daily", "all-time"] } },\n "roles": [],\n "teams": null,\n "voice": "none",\n "modes": [\n { "id": "solo", "execution": "local", "label": "Solo", "instructions": "One run against the clock." }\n ]\n}\n```\n\n- `manifest` is required and must be `1`.\n- `overlay` is optional and defaults to absent. Set `{ "version": 1 }` to publish a standard game and get the whole overlay. `accent` is optional and must be a six-digit `#RRGGBB` colour; no other CSS is accepted. A game without `overlay` keeps its historical flow and draws its own menus, and nothing in this guide changes for it.\n- `id` is required. Use 3 to 32 lowercase ASCII letters or digits, with single hyphens only between groups. The ID becomes the URL slug. Choose it carefully because it cannot be renamed or reused after deletion.\n- `name` is required and must contain 1 to 60 characters.\n- `description` is optional and defaults to an empty string. It accepts a plain string or a localized object such as `{ "en": "A short description.", "it": "Una breve descrizione." }`; each translation must contain 1-500 characters on one line, with a BCP 47 key listed in `languages`. A plain empty string means no description.\n- `cover`, `card` and `icon` are required, distinct relative file paths inside `client/`. Use PNG, JPEG or WebP, at most 2 MB (2,000,000 bytes) per image. Do not include a query, fragment, empty segment or parent segment. See the exact dimensions below.\n- `screenshots` is optional and defaults to `[]`. It accepts up to 8 relative paths inside `client/`.\n- `tags` is optional and defaults to `[]`. It accepts up to 10 values. Each value uses 1 to 24 lowercase letters, digits, or hyphens.\n- `languages` is required: a non-empty array of distinct BCP 47 tags that includes `en`, such as `["it", "en", "pt-BR"]`. English is always required alongside the game\'s own languages. The first entry remains the default and may be a language other than English. Tags are normalized to canonical casing. The catalog and standard menu show the available languages.\n- `platform` is required. Use `desktop` when the game needs a keyboard, mouse, large display, or desktop performance. Use `mobile` when it is designed only for touch and small screens. Use `both` only after checking that layout, performance, and controls work on both.\n- `orientation` is optional and defaults to `landscape`. Use `landscape` or `portrait` to describe the intended mobile layout. The device may not honor an orientation request.\n- `input` is optional and defaults to `[]`. Include every supported input from `keyboard`, `mouse`, `touch`, and `gamepad`. Do not claim an input until the game is usable with it.\n- `visibility` is optional and defaults to `public`. Use `public` for catalog eligibility or `unlisted` for access by direct link only.\n- `network` is optional and defaults to `[]`. List every external host contacted or loaded by the game, without scheme, port, path, query, or fragment, for example `api.example.com`. If an external host is missing, the browser blocks the request. Keep the array empty when the game uses only its own files and Caisual services.\n- `isolated` is optional and defaults to `false`. Use `true` only when the game requires shared memory or threaded WebAssembly. Every external host in `network` must then send headers compatible with cross-origin isolation.\n- `requires` is optional and defaults to `{ "webgl2": false, "webgpu": false, "wasm": false, "threads": false, "memoryMb": null, "performance": "light" }`. Declare the minimum capabilities the game truly needs to run. For example, a game with a WebGPU renderer and a WebGL2 fallback declares only `webgl2`. Set `threads` together with `isolated: true`. `memoryMb` accepts `null` or a multiple of 256 from 512 to 32768. Use `light`, `medium`, or `heavy` for the expected performance load.\n- `players` is optional and defaults to `{ "min": 1, "max": 1 }`. Both values are integers from 1 to 24 and `max` must be at least `min`. Set the range that a room needs before play can start.\n- `lobby` is optional and defaults to `false`. Use `true` when players must choose roles or teams, mark themselves ready, and wait for the host to start. With `false`, play starts when the first player enters and later players may join in progress. A room mode with resolved `players.max === 1` always starts on entry and bypasses the lobby, including an inherited `lobby: true`.\n- `persistent` is optional and defaults to `false`. Use `true` when room members must be able to return with the same code after disconnecting, including while the game is already playing. Persistent rooms expire after 30 days without activity.\n- `spectators` is optional and defaults to `{ "delayMs": 3000 }`. Use `false` to disable watching, `true` for the default three-second delay, or `{ "delayMs": N }` to choose an integer delay from 0 to 30000 milliseconds.\n- `boards` is optional and defaults to `{}`. Each key is a leaderboard id. Use `{ "source": "server" }` to accept only `room.board.submit`, or `{ "source": "client" }` to allow browser submissions. Boards not listed use `client`. A manifest may list up to 32 boards. `label` is optional text or a language-to-text object, 1 to 48 characters on one line per translation, and names the board in the overlay; without it the overlay shows the id. `periods` is optional and defaults to `["all-time"]`: list `daily`, `all-time` or both, without duplicates. `all-time` means the best score with no day attached, not a sum of days. `periods` only chooses what the overlay offers; it does not change what the score APIs accept. `day` is `submit` by default, or `start` for a server board. With `start`, submit a daily score from the room server with `{ day: room.daily.day }` within 10 minutes of its next UTC midnight. See [Daily challenge](./kit.md#daily-challenge).\n- `roles` is optional and defaults to `[]`. Each entry has an `id` of 1 to 32 lowercase letters, digits, or internal hyphens, a `min` integer from 0 to 24, and an optional `max` in the same range. Rooms enforce these capacities in the lobby. `label` is optional text or a language-to-text object, 1 to 32 characters on one line per translation, and names the role in the overlay lobby; without it the overlay shows the id.\n- `teams` is optional and defaults to `null`. An object has `min` and `max` integers from 2 to 24, with `max` at least `min`. Rooms balance players who do not choose a team.\n- `voice` is optional and defaults to `none`. Use `room` so everyone in the room can hear each other, `team` to restrict voice to teammates, or `proximity` when `server.js` sets the gain between player pairs. Use `none` to disable voice.\n- `modes` is optional and defaults to `[]`. A mode has a unique `id` using 1 to 32 lowercase letters, digits, or internal hyphens. It may have `matchmaking` with `key`, an array of 1 to 8 unique field names, and `timeoutMs`, an integer from 1,000 to 300,000. Each field name uses 1 to 32 lowercase letters, digits, or hyphens and starts with a letter or digit. A mode may also define `players: { min, max }` (both integers from 1 to 24, max at least min) and `lobby` (boolean). Each supplied field replaces its root counterpart for creation, joining and matchmaking, including filling an open room; omitted fields inherit the root value. `players` is replaced as a whole, not merged. `mode: null` uses the root configuration. Roles, teams, voice and persistence remain game-wide. Catalog labels consider the resolved modes, or the root range when there are no modes: Single player, Multiplayer, or Solo + Multiplayer.\n- A standard game declares at least one mode, and every mode of a standard game needs `execution`: `local` for a run inside the browser, `room` for a room. A `local` mode resolves to exactly one player with `lobby` false and no matchmaking; it is not a room of one, and the create and match APIs refuse it. A `room` mode requires `server.js`, checked by the CLI and again when the version is published.\n- `label` is optional text or a language-to-text object, 1 to 48 characters on one line per translation, and names the mode in the standard menu; without it the menu shows the id. `instructions` is optional text or a language-to-text object, 1 to 160 characters on one line per translation, and adds a line under the label. Both are text, never HTML, and resolve to the player\'s language with the fallback described below.\n- `matchmaking.defaults` is required when the overlay is expected to start a search on its own. It holds exactly the fields listed in `key`, with safe integers or strings of 1 to 64 characters from letters, digits, `_ . : -`. Without it a search must come from the game\'s own `c.room.match()` call.\n\nThe CLI prints every manifest error in one run. Fix every listed field and rule before retrying.\n\n`requires` is also available to the game itself through `c.device` in the kit, so the game can show its own warning or pick a lighter renderer. The portal does not gate the Play link on it.\n\n## Game translations\n\nUse this convention for every new game, including games with only one language:\n\n1. Put the supported languages in `languages`, always including `en`, with the default first.\n2. Put game UI strings in `client/i18n/<lang>.json`, with canonical filenames such as `en.json`, `it.json` and `pt-BR.json`. Dictionaries are flat objects with identical keys and string values, including placeholders such as `{n}`.\n3. Call `const t = await c.text()` after `caisual.connect()` and before `c.session.ready()`. Render with `t(\'score\', { n: 3 })`. Use `c.player.language` when formatting dates or numbers.\n4. Translate the description, mode labels and instructions in the manifest. Role and leaderboard labels support the same objects. `name` and `tags` are not localized fields.\n\nFor example, a mode can contain:\n\n```json\n{\n "id": "solo",\n "execution": "local",\n "label": { "en": "Solo", "it": "Da solo" },\n "instructions": { "en": "Light up three lights.", "it": "Accendi tre luci." }\n}\n```\n\nA dictionary at `client/i18n/en.json` can contain:\n\n```json\n{ "score": "Lights: {n} / 3", "done": "Complete" }\n```\n\nSee [Game language and strings](./kit.md#game-language-and-strings) for a complete manifest, two dictionaries and a working client.\n\nThe kit makes one request to the game\'s own origin. The kit first resolves the player\'s ordered preferences against declared game languages, using exact tags, parent tags, then the game\'s default. `c.player.language` is that declared game language; `c.player.uiLanguage` is the overlay\'s locale. Caisual resolves each text key from the selected game language, its parent language tags, then the manifest\'s default language, then the key itself. For `pt-BR` with English as default, that is `pt-BR`, `pt`, `en`, key. The same chain selects localized manifest text from `uiLanguage` in the overlay and descriptions from the page language in the catalogue, game page, creator profiles and invitations, including description metadata; a missing label falls back to its id and missing instructions are omitted. A string continues to appear as written. Translation objects must be non-empty, contain valid language tags and satisfy the original text limits for every value. Empty strings are allowed in game dictionaries, but not in manifest translation objects. Description translations must use languages declared in `languages`; a missing description is omitted.\n\n`caisual dev` at startup and `caisual publish` before any upload check `client/i18n/`. No folder is required for an existing game. If the folder exists, the default language file must exist as a regular file or the command fails. All other dictionary issues produce warnings: invalid JSON, non-string values, non-canonical filenames, missing dictionaries for declared languages, and differing keys. Missing-key warnings compare the union of keys across every usable file, including keys absent from the default. Invalid dictionaries are ignored at runtime. Fix the warnings before sharing the game; they do not block development or publication. Restart dev after changing the manifest to reload its language list and repeat the checks.\n\nDeclare `languages: ["it", "en"]` to keep Italian as the default. Add English game strings and translate the manifest labels and instructions too. Declaring English does not create translations. A manifest without `languages`, or without `en`, is rejected.\n\n## client/index.html\n\n`index.html` must be at the root of `client/`. Use relative URLs such as `./game.js` or `assets/sprite.png`. Do not use root-relative URLs such as `/game.js`, and do not use parent paths that leave the published `client/` tree.\n\nTo use player identity, saves, and leaderboards, import the kit from `/__caisual/kit/v1.js` as shown in [kit.md](./kit.md). The path `/__caisual/` is reserved: do not put game files under it.\n\nA standard game fills the window: `html`, `body` and the game surface are 100% of the viewport, with no maximum width, no header, no footer and no editorial frame, and the document must not scroll at 1366x768 or at 390x844 with safe areas applied. Aim for the playable field to cover at least 70% of the visible area on desktop and 60% on mobile; a board with a fixed aspect ratio uses the geometric exception described in [kit.md](./kit.md#full-screen).\n\nCaisual draws its own controls on top: a pill in the top-right corner, about 44 pixels tall and wider when it carries an invitation, and a compact bar at the end of a match. Exit lives in that pill. The exact positions arrive in the game as `reservedRects` on `c.overlay.onChange`, in CSS pixels of the game viewport, so place the game\'s own HUD outside them rather than guessing a corner. While a panel is open `inputBlocked` is `true`: release held keys and stop reading input, but keep simulating, because a panel never pauses a room.\n\nA game published without `overlay` keeps the historical control instead: a small round Exit button over the top-right corner, 36 pixels, inside the safe area. Keep that corner free of controls.\n\nDo not register a service worker. The game runs in an iframe on its own origin inside `caisual.com`. Test it without assuming access to the parent page, parent cookies, or files outside `client/`.\n\nWhen `voice` is not `none`, the portal grants microphone access to the game iframe. The browser still asks the player for permission when the game calls `room.voice.join()`. Call it from a button click or another user gesture, not automatically when the page loads.\n\n## Multiplayer server\n\nAdd `server.js` beside `caisual.json` when the game uses rooms. It is the ESM entry point and must have an `export default`. It may import local files such as `./logic/ships.js`, including `.js`, `.ts`, and `.json` files, and npm packages installed in the game folder. The CLI bundles these imports into one file both when publishing and when starting `caisual dev`.\n\nA minimal relay server looks like this:\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\n\nexport default defineGame({\n tickRate: 0,\n onMessage(room, player, message) {\n room.broadcast(message);\n },\n});\n```\n\n`tickRate` is required and must be an integer from 0 to 60; `defineGame` throws without it. Use `0` for a server that runs only in response to events. Every callback is optional.\n\nThe file may define the optional room callbacks documented in [kit.md](./kit.md). Server code runs without Node.js APIs or network access. Dynamic `import()`, `require()`, and CommonJS exports are not supported. Use only pure JavaScript packages, such as a noise or vector library. A package that needs an HTTP client is not suitable. The `network` field in `caisual.json` controls only requests made by the browser client.\n\nThe bundled `server.js` may be at most 1,000,000 bytes. Room state must remain plain JSON and may be at most 256 KB when serialized. Each incoming game message may be at most 16 KB. Game messages are limited to 20 per second per connection. Excess messages are dropped; `room.onError` receives `rate_limited` at most once per second across both budgets. Protocol service messages, including ping, lobby actions, state requests and voice signaling, have a separate 20/s budget with the same drop policy. More than 100 attempts in each of three consecutive one-second windows in either budget closes the connection with 4008 `rate_limited`; the kit does not reconnect automatically after this abuse closure. Abuse windows start with the first message on the connection; a normal or empty window resets the sequence. Oversized game frames close with 4009 `message_too_large`. Room save values may be at most 128 KB.\n\nPublish a multiplayer game with the same `npx @caisual/cli publish` command. When imports need bundling, the CLI prints `Bundling server.js (N KB).` The uploaded file is the bundle: the CLI validates it, declares its size and SHA-256 digest, and uploads it separately from browser files. The portal validates the stored bundle again before making the new game version current.\n\nIf the portal finds an invalid `server.js`, the command prints `The multiplayer server could not be published.` followed by diagnostic hints. The failed version is kept for diagnosis but never becomes current. If the game already has a working version, players continue to receive that version. Fix the reported problem and publish again to create a new version.\n\n## Test locally\n\nRun the local preview from the game folder before publishing:\n\n```sh\nnpx @caisual/cli dev\n```\n\nYou can pass a game folder and choose another port:\n\n```sh\nnpx @caisual/cli dev ./my-game --port 8790 --day 2026-09-04\n```\n\nThe optional `--day YYYY-MM-DD` flag pins the UTC date for daily seeds and local daily leaderboards, including room scores. Invalid dates are usage errors; omitting the flag uses today in UTC. Scores persist by day in `.caisual-dev/`, so restarting with another date switches boards without erasing earlier scores. Saves, identities and rooms stay shared; queued room scores keep their assigned day across restarts. Room daily contexts stay fixed at creation, including on restoration; new rooms follow the simulated day. Daily `expiresAt` stays on the next real UTC midnight, so it can be compared with the unchanged server clock. Reload the game after restarting dev. Real clocks and timers are unchanged.\n\nThe command prints a portal URL and a game URL. Open the portal URL. It loads the game in an iframe with the same handshake used after publishing, so `c.connected` is `true`, and it mounts the same standard overlay when the manifest declares one. Player identity, saves, leaderboards, daily data, invitations, and rooms all use local data. Add `?lang=` with any game language to test the manifest resolution, including regional tags such as `pt-BR`. For `?lang=ja` with Japanese declared, `c.player.language` is `ja` and `c.player.uiLanguage` is `en`: the overlay supports en, it, es, fr, de and pt. Without the parameter, the game uses the browser\'s ordered preferences. Friends and parties are marked unavailable locally. Each new browser tab gets a different guest identity, while reloading one tab keeps that tab\'s identity.\n\nOpen `/__caisual/players?n=4` on the printed portal URL for independent guest frames with the full overlay. Each has Drop and Spectate controls. Use `dev --latency 120 --jitter 40 --loss 2` to delay room WebSocket messages in each direction, vary the delay and discard 2% of messages for recovery testing. Latency and jitter accept integer milliseconds from 0 to 60000, loss accepts 0 to 100 percent; jitter and loss require latency. HTTP, matchmaking and audio are unaffected. See [Local dev](/docs/local-development) for precise semantics and frame sizes.\n\nReload after client edits; restart dev after editing `caisual.json`, `server.js`, or server imports, including shared physics. Rooms restore from `.caisual-dev/`, so incompatible state changes may need a new room. There is no automatic reload. If a port is busy, dev suggests a command using a currently available port.\n\nWhen `server.js` exists, room data is stored as JSON under `.caisual-dev/` in the game folder. Without `server.js`, the game remains single player and attempts to create a room return `no_server`.\n\nPress Ctrl+C in the terminal to stop the preview. No account or publish key is required.\n\n## Limits\n\n- At most 2,000 files per version.\n- At most 50,000,000 bytes per file.\n- At most 200,000,000 bytes for all files in one version.\n- At most 1,000,000 bytes for `server.js`.\n- At most 60 versions per publishing key in any 24-hour window. Beyond that the portal answers `publish_rate_limit`.\n- Dotfiles, dot-directories, and directories named `node_modules` are ignored.\n- Symbolic links and other non-regular files are rejected.\n\nReduce or split files that exceed the per-file limit. Remove generated files that the browser does not need.\n\n## Publish\n\nUse the key supplied by the creator. Set it in the environment so it does not enter shell history as a command-line flag:\n\n```sh\nnpx @caisual/cli check\nexport CAISUAL_KEY=\'ck_...\'\nnpx @caisual/cli publish\n```\n\nRun the command from the game folder, or pass the folder path after `publish`. For local portal development only, set `CAISUAL_ORIGIN` to the local HTTP origin.\n\nThe CLI validates the folder, computes every file size and SHA-256 digest, creates a new version, uploads the files, completes the version, and prints the game URL. The stable URL is `https://caisual.com/g/<id>`.\n\nBefore contacting the portal, the CLI scans browser files for common WebGL2, WebGPU, WebAssembly, and shared-memory signatures. A possible mismatch is printed to stderr with a `Warning:` prefix and never blocks publishing. Correct an accurate warning by declaring the minimum matching `requires` field, and set `isolated: true` for shared memory. If the signature belongs to unused code, remove that code from the published client bundle.\n\nThe first games from a new creator are reviewed before they can appear in the public catalog. Their stable links still work while review is pending.\n\n## Versions and rollback\n\nNew games start on the current version. An existing room stays on its own version for its entire life, including players arriving later through an invitation, a typed code, friends, spectators or Resume. Its client, manifest, overlay, iframe permissions and images all come from that version.\n\nEvery `publish` creates a new immutable version, even for a tiny correction. The permanent address stays `/g/<id>`. The home page sorts games by the creation date of the current version; publishing can move a game up and a rollback can move it down.\n\n## Updates while a page is open\n\nCreating a room or starting a matchmaking search from an outdated page fails with `version_outdated` and `currentVersion`. The standard overlay shows **This game was updated** and a **Reload game** button that reloads the portal page on the current version.\n\nJoining or watching an existing room from another version fails with `version_mismatch` and `roomVersion`. For a typed code or Resume, the standard overlay reloads the portal with the invitation so it can load the room\'s version. Spectators keep their watch intent. A room whose version is no longer ready, or whose game was deleted, shows **This room is no longer available**, with a link to the current game.\n\nMatchmaking keeps versions separate. A reservation already accepted before an update can finish on its original version. Existing rooms are not restarted by an update or rollback.\n\nGames with their own menus receive these errors both as rejected room operations and through `c.room.onError(listener)`. Call `c.room.reload()` after either error to load the current game or the room referenced by the failed join or watch:\n\n```js\nc.room.onError((error) => {\n if (error.code === \'version_outdated\' || error.code === \'version_mismatch\') {\n showReloadButton(() => c.room.reload());\n }\n});\n```\n\n## List and restore versions\n\nUse the same `CAISUAL_KEY` environment variable as for publishing:\n\n```sh\ncaisual versions\ncaisual versions ./my-game\ncaisual rollback --to 7\ncaisual rollback my-game --to 7\n```\n\nThe target defaults to the current folder, or accepts a folder or game ID. `versions` lists the number, date, state and size and marks the current version. The account page also marks it.\n\nRollback selects an existing ready version of that game. It copies no files, creates no new number and does not consume the 60-version daily allowance. Visibility and moderation stay unchanged. **Rollback restores code, not saved data or scores.**\n\nIf another version was activated while a publish was uploading, the upload becomes ready but does not replace it. The CLI reports:\n\n```text\nVersion N is ready but not current: version M was activated in the meantime. Run caisual rollback --to N to activate it.\n```\n\nA manifest\'s visibility takes effect only when activation succeeds. An `unlist` or `relist` issued during an upload wins over that upload\'s manifest visibility.\n\n## Game versions and data formats\n\nSaves, leaderboards and `room.shared` belong to the game, not to a game version. The creator is responsible for data compatibility. A game version and a data format version are separate things.\n\nFor a compatible change, keep the same keys. For an incompatible change, use a new key such as `progress_v2`. Import from `progress` only when `progress_v2` is missing. Keep the original, identify the format inside each value and never overwrite a format you do not recognize. Use the same convention for `room.shared`. Progress written later by an old version is not automatically merged into the new key.\n\nFor leaderboards, keep the same ID while results remain comparable. Use a new ID such as `main_s2` when rules, scoring scale or trust rules change. Old rooms continue writing to their old board. Keep each board ID\'s rules consistent across game versions. Scores record the originating game version for diagnostics; this does not automatically split the leaderboard.\n\nThe daily seed does not depend on the game version. Do not change the generator or rules halfway through a UTC day without changing the board ID.\n\n## Retention and local development\n\nGood versions stay. Old ready versions are retained for rooms and rollback, without automatic age-based deletion. Failed uploads are cleaned from both client and server storage; uploads left open for more than 24 hours fail with a note and are cleaned. Version numbers are never reused. Deleting a game removes its client and server files.\n\n`caisual dev` always uses game version 1. It does not simulate publishing, version changes or rollback.\n\n## Update, unlist, or delete\n\nTo update a game, change its files without changing `id`, then run `npx @caisual/cli publish` again. This creates a new version and keeps the same stable game URL.\n\nTo remove the current game from the catalog without publishing a new version, run:\n\n```sh\nCAISUAL_KEY=\'ck_...\' npx @caisual/cli unlist\n```\n\nRestore its public visibility with:\n\n```sh\nCAISUAL_KEY=\'ck_...\' npx @caisual/cli relist\n```\n\nDelete it permanently only when you are certain:\n\n```sh\nCAISUAL_KEY=\'ck_...\' npx @caisual/cli delete --yes\n```\n\nEach command reads the `id` from `caisual.json` in the current folder. You may instead pass a game folder or an ID directly, for example `npx @caisual/cli unlist ./my-game` or `npx @caisual/cli relist my-game`. The publishing key always comes from `CAISUAL_KEY`, never from a flag. Deletion has no interactive prompt, is permanent, removes the stored game files, and never frees the ID for reuse.\n\n## Common errors\n\n- `CAISUAL_KEY is required`: export the creator\'s key in the same shell before publishing or managing a game.\n- `The publish API key is not valid`: create a new key in the account dashboard if the old key expired or was revoked.\n- `game_not_found`: check that the game ID is correct and belongs to the creator represented by `CAISUAL_KEY`; deleted games return the same error.\n- `caisual.json is not valid`: read every reported field and rule, fix all of them, then retry.\n- `client/index.html: file not found`: place `index.html` directly under `client/`, not in a nested build folder.\n- `referenced file not found`: make sure `cover`, `card`, `icon` and every screenshot path match a file under `client/`, including letter case.\n- `file is larger than 50 MB`: compress, reduce, or split the asset and update its references.\n- `upload failed` or a temporary portal error: keep the files unchanged and retry the same publish command. The CLI retries temporary upload failures automatically.\n- `publish_rate_limit`: this key has already created 60 versions in the last 24 hours. Wait until the oldest one leaves the window.\n- `burst_rate_limit`: too many publishing or management requests arrived at once. Wait briefly and retry.\n- `The multiplayer server could not be published`: read every diagnostic hint, fix `server.js`, and publish again. The failed version does not replace the current one.\n- An external browser request works locally but fails after publishing: add its host to `network` and publish a new version. Server code cannot make outbound network requests.\n- A threaded WebAssembly game fails to start: set `isolated` to `true` and verify that every declared external host supports cross-origin isolation.\n\n\n## Required game images\n\nThe look of the game belongs to its creator: templates are deliberately neutral, and their placeholder images must be replaced.\n\nEvery manifest must declare three different files inside `client/`. All three are required PNG, JPEG or WebP images, at most 2 MB (2,000,000 bytes) each:\n\n| Field | Exact size | Use |\n| --- | --- | --- |\n| `cover` | 1536x1024, 3:2 | Featured home card, game page and overlay boot background, social preview, mobile invitation |\n| `card` | 1024x1024, 1:1 | Secondary home cards and square invitation artwork |\n| `icon` | 1024x1024, 1:1 | Game window favicon, overlay, friends and parties |\n\n**No text inside any image:** no title, slogan, letters or numbers. The portal displays the game name. Keep the icon simple enough to read at a small size. The files must be different, including `card` and `icon`. Screenshots keep their existing rules.\n\nThe 3:2 and 1:1 ratios match native formats of widely used image generators. Export at the exact sizes above: Caisual does not crop, stretch or accept a size range. `caisual check` and publish validate the format from the file headers, exact dimensions and weight. The portal repeats these checks for direct publication. The absence of text is an editorial requirement, not an automatic check.\n\nAll three `caisual init` variants include small, real PNG examples at these sizes. Replace them with artwork for your game before sharing it. English (`en`) is always required in `languages`; the first language remains the default, for example `["it", "en"]`.\n';
1237
+ var publish_default = '# Publish a game on Caisual\n\nCaisual hosts browser games supplied as folders. Each publish creates an immutable version. It becomes current unless another version was activated after the upload opened.\nThe publishing flow supports both single-player and multiplayer games and does not require changes in the Caisual dashboard. Player identity, rooms, cloud saves, leaderboards, and the daily challenge come from the game kit, documented in [kit.md](./kit.md).\n\nA game published with `"overlay": { "version": 1 }` is a standard game: it runs full screen and Caisual draws the menu, the lobby, invitations, friends, matchmaking, spectators, leaderboards, voice, the end of a match and Play again on top of it. Write the field, the HUD and the settings; declare the rest in the manifest. See [Sessions and the standard overlay](./kit.md#sessions-and-the-standard-overlay).\n\n## Game folder\n\nUse this structure:\n\n```text\nmy-game/\n caisual.json\n server.js # optional, required only for multiplayer rooms\n client/\n index.html\n i18n/\n en.json\n it.json\n ...\n```\n\n`caisual.json` and `client/index.html` are required. Put every file used by the game under `client/`.\n\nRun `npx @caisual/cli init my-game` to create a minimal single-player folder with the standard overlay and one local mode. Run `npx @caisual/cli init --multiplayer my-game` to add a room mode with matchmaking and a `server.js`. Both templates include `client/i18n/en.json` used by the example client through `const t = await c.text()`. They are full screen and use `c.session` and `c.overlay`; neither draws a menu or a lobby of its own.\n\nThe whole CLI is:\n\n```text\ncaisual init [--multiplayer | --arcade] [folder]\ncaisual dev [folder] [--port 8790] [--day YYYY-MM-DD] [--latency ms [--jitter ms] [--loss percent]]\ncaisual check [folder] [--json]\ncaisual publish [folder]\ncaisual versions [folder|id]\ncaisual rollback [folder|id] --to <n>\ncaisual unlist [folder|id]\ncaisual relist [folder|id]\ncaisual delete [folder|id] --yes\ncaisual skill\ncaisual --help\ncaisual --version\n```\n\n`caisual init --arcade my-arena` creates a complete online action starter in English and Italian. It includes a shared fixed-step simulation, an authoritative `server.js`, numbered and acknowledged controls, local prediction with reconciliation, remote interpolation, safe HUD placement, standings and fast rematches. All `init` variants also generate `package.json` with `dev`, `test` and optional `test:browser` scripts. See [Responsive action games](./kit.md#responsive-action-games) and [Testing with a browser](/docs/local-development#testing-with-a-browser).\n\n`caisual check [folder] [--json]` runs every local check used by publish: the manifest, game texts, client files, the `server.js` bundle, the three required images, and screenshots. It needs no key and uploads nothing. It validates files without launching or playing the game in a browser. With `--json` it prints a report for tools and agents. It exits with code 2 when the report has errors.\n\n`caisual skill` writes this guide, [kit.md](./kit.md) and the index of the guides at [/docs](/docs) into `.claude/skills/caisual/SKILL.md` in the current folder and adds a `## Caisual` section to `AGENTS.md`, so an agent working in that repository reads the rules before it starts. `caisual init` does the same in the new game folder, so a fresh game already carries the skill.\n\n## caisual.json\n\nThe file must contain one JSON object. Unknown fields are rejected. This is a complete single-player example:\n\n```json\n{\n "manifest": 1,\n "id": "my-game",\n "name": "My Game",\n "description": { "en": "A short description of the game." },\n "cover": "cover.png",\n "card": "card.png",\n "icon": "icon.png",\n "screenshots": ["screenshots/level-one.png"],\n "tags": ["puzzle"],\n "languages": ["en", "it"],\n "platform": "both",\n "overlay": { "version": 1, "accent": "#397e83" },\n "orientation": "landscape",\n "input": ["keyboard", "mouse", "touch"],\n "visibility": "public",\n "network": [],\n "requires": { "webgl2": false, "webgpu": false, "wasm": false, "threads": false, "memoryMb": null, "performance": "light" },\n "players": { "min": 1, "max": 1 },\n "lobby": false,\n "persistent": false,\n "spectators": true,\n "boards": { "main": { "source": "client", "label": "Best run", "periods": ["daily", "all-time"] } },\n "roles": [],\n "teams": null,\n "voice": "none",\n "modes": [\n { "id": "solo", "execution": "local", "label": "Solo", "instructions": "One run against the clock." }\n ]\n}\n```\n\n- `manifest` is required and must be `1`.\n- `overlay` is optional and defaults to absent. Set `{ "version": 1 }` to publish a standard game and get the whole overlay. `accent` is optional and must be a six-digit `#RRGGBB` colour; no other CSS is accepted. A game without `overlay` keeps its historical flow and draws its own menus, and nothing in this guide changes for it.\n- `id` is required. Use 3 to 32 lowercase ASCII letters or digits, with single hyphens only between groups. The ID becomes the URL slug. Choose it carefully because it cannot be renamed or reused after deletion.\n- `name` is required and must contain 1 to 60 characters.\n- `description` is optional and defaults to an empty string. It accepts a plain string or a localized object such as `{ "en": "A short description.", "it": "Una breve descrizione." }`; each translation must contain 1-500 characters on one line, with a BCP 47 key listed in `languages`. A plain empty string means no description.\n- `cover`, `card` and `icon` are required, distinct relative file paths inside `client/`. Use PNG, JPEG or WebP, at most 2 MB (2,000,000 bytes) per image. Do not include a query, fragment, empty segment or parent segment. See the exact dimensions below.\n- `screenshots` is optional and defaults to `[]`. It accepts up to 8 relative paths inside `client/`.\n- `tags` is optional and defaults to `[]`. It accepts up to 10 values. Each value uses 1 to 24 lowercase letters, digits, or hyphens.\n- `languages` is required: a non-empty array of distinct BCP 47 tags that includes `en`, such as `["it", "en", "pt-BR"]`. English is always required alongside the game\'s own languages. The first entry remains the default and may be a language other than English. Tags are normalized to canonical casing. The catalog and standard menu show the available languages.\n- `platform` is required. Use `desktop` when the game needs a keyboard, mouse, large display, or desktop performance. Use `mobile` when it is designed only for touch and small screens. Use `both` only after checking that layout, performance, and controls work on both.\n- `orientation` is optional and defaults to `landscape`. Use `landscape` or `portrait` to describe the intended mobile layout. The device may not honor an orientation request.\n- `input` is optional and defaults to `[]`. Include every supported input from `keyboard`, `mouse`, `touch`, and `gamepad`. Do not claim an input until the game is usable with it.\n- `visibility` is optional and defaults to `public`. Use `public` for catalog eligibility or `unlisted` for access by direct link only.\n- `network` is optional and defaults to `[]`. List every external host contacted or loaded by the game, without scheme, port, path, query, or fragment, for example `api.example.com`. If an external host is missing, the browser blocks the request. Keep the array empty when the game uses only its own files and Caisual services.\n- `requires` is optional and defaults to `{ "webgl2": false, "webgpu": false, "wasm": false, "threads": false, "memoryMb": null, "performance": "light" }`. Declare the minimum capabilities the game truly needs to run. For example, a game with a WebGPU renderer and a WebGL2 fallback declares only `webgl2`. Shared memory and threaded WebAssembly are unavailable inside the portal page; use a build without threads. `memoryMb` accepts `null` or a multiple of 256 from 512 to 32768. Use `light`, `medium`, or `heavy` for the expected performance load.\n- `players` is optional and defaults to `{ "min": 1, "max": 1 }`. Both values are integers from 1 to 24 and `max` must be at least `min`. Set the range that a room needs before play can start.\n- `lobby` is optional and defaults to `false`. Use `true` when players must choose roles or teams, mark themselves ready, and wait for the host to start. With `false`, play starts when the first player enters and later players may join in progress. A room mode with resolved `players.max === 1` always starts on entry and bypasses the lobby, including an inherited `lobby: true`.\n- `persistent` is optional and defaults to `false`. Use `true` when room members must be able to return with the same code after disconnecting, including while the game is already playing. Persistent rooms expire after 30 days without activity.\n- `spectators` is optional and defaults to `{ "delayMs": 3000 }`. Use `false` to disable watching, `true` for the default three-second delay, or `{ "delayMs": N }` to choose an integer delay from 0 to 30000 milliseconds.\n- `boards` is optional and defaults to `{}`. Each key is a leaderboard id. Use `{ "source": "server" }` to accept only `room.board.submit`, or `{ "source": "client" }` to allow browser submissions. Boards not listed use `client`. A manifest may list up to 32 boards. `label` is optional text or a language-to-text object, 1 to 48 characters on one line per translation, and names the board in the overlay; without it the overlay shows the id. `periods` is optional and defaults to `["all-time"]`: list `daily`, `all-time` or both, without duplicates. `all-time` means the best score with no day attached, not a sum of days. `periods` only chooses what the overlay offers; it does not change what the score APIs accept. `day` is `submit` by default, or `start` for a server board. With `start`, submit a daily score from the room server with `{ day: room.daily.day }` within 10 minutes of its next UTC midnight. See [Daily challenge](./kit.md#daily-challenge).\n- `roles` is optional and defaults to `[]`. Each entry has an `id` of 1 to 32 lowercase letters, digits, or internal hyphens, a `min` integer from 0 to 24, and an optional `max` in the same range. Rooms enforce these capacities in the lobby. `label` is optional text or a language-to-text object, 1 to 32 characters on one line per translation, and names the role in the overlay lobby; without it the overlay shows the id.\n- `teams` is optional and defaults to `null`. An object has `min` and `max` integers from 2 to 24, with `max` at least `min`. Rooms balance players who do not choose a team.\n- `voice` is optional and defaults to `none`. Use `room` so everyone in the room can hear each other, `team` to restrict voice to teammates, or `proximity` when `server.js` sets the gain between player pairs. Use `none` to disable voice.\n- `modes` is optional and defaults to `[]`. A mode has a unique `id` using 1 to 32 lowercase letters, digits, or internal hyphens. It may have `matchmaking` with `key`, an array of 1 to 8 unique field names, and `timeoutMs`, an integer from 1,000 to 300,000. Each field name uses 1 to 32 lowercase letters, digits, or hyphens and starts with a letter or digit. A mode may also define `players: { min, max }` (both integers from 1 to 24, max at least min) and `lobby` (boolean). Each supplied field replaces its root counterpart for creation, joining and matchmaking, including filling an open room; omitted fields inherit the root value. `players` is replaced as a whole, not merged. `mode: null` uses the root configuration. Roles, teams, voice and persistence remain game-wide. Catalog labels consider the resolved modes, or the root range when there are no modes: Single player, Multiplayer, or Solo + Multiplayer.\n- A standard game declares at least one mode, and every mode of a standard game needs `execution`: `local` for a run inside the browser, `room` for a room. A `local` mode resolves to exactly one player with `lobby` false and no matchmaking; it is not a room of one, and the create and match APIs refuse it. A `room` mode requires `server.js`, checked by the CLI and again when the version is published.\n- `label` is optional text or a language-to-text object, 1 to 48 characters on one line per translation, and names the mode in the standard menu; without it the menu shows the id. `instructions` is optional text or a language-to-text object, 1 to 160 characters on one line per translation, and adds a line under the label. Both are text, never HTML, and resolve to the player\'s language with the fallback described below.\n- `matchmaking.defaults` is required when the overlay is expected to start a search on its own. It holds exactly the fields listed in `key`, with safe integers or strings of 1 to 64 characters from letters, digits, `_ . : -`. Without it a search must come from the game\'s own `c.room.match()` call.\n\nThe CLI prints every manifest error in one run. Fix every listed field and rule before retrying.\n\n`requires` is also available to the game itself through `c.device` in the kit, so the game can show its own warning or pick a lighter renderer. The portal does not gate the Play link on it.\n\n## Game translations\n\nUse this convention for every new game, including games with only one language:\n\n1. Put the supported languages in `languages`, always including `en`, with the default first.\n2. Put game UI strings in `client/i18n/<lang>.json`, with canonical filenames such as `en.json`, `it.json` and `pt-BR.json`. Dictionaries are flat objects with identical keys and string values, including placeholders such as `{n}`.\n3. Call `const t = await c.text()` after `caisual.connect()` and before `c.session.ready()`. Render with `t(\'score\', { n: 3 })`. Use `c.player.language` when formatting dates or numbers.\n4. Translate the description, mode labels and instructions in the manifest. Role and leaderboard labels support the same objects. `name` and `tags` are not localized fields.\n\nFor example, a mode can contain:\n\n```json\n{\n "id": "solo",\n "execution": "local",\n "label": { "en": "Solo", "it": "Da solo" },\n "instructions": { "en": "Light up three lights.", "it": "Accendi tre luci." }\n}\n```\n\nA dictionary at `client/i18n/en.json` can contain:\n\n```json\n{ "score": "Lights: {n} / 3", "done": "Complete" }\n```\n\nSee [Game language and strings](./kit.md#game-language-and-strings) for a complete manifest, two dictionaries and a working client.\n\nThe kit makes one request to the game\'s own origin. The kit first resolves the player\'s ordered preferences against declared game languages, using exact tags, parent tags, then the game\'s default. `c.player.language` is that declared game language; `c.player.uiLanguage` is the overlay\'s locale. Caisual resolves each text key from the selected game language, its parent language tags, then the manifest\'s default language, then the key itself. For `pt-BR` with English as default, that is `pt-BR`, `pt`, `en`, key. The same chain selects localized manifest text from `uiLanguage` in the overlay and descriptions from the page language in the catalogue, game page, creator profiles and invitations, including description metadata; a missing label falls back to its id and missing instructions are omitted. A string continues to appear as written. Translation objects must be non-empty, contain valid language tags and satisfy the original text limits for every value. Empty strings are allowed in game dictionaries, but not in manifest translation objects. Description translations must use languages declared in `languages`; a missing description is omitted.\n\n`caisual dev` at startup and `caisual publish` before any upload check `client/i18n/`. No folder is required for an existing game. If the folder exists, the default language file must exist as a regular file or the command fails. All other dictionary issues produce warnings: invalid JSON, non-string values, non-canonical filenames, missing dictionaries for declared languages, and differing keys. Missing-key warnings compare the union of keys across every usable file, including keys absent from the default. Invalid dictionaries are ignored at runtime. Fix the warnings before sharing the game; they do not block development or publication. Restart dev after changing the manifest to reload its language list and repeat the checks.\n\nDeclare `languages: ["it", "en"]` to keep Italian as the default. Add English game strings and translate the manifest labels and instructions too. Declaring English does not create translations. A manifest without `languages`, or without `en`, is rejected.\n\n## client/index.html\n\n`index.html` must be at the root of `client/`. Use relative URLs such as `./game.js` or `assets/sprite.png`. Do not use root-relative URLs such as `/game.js`, and do not use parent paths that leave the published `client/` tree.\n\nTo use player identity, saves, and leaderboards, import the kit from `/__caisual/kit/v1.js` as shown in [kit.md](./kit.md). The path `/__caisual/` is reserved: do not put game files under it.\n\nA standard game fills the window: `html`, `body` and the game surface are 100% of the viewport, with no maximum width, no header, no footer and no editorial frame, and the document must not scroll at 1366x768 or at 390x844 with safe areas applied. Aim for the playable field to cover at least 70% of the visible area on desktop and 60% on mobile; a board with a fixed aspect ratio uses the geometric exception described in [kit.md](./kit.md#full-screen).\n\nCaisual draws its own controls on top: a pill in the top-right corner, about 44 pixels tall and wider when it carries an invitation, and a compact bar at the end of a match. Exit lives in that pill. The exact positions arrive in the game as `reservedRects` on `c.overlay.onChange`, in CSS pixels of the game viewport, so place the game\'s own HUD outside them rather than guessing a corner. While a panel is open `inputBlocked` is `true`: release held keys and stop reading input, but keep simulating, because a panel never pauses a room.\n\nA game published without `overlay` keeps the historical control instead: a small round Exit button over the top-right corner, 36 pixels, inside the safe area. Keep that corner free of controls.\n\nDo not register a service worker. The game runs in an iframe on its own origin inside `caisual.com`. Test it without assuming access to the parent page, parent cookies, or files outside `client/`.\n\nWhen `voice` is not `none`, the portal grants microphone access to the game iframe. The browser still asks the player for permission when the game calls `room.voice.join()`. Call it from a button click or another user gesture, not automatically when the page loads.\n\n## Multiplayer server\n\nAdd `server.js` beside `caisual.json` when the game uses rooms. It is the ESM entry point and must have an `export default`. It may import local files such as `./logic/ships.js`, including `.js`, `.ts`, and `.json` files, and npm packages installed in the game folder. The CLI bundles these imports into one file both when publishing and when starting `caisual dev`. Relative default imports of `.wasm` stay external and their files are copied separately.\n\nA minimal relay server looks like this:\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\n\nexport default defineGame({\n tickRate: 0,\n onMessage(room, player, message) {\n room.broadcast(message);\n },\n});\n```\n\n`tickRate` is required and must be an integer from 0 to 60; `defineGame` throws without it. Use `0` for a server that runs only in response to events. Every callback is optional.\n\nThe file may define the optional room callbacks documented in [kit.md](./kit.md). Server code runs without Node.js APIs or network access. Dynamic `import()`, `require()`, and CommonJS exports are not supported. Use only pure JavaScript packages, such as a noise or vector library. A package that needs an HTTP client is not suitable. The `network` field in `caisual.json` controls only requests made by the browser client.\n\nThe final bundle may only import `@caisual/kit/server` and default-import `.wasm` files, for example `import engine from \'./physics/add.wasm\'`. Paths stay inside the game folder without `..` segments. The value is an already compiled `WebAssembly.Module`; use `new WebAssembly.Instance(engine, imports)` in `onCreate` or on first use. No shared memory or threads. At most 8 files are allowed, 8,000,000 bytes per file and 16,000,000 bytes total. No manifest field is added: `requires.wasm` still describes the browser client. Use this only for existing engines. Budget for compilation on room wake, because a larger binary delays resumption; recreate instances lazily and restore their state after a wake. See [WebAssembly on the server](./kit.md#webassembly-on-the-server).\n\nThe bundled `server.js` may be at most 4,000,000 bytes. Room state must remain plain JSON and may be at most 512 KB when serialized. Game messages are limited to 64 KB per frame in either direction and 30 per second per connection. Incoming service frames also have a 64 KB limit; state synchronization carries the separately limited room state. Excess messages are dropped; `room.onError` receives `rate_limited` at most once per second across both budgets. Protocol service messages, including ping, lobby actions, state requests and voice signaling, have a separate 30/s budget with the same drop policy. More than 150 attempts in each of three consecutive one-second windows in either budget closes the connection with 4008 `rate_limited`; the kit does not reconnect automatically after this abuse closure. Abuse windows start with the first message on the connection; a normal or empty window resets the sequence. Oversized game frames close with 4009 `message_too_large`. Room save values may be at most 256 KB.\n\nPublish a multiplayer game with the same `npx @caisual/cli publish` command. When imports need bundling, the CLI prints `Bundling server.js (N KB).` The uploaded file is the bundle: the CLI validates it, declares its size and SHA-256 digest, and uploads it separately from browser files. Each imported binary is declared in the optional `server.wasm` array as `{ path, bytes, sha256 }`, uploaded to its `serverWasmUploads` URL with Content-Length, and verified with SHA-256. The files stay in the private server archive and follow the version retention rules. The portal validates the stored bundle, the imported file list and the binaries again before making the new game version current.\n\nIf the portal finds an invalid `server.js`, the command prints `The multiplayer server could not be published.` followed by diagnostic hints. The failed version is kept for diagnosis but never becomes current. If the game already has a working version, players continue to receive that version. Fix the reported problem and publish again to create a new version.\n\n## Test locally\n\nRun the local preview from the game folder before publishing:\n\n```sh\nnpx @caisual/cli dev\n```\n\nYou can pass a game folder and choose another port:\n\n```sh\nnpx @caisual/cli dev ./my-game --port 8790 --day 2026-09-04\n```\n\nThe optional `--day YYYY-MM-DD` flag pins the UTC date for daily seeds and local daily leaderboards, including room scores. Invalid dates are usage errors; omitting the flag uses today in UTC. Scores persist by day in `.caisual-dev/`, so restarting with another date switches boards without erasing earlier scores. Saves, identities and rooms stay shared; queued room scores keep their assigned day across restarts. Room daily contexts stay fixed at creation, including on restoration; new rooms follow the simulated day. Daily `expiresAt` stays on the next real UTC midnight, so it can be compared with the unchanged server clock. Reload the game after restarting dev. Real clocks and timers are unchanged.\n\nThe command prints a portal URL and a game URL. Open the portal URL. It loads the game in an iframe with the same handshake used after publishing, so `c.connected` is `true`, and it mounts the same standard overlay when the manifest declares one. Player identity, saves, leaderboards, daily data, invitations, and rooms all use local data. Add `?lang=` with any game language to test the manifest resolution, including regional tags such as `pt-BR`. For `?lang=ja` with Japanese declared, `c.player.language` is `ja` and `c.player.uiLanguage` is `en`: the overlay supports en, it, es, fr, de and pt. Without the parameter, the game uses the browser\'s ordered preferences. Friends and parties are marked unavailable locally. Each new browser tab gets a different guest identity, while reloading one tab keeps that tab\'s identity.\n\nOpen `/__caisual/players?n=4` on the printed portal URL for independent guest frames with the full overlay. Each has Drop and Spectate controls. Use `dev --latency 120 --jitter 40 --loss 2` to delay room WebSocket messages in each direction, vary the delay and discard 2% of messages for recovery testing. Latency and jitter accept integer milliseconds from 0 to 60000, loss accepts 0 to 100 percent; jitter and loss require latency. HTTP, matchmaking and audio are unaffected. See [Local dev](/docs/local-development) for precise semantics and frame sizes.\n\nReload after client edits; restart dev after editing `caisual.json`, `server.js`, or server imports, including shared physics. Rooms restore from `.caisual-dev/`, so incompatible state changes may need a new room. There is no automatic reload. If a port is busy, dev suggests a command using a currently available port.\n\nWhen `server.js` exists, room data is stored as JSON under `.caisual-dev/` in the game folder. Without `server.js`, the game remains single player and attempts to create a room return `no_server`.\n\nPress Ctrl+C in the terminal to stop the preview. No account or publish key is required.\n\n## Limits\n\n- At most 3,000 files per version.\n- At most 100,000,000 bytes per file.\n- At most 500,000,000 bytes for all files in one version.\n- At most 4,000,000 bytes for `server.js`.\n- At most 60 versions per publishing key in any 24-hour window. Beyond that the portal answers `publish_rate_limit`.\n- Dotfiles, dot-directories, and directories named `node_modules` are ignored.\n- Symbolic links and other non-regular files are rejected.\n\nReduce or split files that exceed the per-file limit. Remove generated files that the browser does not need.\n\n## Publish\n\nUse the key supplied by the creator. Set it in the environment so it does not enter shell history as a command-line flag:\n\n```sh\nnpx @caisual/cli check\nexport CAISUAL_KEY=\'ck_...\'\nnpx @caisual/cli publish\n```\n\nRun the command from the game folder, or pass the folder path after `publish`. For local portal development only, set `CAISUAL_ORIGIN` to the local HTTP origin.\n\nThe CLI validates the folder, computes every file size and SHA-256 digest, creates a new version, uploads the files, completes the version, and prints the game URL. The stable URL is `https://caisual.com/g/<id>`.\n\nBefore contacting the portal, the CLI scans browser files for common WebGL2, WebGPU, WebAssembly, and shared-memory signatures. A possible mismatch is printed to stderr with a `Warning:` prefix and never blocks publishing. Correct an accurate warning by declaring the minimum matching `requires` field, and use a build without threads if shared memory is detected. If the signature belongs to unused code, remove that code from the published client bundle.\n\nThe first games from a new creator are reviewed before they can appear in the public catalog. Their stable links still work while review is pending.\n\n## Versions and rollback\n\nNew games start on the current version. An existing room stays on its own version for its entire life, including players arriving later through an invitation, a typed code, friends, spectators or Resume. Its client, manifest, overlay, iframe permissions and images all come from that version.\n\nEvery `publish` creates a new immutable version, even for a tiny correction. The permanent address stays `/g/<id>`. The home page sorts games by the creation date of the current version; publishing can move a game up and a rollback can move it down.\n\n## Updates while a page is open\n\nCreating a room or starting a matchmaking search from an outdated page fails with `version_outdated` and `currentVersion`. The standard overlay shows **This game was updated** and a **Reload game** button that reloads the portal page on the current version.\n\nJoining or watching an existing room from another version fails with `version_mismatch` and `roomVersion`. For a typed code or Resume, the standard overlay reloads the portal with the invitation so it can load the room\'s version. Spectators keep their watch intent. A room whose version is no longer ready, or whose game was deleted, shows **This room is no longer available**, with a link to the current game.\n\nMatchmaking keeps versions separate. A reservation already accepted before an update can finish on its original version. Existing rooms are not restarted by an update or rollback.\n\nGames with their own menus receive these errors both as rejected room operations and through `c.room.onError(listener)`. Call `c.room.reload()` after either error to load the current game or the room referenced by the failed join or watch:\n\n```js\nc.room.onError((error) => {\n if (error.code === \'version_outdated\' || error.code === \'version_mismatch\') {\n showReloadButton(() => c.room.reload());\n }\n});\n```\n\n## List and restore versions\n\nUse the same `CAISUAL_KEY` environment variable as for publishing:\n\n```sh\ncaisual versions\ncaisual versions ./my-game\ncaisual rollback --to 7\ncaisual rollback my-game --to 7\n```\n\nThe target defaults to the current folder, or accepts a folder or game ID. `versions` lists the number, date, state and size and marks the current version. The account page also marks it.\n\nRollback selects an existing ready version of that game. It copies no files, creates no new number and does not consume the 60-version daily allowance. Visibility and moderation stay unchanged. **Rollback restores code, not saved data or scores.**\n\nIf another version was activated while a publish was uploading, the upload becomes ready but does not replace it. The CLI reports:\n\n```text\nVersion N is ready but not current: version M was activated in the meantime. Run caisual rollback --to N to activate it.\n```\n\nA manifest\'s visibility takes effect only when activation succeeds. An `unlist` or `relist` issued during an upload wins over that upload\'s manifest visibility.\n\n## Game versions and data formats\n\nSaves, leaderboards and `room.shared` belong to the game, not to a game version. The creator is responsible for data compatibility. A game version and a data format version are separate things.\n\nFor a compatible change, keep the same keys. For an incompatible change, use a new key such as `progress_v2`. Import from `progress` only when `progress_v2` is missing. Keep the original, identify the format inside each value and never overwrite a format you do not recognize. Use the same convention for `room.shared`. Progress written later by an old version is not automatically merged into the new key.\n\nFor leaderboards, keep the same ID while results remain comparable. Use a new ID such as `main_s2` when rules, scoring scale or trust rules change. Old rooms continue writing to their old board. Keep each board ID\'s rules consistent across game versions. Scores record the originating game version for diagnostics; this does not automatically split the leaderboard.\n\nThe daily seed does not depend on the game version. Do not change the generator or rules halfway through a UTC day without changing the board ID.\n\n## Retention and local development\n\nGood versions stay. Old ready versions are retained for rooms and rollback, without automatic age-based deletion. Failed uploads are cleaned from both client and server storage; uploads left open for more than 24 hours fail with a note and are cleaned. Version numbers are never reused. Deleting a game removes its client and server files.\n\n`caisual dev` always uses game version 1. It does not simulate publishing, version changes or rollback.\n\n## Update, unlist, or delete\n\nTo update a game, change its files without changing `id`, then run `npx @caisual/cli publish` again. This creates a new version and keeps the same stable game URL.\n\nTo remove the current game from the catalog without publishing a new version, run:\n\n```sh\nCAISUAL_KEY=\'ck_...\' npx @caisual/cli unlist\n```\n\nRestore its public visibility with:\n\n```sh\nCAISUAL_KEY=\'ck_...\' npx @caisual/cli relist\n```\n\nDelete it permanently only when you are certain:\n\n```sh\nCAISUAL_KEY=\'ck_...\' npx @caisual/cli delete --yes\n```\n\nEach command reads the `id` from `caisual.json` in the current folder. You may instead pass a game folder or an ID directly, for example `npx @caisual/cli unlist ./my-game` or `npx @caisual/cli relist my-game`. The publishing key always comes from `CAISUAL_KEY`, never from a flag. Deletion has no interactive prompt, is permanent, removes the stored game files, and never frees the ID for reuse.\n\n## Common errors\n\n- `CAISUAL_KEY is required`: export the creator\'s key in the same shell before publishing or managing a game.\n- `The publish API key is not valid`: create a new key in the account dashboard if the old key expired or was revoked.\n- `game_not_found`: check that the game ID is correct and belongs to the creator represented by `CAISUAL_KEY`; deleted games return the same error.\n- `caisual.json is not valid`: read every reported field and rule, fix all of them, then retry.\n- `client/index.html: file not found`: place `index.html` directly under `client/`, not in a nested build folder.\n- `referenced file not found`: make sure `cover`, `card`, `icon` and every screenshot path match a file under `client/`, including letter case.\n- `file is larger than 100 MB`: compress, reduce, or split the asset and update its references.\n- `upload failed` or a temporary portal error: keep the files unchanged and retry the same publish command. The CLI retries temporary upload failures automatically.\n- `publish_rate_limit`: this key has already created 60 versions in the last 24 hours. Wait until the oldest one leaves the window.\n- `burst_rate_limit`: too many publishing or management requests arrived at once. Wait briefly and retry.\n- `The multiplayer server could not be published`: read every diagnostic hint, fix `server.js`, and publish again. The failed version does not replace the current one.\n- An external browser request works locally but fails after publishing: add its host to `network` and publish a new version. Server code cannot make outbound network requests.\n- A threaded WebAssembly game fails to start: use a build without threads. Games embedded in the portal cannot use shared memory.\n\n\n## Required game images\n\nThe look of the game belongs to its creator: templates are deliberately neutral, and their placeholder images must be replaced.\n\nEvery manifest must declare three different files inside `client/`. All three are required PNG, JPEG or WebP images, at most 2 MB (2,000,000 bytes) each:\n\n| Field | Exact size | Use |\n| --- | --- | --- |\n| `cover` | 1536x1024, 3:2 | Featured home card, game page and overlay boot background, social preview, mobile invitation |\n| `card` | 1024x1024, 1:1 | Secondary home cards and square invitation artwork |\n| `icon` | 1024x1024, 1:1 | Game window favicon, overlay, friends and parties |\n\n**No text inside any image:** no title, slogan, letters or numbers. The portal displays the game name. Keep the icon simple enough to read at a small size. The files must be different, including `card` and `icon`. Screenshots keep their existing rules.\n\nThe 3:2 and 1:1 ratios match native formats of widely used image generators. Export at the exact sizes above: Caisual does not crop, stretch or accept a size range. `caisual check` and publish validate the format from the file headers, exact dimensions and weight. The portal repeats these checks for direct publication. The absence of text is an editorial requirement, not an automatic check.\n\nAll three `caisual init` variants include small, real PNG examples at these sizes. Replace them with artwork for your game before sharing it. English (`en`) is always required in `languages`; the first language remains the default, for example `["it", "en"]`.\n';
1054
1238
 
1055
1239
  // ../../docs/kit.md
1056
- var kit_default = '# Caisual game kit\n\nThe kit gives a published game a stable player identity, cloud saves, leaderboards, a daily challenge seed, and multiplayer rooms with server-owned state.\n\nThe kit is a single JavaScript module with no dependencies. It never touches the page: the game keeps its own rendering, input, and UI.\n\n## Load the kit\n\nEvery published game can import the kit from its own origin, without a bundler and without an npm install:\n\n```html\n<script type="module">\n import { caisual } from \'/__caisual/kit/v1.js\';\n\n const c = await caisual.connect();\n console.log(c.player.name);\n</script>\n```\n\nGames built with a bundler can install the same module from npm:\n\n```sh\nnpm install @caisual/kit\n```\n\n```js\nimport { caisual } from \'@caisual/kit\';\n```\n\nBoth forms expose the same API. The module also sets `globalThis.caisual` for classic scripts that load it first.\n\nThe path `/__caisual/` is reserved on every game origin. Do not put game files under it.\n\n## Connect\n\n```js\nconst c = await caisual.connect();\n```\n\n`connect()` completes when the game is running inside caisual.com and has received its player identity, or after a short timeout when it is not. Calling it again returns the same promise.\n\n- `c.connected` is `true` inside caisual.com and `false` when the game runs on its own, for example from a local folder during development or when its files are copied elsewhere.\n- `c.player` is `{ id, name, guest, language, uiLanguage }`. `id` is stable for the player across sessions and across every version of the game. `name` is the account username, or a stable `Guest-XXXX` name derived from the player id. The four-character suffix uses `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, excluding I, O, 0 and 1; it helps distinguish guests but is not a unique identifier. `guest` is `true` for players without an account. `language` selects game strings; `uiLanguage` is the overlay locale. On first sign-in to an account without a player identity, the browser guest is adopted with its existing id, saves and scores. An account that already has a player identity uses that identity instead; guest data is not merged.\n- When not connected, `c.player` has `id: "local"`, `name: "Guest"`, `guest: true`, plus both language fields. If the host answered, its language information is kept; without a handshake, `language` is the normalized `navigator.language` (or `en`) and `uiLanguage` is its overlay fallback.\n\nDo not store the ticket or reimplement the handshake. The kit handles identity, renewal, and retries.\n\n## Game language and strings\n\n`c.player.language` is the game\'s language; `c.player.uiLanguage` is the overlay\'s locale. Use the former for game strings and formatting, or the latter when intentionally aligning text with the overlay.\n\nThe kit resolves the player\'s explicit portal language choice, otherwise `navigator.languages` in order, against `manifest.languages`. For each preference it tries the exact tag and then its parent tags; if none of the preferences match, it uses the game\'s first declared language. The result is always a normalized declared tag. For example, `ja-JP` with `languages: ["en", "ja"]` selects `ja`, while `uiLanguage` is `en`. The overlay supports English, Italian, Spanish, French, German and Portuguese; it keeps regional tags in those families, such as `pt-BR`, and falls back to `en` for other languages. Games may declare languages outside these six.\n\nA localized portal URL counts as a language choice. The language selector remembers explicit choices, including English; without either, the browser\'s ordered preferences apply. The handshake keeps its legacy `language` field for existing kits, and also sends `uiLanguage`, `languagePreferences` and `gameLanguages`. The kit resolves the game language, including when player services fail after a successful handshake.\n\nWithout a handshake, no manifest is available: `language` is the raw preference from `navigator.language`, normalized as a BCP 47 tag, or `en` if invalid or unavailable. It is not restricted to declared game languages. `uiLanguage` uses the overlay fallback. In `caisual dev`, `?lang=ja` selects `ja` when the manifest declares it, while the overlay stays in English. Without `?lang=`, dev uses `navigator.languages` in order for the game.\n\nPut all game UI strings in flat JSON dictionaries named `client/i18n/<lang>.json`. Use canonical BCP 47 filenames, for example `en.json`, `it.json`, `pt.json`, `pt-BR.json`. Every value is a string; keys are identical across dictionaries. Text can contain named placeholders such as `{n}`.\n\nDeclare supported languages in `caisual.json`, always including `en`. English is required alongside the game\'s own languages; the first entry is the default and may be another language. This complete manifest supports a local game:\n\n```json\n{\n "manifest": 1,\n "cover": "cover.png",\n "card": "card.png",\n "icon": "icon.png",\n "id": "three-lights",\n "name": "Three Lights",\n "platform": "both",\n "languages": ["en", "it"],\n "overlay": { "version": 1 },\n "modes": [{\n "id": "solo",\n "execution": "local",\n "label": { "en": "Solo", "it": "Da solo" },\n "instructions": { "en": "Light up three lights.", "it": "Accendi tre luci." }\n }]\n}\n```\n\n`client/i18n/en.json`:\n\n```json\n{ "score": "Lights: {n} / 3", "light": "Light up", "done": "Complete" }\n```\n\n`client/i18n/it.json`:\n\n```json\n{ "score": "Luci: {n} / 3", "light": "Accendi", "done": "Tutte accese!" }\n```\n\nLoad once during setup, before `c.session.ready()`. The returned function is synchronous and can be used in every draw call:\n\n```html\n<!doctype html>\n<html>\n<head><meta charset="utf-8"><title>Three Lights</title></head>\n<body style="margin:0;min-height:100dvh;display:grid;place-content:center">\n <p id="score"></p>\n <button id="light" disabled></button>\n <script type="module">\n import { caisual } from \'/__caisual/kit/v1.js\';\n const c = await caisual.connect();\n const t = await c.text();\n document.documentElement.lang = c.player.language;\n const score = document.querySelector(\'#score\');\n const light = document.querySelector(\'#light\');\n let n = 0, blocked = false, active = !c.session.capabilities.overlay;\n function draw() {\n score.textContent = n === 3 ? t(\'done\') : t(\'score\', { n });\n light.textContent = t(\'light\');\n light.disabled = blocked || !active || n === 3;\n }\n c.overlay.onChange((view) => { blocked = view.inputBlocked; draw(); });\n c.session.onChange((session) => {\n if (session.kind === \'local\' && session.status === \'playing\') n = 0;\n active = !c.session.capabilities.overlay || (session.kind === \'local\' && session.status === \'playing\');\n draw();\n });\n light.onclick = () => {\n n += 1;\n if (n === 3 && c.session.current.kind === \'local\') c.session.finish();\n draw();\n };\n draw();\n c.session.ready();\n </script>\n</body>\n</html>\n```\n\n`c.text(): Promise<Text>` makes one request to the game\'s own origin, tied to the version currently open. Caisual and `caisual dev` read the matching files and merge them per key: `pt-BR` then `pt` then the manifest\'s default language. Longer tags fall back through their parent tags, such as `zh-Hant-TW`, `zh-Hant`, `zh`. The default file is tried once. If no file contains a key, `t` returns that key. Empty strings are valid translations.\n\nConcurrent and later `c.text()` calls share the same promise and translator for that connection. There are no dependencies, eager downloads, or per-call network requests. Missing files, invalid dictionaries and network failures do not prevent startup. If the Caisual text service is unavailable, for example on a plain static host outside Caisual, the translator returns keys; it does not probe other URLs. Use `caisual dev` to preview the complete convention.\n\n`t(\'score\', { n: 3 })` replaces named placeholders with strings or numbers. An omitted placeholder stays unchanged, such as `{n}`. The result is plain text, with no HTML processing, plural rules or automatic translation. Set `textContent` or draw it on the canvas; do not insert it as HTML. A new document gets the host\'s current language and a fresh translator.\n\n`description`, mode `label` and `instructions`, role `label`, and leaderboard `label` accept a string or a language-to-text object, and resolve from `uiLanguage` in the overlay using the same fallback chain. Descriptions also resolve from the page language in the catalogue, game page, profiles, invitations and metadata. Each description translation must contain 1-500 characters on one line and use a key declared in `languages`. `name` and `tags` keep their existing forms. Existing single-string labels stay unchanged. See [manifest languages and validation](./publish.md#game-translations) for CLI checks and migration from `language`.\n\n## Sessions and the standard overlay\n\nA game that declares `overlay` in `caisual.json` is a standard game: it runs full screen and Caisual draws everything around it. The opening menu, the mode choice, the lobby with roles, teams and ready, invitations, friends and parties, matchmaking, spectators, leaderboards, voice, the end of a match and Play again belong to the platform. The game keeps the field, its own HUD and its own settings.\n\n```json\n{ "overlay": { "version": 1, "accent": "#397e83" } }\n```\n\nTwo objects appear on the connection. `c.session` says which session the game is in, `c.overlay` says when the platform is on top of it.\n\n```js\nconst c = await caisual.connect();\n\nconst stopSession = c.session.onChange((session) => {\n detachGameListeners();\n if (session.kind === \'idle\') return showAttractScene();\n if (session.kind === \'local\') return showLocalRun(session.mode, session.status);\n attachGameListeners(session.room, session.kind === \'watch\');\n draw(session.room.state);\n});\n\nconst stopOverlay = c.overlay.onChange(({ inputBlocked, reservedRects }) => {\n clearHeldKeys();\n setInputEnabled(!inputBlocked);\n placeHudOutside(reservedRects);\n});\n\nawait loadAssetsAndChosenView();\nc.session.ready();\n```\n\n### The session\n\n`c.session.current` reads the session at once. `onChange` repeats the current value immediately to every new listener, returns a function that removes it, and then reports attaches, detaches and the end of a local run. It never fires for a move or a roster change: those stay on the room listeners.\n\n- `{ kind: \'idle\' }`: no session. Show an attract scene, not a menu.\n- `{ kind: \'local\', id, mode, status }`: a run of a mode declared with `"execution": "local"`. `status` is `playing` or `ended`.\n- `{ kind: \'room\', id, room }`: `room` is the `Room` documented below, already attached.\n- `{ kind: \'watch\', id, room }`: `room` is a `Spectate`. Draw it read only.\n\n`id` changes on every attach, so a second local run is distinguishable from the first.\n\n`c.session.ready()` says the game has loaded its assets and installed its listeners. Call it once, at the end of setup: until then the overlay waits instead of starting a session under a game that is still downloading. It is idempotent.\n\n`c.session.finish()` ends a local run and shows the compact end bar with a Play again action. It applies only to `kind: \'local\'`: on a room it fails with `not_local` and on idle it does nothing. The result of an online match comes from the server, never from `finish()`.\n\n`c.session.capabilities` is `{ local, rooms, overlay, requestRole }`. Outside caisual.com `overlay` and `rooms` are `false` while `local` stays `true`, which is the signal to run the game\'s own offline fallback. No fake room is created.\n\nTwo front ends of the same game share one session. Switching view does not call `ready()` again and does not detach the room: the new renderer reads `c.session.current` and draws.\n\n### The overlay on top\n\n`c.overlay.onChange` repeats the current geometry immediately, then on every change:\n\n- `inputBlocked` is `true` while a panel is open. Release held keys and stop reading input, but keep simulating: opening a panel never pauses a room.\n- `reservedRects` is an array of up to eight `{ x, y, width, height }` rectangles in CSS pixels of the game viewport. Keep the game\'s own HUD out of them, and nothing else: never resize, move, or letterbox the field because of them. The overlay sits on top of the game and the game must not shift when a bar or a pill appears. The field under a closed overlay stays visible and clickable.\n\nThe complete `OverlayView` value is `{ inputBlocked, reservedRects, safeArea?: { top, right, bottom, left }, shortcutEnabled? }`.\n\nThe host measures `safeArea: { top, right, bottom, left }` in CSS pixels of the game viewport, accounting for the iframe\'s position, borders and scale. It updates after viewport changes, including rotation. Use these values for HUD margins: `env(safe-area-inset-*)` inside the iframe usually reads zero. The kit also sets `--caisual-safe-top`, `--caisual-safe-right`, `--caisual-safe-bottom` and `--caisual-safe-left` on the game document root:\n\n```css\n.hud {\n top: max(16px, var(--caisual-safe-top, 0px));\n left: max(16px, var(--caisual-safe-left, 0px));\n}\n```\n\n`safeArea` is optional for compatibility with older hosts; treat a missing value as four zeros. Older kits keep receiving their supported geometry fields. During the opening screen, `inputBlocked` is true from the first view, including before the first host measurement.\n\n\n`c.overlay.open(panel)` asks the platform to open one of `home`, `room`, `invite`, `friends`, `voice`, `boards`. It is a request, not a permission: it creates no room and grants nothing. Outside caisual.com it does nothing.\n\nShift+Tab from the field opens the menu and Escape closes it. Text fields inside the game keep their own shortcut.\n\n### What a standard game no longer builds\n\nRemove these and let the overlay do them:\n\n- a start menu with Create, Join or a code field;\n- invitation links, copy buttons and share sheets;\n- the lobby: roster, ready, role and team pickers, the Start button;\n- a matchmaking screen with its cancel button;\n- a friends or party list;\n- voice buttons;\n- a leaderboard screen;\n- an Exit or Back to Caisual button;\n- a Play again button after a match.\n\nThe end bar recognizes optional standings and winners; the game may draw additional result details inside the field. `c.room.create`, `join`, `match` and `watch` stay available for a game that wants its own entry point: under a standard overlay the room they return becomes the current session and the standard controls follow it. Do not attach a second room controller from a second renderer.\n\n### Full screen\n\nA standard game fills the window. `html`, `body` and the game surface are 100% of the viewport: no maximum width, no header, no footer, no editorial frame, and no document scrolling at 1366x768 or 390x844, safe areas included. Only the HUD and compact controls sit over the scene.\n\nAim for the playable field to cover at least 70% of the visible area on desktop and 60% on mobile, counting only what shows or controls the game. A board with a fixed aspect ratio cannot always reach that: a square board on a 1366x768 window tops out near 56% before any HUD. That is the declared geometric exception: the board must then fill at least 90% of the largest rectangle that fits the area left free, and the HUD must have a stated ceiling, typically 48 to 64 pixels on desktop and about 160 pixels of controls on a phone.\n\n### Resume\n\nCaisual keeps one resume reference per game and per player, in a save key it owns. Leaving through the overlay with Leave for now stores the room code and detaches without giving up the seat; the standard menu then offers Resume, which rejoins from that code. Leave room removes the reference and gives up the seat, and so does a terminal room end. A `finished` room waiting for a rematch keeps its resume reference. A network drop keeps it.\n\nA game does not read or write that key, and does not build its own Resume button. A reference that is no longer valid returns the service error and the overlay explains it: it does not retry forever. Resume carries the room code, not a promise to reopen the same published version of the game.\n\n### Voice and leaderboards\n\nIn a standard game the overlay\'s voice panel carries Join, Leave, Mute, and the list of who is in the call, with the click the browser requires. A standard game does not draw its own voice buttons. The `room.voice` API below remains for games without the standard overlay and for server-side gain and proximity rules.\n\nLeaderboards are read by the overlay from the published manifest, using the boards and periods declared there. The overlay reads the official verified scores after a submission and offers Refresh: a game does not need a board screen. Scores are still submitted by the game or, better, by `server.js`.\n\n### Known gaps\n\nThree things are deliberately not in this version, and a game should not work around them:\n\n- resolving the original game version behind a persistent Resume;\n- inviting one friend straight into a room, as opposed to a party;\n- matchmaking for a whole group at once.\n\n## Daily challenge\n\n`c.daily` describes the day selected when the connection opened. `day`, `seed` and `expiresAt` stay fixed for that connection, including across UTC midnight. `expiresAt` is the next UTC midnight in milliseconds on the server clock.\n\n```js\nc.daily.day;\nc.daily.seed;\nc.daily.expiresAt;\nconst random = c.daily.rng();\nconst stop = c.daily.onChange(({ day, seed, expiresAt }) => {\n offerNextDailyRun({ day, seed, expiresAt });\n});\n```\n\n`onChange` reports a new `{ day, seed, expiresAt }` when the current UTC day changes. It returns an unsubscribe function and does not immediately replay the initial value. A suspended browser or a connection failure can delay the notification; the kit retries and reports the latest day when it can. The event does not mutate `c.daily` or reseed either generator. `rng()` always creates a fresh generator from the connection\'s original seed, and `random()` keeps advancing the original shared generator. Use the event\'s context explicitly for a new daily run, or open a new document. Repeated `connect()` calls return the same connection.\n\n`day` is a UTC date such as `"2026-09-04"`; `seed` is an unsigned 32-bit integer shared by all players of that game on that day. Both generators produce values in [0, 1). `c.time.now()` is in milliseconds aligned with the portal clock. Without a connection, daily data uses the local clock and hostname, with the same listener and generator behavior.\n\nOn the server, `room.daily` is `{ day, seed, expiresAt }`, fixed at **room creation**, persisted across sleep and rematches. Its seed matches a client connection opened for that game on the creation day. A client connected yesterday can therefore have a different `c.daily` from a room created today: send the room\'s daily context through game state when rendering its course. To switch a persistent or rematched room to a new daily course, create a new room.\n\nBoards accept `"day": "submit" | "start"` in the manifest. Omission means `submit`, preserving submission-day scoring. With `start`, which requires `source: "server"`, submit `room.board.submit(player, \'run\', score, { day: room.daily.day })`. An explicit `day` implies a daily score. It must equal the room\'s creation day and arrive no later than **10 minutes after `room.daily.expiresAt`**, including the exact boundary. A run started at 23:58 and submitted at 00:04 is attributed to its starting day. The all-time score is a separate submission without `day` or `daily`.\n\n| Error code | Meaning |\n| --- | --- |\n| `board_day_required` | A daily score on a start-day board omitted `day`. |\n| `board_day_mismatch` | `day` differs from `room.daily.day`. |\n| `board_day_expired` | More than 10 minutes have elapsed after the room\'s daily expiry. |\n| `board_day_policy` | An explicit day was used on a submit-day board or with `daily: false`. |\n\nThese server calls throw synchronously; handle expected errors inside the callback if the game should continue. Accepted scores keep their assigned day and submission timestamp across delayed writes and retries. Client scores continue to use the day when the portal receives them and cannot choose a day.\n\n`caisual dev --day YYYY-MM-DD` fixes the simulated day and seed for new connections and rooms. Clocks stay real: `expiresAt` is the next **real** UTC midnight at connection or room creation, so comparing it with `c.time.now()` or `room.time.now()` remains valid even for a simulated date. Restored rooms keep their original daily context. Restart dev and reload to change the flag; create a new room for the newly selected day.\n\n## Saves\n\nEach player has up to 32 saves per game. A save is any JSON value up to 256 KB when serialized.\n\n```js\nawait c.save.set(\'slot1\', { level: 3, coins: 120 }); // -> { key, bytes, updatedAt }\nconst data = await c.save.get(\'slot1\'); // -> the value, or null\nawait c.save.remove(\'slot1\');\nconst saves = await c.save.list(); // -> [{ key, bytes, updatedAt }]\n```\n\n- Keys use 1 to 32 characters: lowercase letters, digits, `_` or `-`, starting with a letter or digit.\n- `updatedAt` is a millisecond timestamp.\n- Saves are per player and per game. Another game cannot read them.\n- When not connected, saves go to the browser\'s local storage on the game origin.\n\nErrors reject the promise with an `Error` whose `code` is one of `invalid_request`, `not_found`, `save_limit`, `payload_too_large`, `rate_limited`, `invalid_ticket`, `internal_error`, or `offline`.\n\n## Leaderboards\n\nA leaderboard is identified by a board id chosen by the game. Scores are non-negative integers and higher is better. Each player keeps one entry per board, and one per board per day for daily boards: the best score is kept.\n\n```js\nconst result = await c.board.submit(\'main\', 1234);\n// -> { accepted: true, best: 1234, rank: 7, day: null, verified: false }\n\nconst daily = await c.board.submit(\'main\', 1234, { daily: true });\n// -> { accepted: true, best: 1234, rank: 7, day: "2026-09-04", verified: false }\n\nconst top = await c.board.top(\'main\', { daily: true, limit: 10 });\n// -> { day: "2026-09-04", entries: [{ rank, name, score, guest, me, verified }], me: { rank, score, verified } | null }\n```\n\n- Board ids use the same format as save keys.\n- `submit` never rejects because of connectivity. When the game is not connected it resolves `{ accepted: false, reason: "offline" }`.\n- `best` is the score kept for this player after the submission, which can be higher than the submitted one.\n- `rank` counts players with a strictly higher score. Ties are ordered by who reached the score first.\n- Accounts and guests are ranked separately. `top()` returns account players by default; pass `guests: true` to list guests instead. `me` always refers to the current player within their own category, even beyond `limit`.\n- `limit` is 1 to 100 and defaults to 10.\n- Pass `day: "2026-09-06"` to `top()` to read exactly that UTC day, even after midnight. A day implies the daily filter. A date that is not a real `YYYY-MM-DD` is rejected.\n- `verified` is `true` when the kept score came from the room server. Browser scores cannot replace a verified score.\n- Add `"boards": { "main": { "source": "server" } }` to `caisual.json` for a server-only board. It accepts scores only from `room.board.submit` in `server.js`.\n- An omitted board has `source: "client"`. Browser submissions keep working for existing games.\n- A browser submission to a server-only board rejects with `board_server_only`.\n\n### Verified scores in a single-player game\n\nA verified solo game uses a `room` mode with one place. The server validates each move and computes the score. This complete example accepts each of three cells once; a client cannot submit a score or claim the same cell twice. The standard overlay provides Play again. The optional client button below calls the same `restart()` API when no overlay is available.\n\n`caisual.json`:\n\n```json\n{\n "manifest": 1,\n "cover": "cover.png",\n "card": "card.png",\n "icon": "icon.png", "id": "verified-cells", "name": "Verified Cells", "platform": "both",\n "languages": ["en"], "overlay": { "version": 1 },\n "modes": [{ "id": "solo", "execution": "room", "players": { "min": 1, "max": 1 }, "lobby": false }],\n "boards": { "run": { "source": "server", "day": "start", "label": "Cells", "periods": ["daily", "all-time"] } }\n}\n```\n\n`server.js`:\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\n\nfunction reset(room) {\n room.state = { cells: [false, false, false], score: 0, daily: { ...room.daily } };\n}\nexport default defineGame({\n tickRate: 0,\n onCreate: reset,\n onRestart: reset,\n onMessage(room, player, message) {\n if (room.status !== \'playing\' || player.role === \'spectator\') return;\n const cell = message?.cell;\n if (!Number.isInteger(cell) || cell < 0 || cell > 2 || room.state.cells[cell]) return;\n room.state.cells[cell] = true;\n room.state.score += 1;\n if (room.state.score !== 3) return;\n const result = { standings: [{ playerId: player.id, score: room.state.score }], winners: [player.id], unit: \'points\' };\n room.board.submit(player, \'run\', room.state.score);\n if (room.time.now() > room.daily.expiresAt + 10 * 60_000) {\n // La corsa scaduta conserva il record assoluto; una stanza nuova scegliera\' il nuovo giorno.\n room.end({ ...result, data: { dailyExpired: true } });\n return;\n }\n room.board.submit(player, \'run\', room.state.score, { day: room.daily.day });\n room.end(result, { rematch: true });\n },\n});\n```\n\n`client/index.html`:\n\n```html\n<!doctype html>\n<html lang="en">\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">\n<title>Verified Cells</title>\n<style>body{margin:0;min-height:100dvh;display:grid;place-content:center;gap:12px;background:#000000;color:#ffffff;font:16px system-ui}button{min-width:48px;min-height:48px}</style>\n<p id="score"></p><div id="cells"></div><button id="again" hidden>Play again</button>\n<script type="module">\n import { caisual } from \'/__caisual/kit/v1.js\';\n const c = await caisual.connect();\n const score = document.querySelector(\'#score\'), cells = document.querySelector(\'#cells\'), again = document.querySelector(\'#again\');\n let room = null, blocked = false, stops = [];\n function draw() {\n cells.replaceChildren();\n if (!room) { score.textContent = \'Choose Play to start.\'; again.hidden = true; return; }\n score.textContent = `${room.state.score} / 3`;\n room.state.cells.forEach((claimed, cell) => {\n const button = document.createElement(\'button\'); button.textContent = claimed ? \'\u2713\' : String(cell + 1);\n button.disabled = blocked || room.connection !== \'connected\' || room.status !== \'playing\' || claimed;\n button.onclick = () => room.send({ cell }); cells.append(button);\n });\n again.hidden = c.session.capabilities.overlay || room.status !== \'finished\';\n again.disabled = blocked || room.connection !== \'connected\';\n }\n again.onclick = () => { again.disabled = true; room.restart(); };\n c.overlay.onChange((view) => { blocked = view.inputBlocked; draw(); });\n function attach(next) {\n stops.forEach((stop) => stop()); stops = []; room = next;\n if (room) stops.push(room.onState(draw), room.onStatus(draw), room.onConnection(draw));\n draw();\n }\n c.session.onChange((session) => attach(session.kind === \'room\' ? session.room : null));\n c.session.ready();\n if (!c.session.capabilities.overlay && c.connected) attach(await c.room.create({ mode: \'solo\' }));\n</script>\n</html>\n```\n\n`room.board.submit(\'run\', score, { day: room.daily.day })` is also accepted in a solo room: the sole non-spectator member is implicit. In multiplayer, always pass `player` or its id first; omitting it throws `board_player_required`. `room.board.submit` queues the score; use `room.onScoreQueued` and the overlay\'s board refresh to observe it, not a state transition as a storage receipt. A rematch keeps the room\'s creation day; a new room selects the current day.\n\n## Device\n\n`c.device` contains the browser and device report collected while `connect()` runs:\n\n```ts\ninterface DeviceReport {\n webgl2: boolean;\n webgpu: boolean;\n wasm: boolean;\n threads: boolean;\n isolated: boolean;\n gpu: \'hardware\' | \'software\' | \'none\';\n memoryMb: number | null;\n cores: number | null;\n mobile: boolean;\n tier: \'low\' | \'mid\' | \'high\';\n}\n```\n\nUse capability fields to choose a renderer, then use `tier` to reduce pixel ratio and quality on smaller devices:\n\n```js\nconst renderer = c.device.webgpu\n ? createWebGpuRenderer()\n : createWebGl2Renderer();\n\nconst pixelRatio = c.device.tier === \'high\' ? devicePixelRatio : 1;\nconst quality = c.device.tier === \'low\' ? \'low\' : \'high\';\nrenderer.configure({ pixelRatio, quality });\n```\n\nThe probe takes at most 1.5 seconds. `memoryMb` and `cores` are `null` when the browser does not expose them. The report stays in the browser and is not saved or sent to Caisual.\n\n### Two front ends, one game\n\nKeep one `client/index.html`, one game ID and one server. With `platform: "both"`, choose separate front ends in that entry without navigating or adding another iframe:\n```js\nimport { caisual } from \'/__caisual/kit/v1.js\';\nconst c = await caisual.connect();\nlet preference = null;\ntry { preference = localStorage.getItem(\'layout\'); } catch {}\nconst touch = preference === \'touch\' || (preference !== \'desktop\' && (c.device.mobile || matchMedia(\'(pointer: coarse)\').matches));\nconst screen = touch ? await import(\'./touch/main.js\') : await import(\'./desktop/main.js\');\nscreen.mount({ c, root: document.querySelector(\'#app\') });\n```\nOffer a manual layout choice, persist it when storage is available, and keep rules and room connections shared. Both front ends use relative asset paths inside `client/`.\n\n## Game version changes\n\nNew games use the current version. Existing rooms retain their version for their whole life, including invitations, typed codes, friends, spectators and Resume. Matchmaking never mixes versions; an accepted reservation can finish on its original version. Saves, boards and `room.shared` remain per game, with data compatibility handled by the creator. See [Versions and rollback](./publish.md#versions-and-rollback).\n\nA room create or match operation rejects with `version_outdated` and `currentVersion` when the loaded game is no longer current. A join, watch or reconnect rejects with `version_mismatch` and `roomVersion` when the room uses another version. The standard overlay shows **This game was updated** with **Reload game** for outdated pages; a mismatched code or Resume reloads the portal with the room invitation, preserving watch intent for spectators.\n\nCustom menus can subscribe with `c.room.onError(listener)`, which returns an unsubscribe function. The error has `code`, `message`, and the applicable version number. The operation also rejects with that error. `c.room.reload()` reloads the portal on the current game after `version_outdated`, or on the last mismatched room after `version_mismatch`. Show a button calling it after either error. This works without the standard overlay.\n\n`caisual dev` always uses game version 1 and does not simulate version changes.\n\n## Rooms\n\nA room brings players into the same running game. Creating and joining require a published `server.js`; single-player games can ignore `c.room`.\n\nIn a standard game the overlay creates, joins, matches and watches on the player\'s behalf, and hands the game the room through `c.session`. The calls below stay available, and their result becomes the current session. Read them for what the room object offers; do not rebuild the entry screens around them. An optional [MatchResult](#recognized-match-results) in `room.end` lets the overlay display winners and standings while `room.result` keeps the original JSON.\n\n```js\nconst c = await caisual.connect();\n\nc.room.invited; // invitation code from the game page, or null\n\nconst room = await c.room.create({ mode: null });\n// Or join the invitation that opened the game:\nconst invitedRoom = await c.room.join();\n// Or enter a code supplied by the player:\nconst codedRoom = await c.room.join(\'ABC234\');\n\nroom.code;\nroom.seed; // unsigned 32-bit integer fixed for this room\nroom.tickRate;\nroom.latency;\nroom.invite(); // { code: "ABC234", url: "https://caisual.com/r/ABC234" }\n```\n\nPass a mode id from the manifest to `create({ mode })`, or `null` to use the root configuration. Optional `players` and `lobby` on that mode replace the root values; joining keeps the configuration of the room being joined. `join()` uses `c.room.invited`; without an invitation, pass the six-character code explicitly. A standard game does not need `invite()`: the overlay owns the invitation panel and the copy action.\n\n### Crew\n\nThe kit automatically reports the player\'s current room to the Caisual portal, so the player\'s friends can join with one click. The game does not need to send or handle anything for this. There is no `c.crew` API in this version. In a standard game the friends and party list is a panel of the overlay, so there is nothing to draw either.\n\n### Matchmaking\n\nUse `c.room.match()` to find players who requested the same mode and key. The key must contain exactly the fields declared by that mode\'s `matchmaking.key` in `caisual.json`.\n\n```js\nconst room = await c.room.match({\n mode: \'daily\',\n key: { day: c.daily.day, stage: 3 },\n onWaiting({ players, min, max }) {\n showQueue(`${players}/${max} players, ${min} required`);\n },\n});\n```\n\nMatchmaking uses the selected mode\'s resolved `players` and `lobby`, and that mode\'s `matchmaking.timeoutMs`. A room opens as soon as the queue reaches the resolved `players.max`. When `matchmaking.timeoutMs` expires, it also opens if at least `players.min` players are waiting. Otherwise the promise rejects with `no_match`, and the game should offer the player another option. A new search first tries to fill a matching room that is already open and can still accept players.\n\nPass an `AbortSignal` as `signal` to let the player cancel a search. Cancellation rejects with `cancelled`. In a standard game the search screen, its Cancel button and the lobby that follows are the overlay\'s: declare `matchmaking.defaults` in the mode and the player can start a search from the standard menu without the game passing a key.\n\nRoom status is one of:\n\n- `lobby`: players are joining and choosing their setup.\n- `countdown`: the lobby has accepted `start()` and play begins at the announced server time.\n- `playing`: the game server is running the match.\n- `finished`: the match is over and the room is waiting for a rematch, with sockets open and `room.result` available.\n- `ended`: the match or connection has ended. `room.result` contains the result last reported by the room. A definitive connection closure uses `{ closed: 4003 }` when the player was kicked, `{ closed: 4004 }` when the room ended, `{ closed: 4005 }` when the published version closed, or `{ closed: 4006 }` when the same player opened the room in another tab.\n\nThe current lobby data is available directly:\n\n```js\nroom.players; // [{ id, name, guest, role, team, ready, connected }]\nroom.you; // this player\'s id\nroom.host; // the current host\'s id, or null\n\nroom.ready(true);\nroom.setRole(\'captain\');\nroom.setTeam(1);\n\nif (room.you === room.host) room.start();\n```\n\nIn a standard game the overlay calls these four for the player: read `room.players` to draw the field, not to build a roster panel. `ready`, role, team, and `start()` are lobby actions. Starting requires the host, every connected player to be ready, and the player, role, and team minimums from the manifest. Calling `start()` begins a three-second countdown. A role or team change clears that player\'s ready state. The built-in `spectator` role is still a player slot for setups such as a shared screen with phone controllers. Use `watch()` for someone who only observes and does not occupy a player slot.\n\n### Rematch in the same room\n\nThe server opts in per match with `room.end(result, { rematch: true })`. The room becomes `finished`, keeps its code, members, state and open connections, and exposes the result through `room.result` and `onStatus(\'finished\', result, at)`. Voice and spectator streams remain connected. `room.end(result)` or `{ rematch: false }` still ends the room permanently with status `ended` and close code 4004.\n\nWith `rematch: true` in a multiplayer room, each connected player calls `room.restart()` once to become ready for the rematch. In `finished`, `room.players[].ready` means rematch readiness. Once every connected non-spectator player is ready and the mode\'s `players.min` is met, the host calls `room.restart()` again to confirm. The first host call only registers readiness. A host with the built-in `spectator` role does not register readiness and can confirm once the players are ready. Repeated non-host calls do nothing. Calling outside `finished` fails with `rematch_unavailable`; an early host confirmation reports `players_not_ready` through `room.onError`.\n\nThe standard overlay handles these calls with Play again, the readiness count and names, and Start rematch for the host. Games without the overlay can call `restart()` directly. No member is removed for declining. With `rematch: true`, there is no automatic start after a departure: the current host must confirm.\n\nAt confirmation, the kit clears the result to `null` and all readiness flags, then calls optional `onRestart(room)` with status `lobby` when the mode has a lobby, or `playing` otherwise. **The kit does not reset `room.state`.** Reset match data in `onRestart`, keeping series scores or other data as needed. With a lobby, players choose their setup and get ready again before the normal countdown and `onStart`. Without a lobby, `onStart` follows `onRestart` immediately. Clients receive the new state and an `onStatus` transition with a null result. The room identity, seed and tick sequence are retained; `session.onChange` does not create a new session for a rematch. Listen to `room.onState` and `room.onStatus`.\n\n### Fast rematches and solo rooms\n\nThe existing `rematch: true` flow above is unchanged for multiplayer rooms. To shorten it, use:\n\n```js\nroom.end(result, { rematch: { keepSetup: true, autoStart: true } });\n```\n\nBoth flags default to `false`. `keepSetup` restores the previous readiness flags, keeps roles and teams, and skips the lobby: `onRestart` runs with status `countdown`, followed by the usual three-second countdown and `onStart`. The result clears before `onRestart`. Even a mode without a lobby gets this countdown. Readiness during `finished` is still consent for the next match, separate from the previous setup. Player, role and team minimums are rechecked during the countdown; if they fail, the room returns to the lobby so the setup can be repaired.\n\n`autoStart` starts the rematch when all connected non-spectator players have accepted and the mode\'s minimum is met, including after a departure leaves that condition satisfied. The overlay shows the readiness count and names without a host confirmation button or wait-for-host text. With `autoStart`, the host can also call `restart()` again to confirm early once at least `players.min` connected non-spectator players have accepted. Members who have not accepted remain in the room and join the next phase; they are not removed. With `autoStart` alone, the next phase is the usual lobby or immediate play; combine it with `keepSetup` to skip another ready/start cycle.\n\nA room whose resolved `players.max` is `1` is a solo room, including when it inherits `lobby: true`: it starts on entry and shows no lobby, invitation, code or wait-for-host prompt. In `finished`, one `room.restart()` immediately runs `onRestart`, then `onStart`, without consent or host confirmation or a countdown. The standard Play again button makes that one call. After a terminal end it creates a new solo room without opening or copying an invite. No additional manifest field is needed; `caisual init` without `--multiplayer` still creates a local game.\n\nThe waiting rules are:\n\n- All pending `room.schedule` handlers are cancelled when the match finishes, including handlers already due in the same batch. Scheduling during `finished` has no effect. Schedule new work in `onRestart` or `onStart`. Tick callbacks stop immediately; game input during the wait is discarded, and queued continuous input is cleared on the client.\n- `onEnd` runs once for the completed match, with status `finished` and its result. A subsequent timeout only closes the room and does not call `onEnd` again. Restart does not resubmit or clear pending board scores: each submission is drained once from the existing score queue. The game must not submit the previous match\'s scores again in `onRestart`.\n- Disconnecting clears that member\'s readiness and transfers the host to the oldest connected member. Disconnected players do not count toward readiness or the minimum. The usual 60-second reconnection grace applies; persistent members keep their seats after it. Rejoining requires a new readiness call. Connection and departure callbacks continue during the wait.\n- New members may join by invitation during the wait, up to the resolved `players.max`, and start unready. A lobby mode therefore reopens admission while `finished`; without a lobby, admission continues as during play. Disconnected members still occupy seats until removed. Roles and teams retain their existing capacity rules.\n- `watch()` spectators do not vote or occupy seats and keep following the same delayed stream across matches. Members with role `spectator` occupy a seat but do not vote or count toward the rematch minimum.\n- The room closes with 4004 exactly two minutes after entering `finished` if no restart is confirmed, retaining the last result. Readiness, joins and pings do not extend this deadline. It also applies to persistent rooms and survives sleep or restoration; it does not depend on active ticks.\n\n`finished` is a new status, not a terminal connection state. Older room clients receive the unfamiliar status and keep their sockets open instead of taking their `ended` cleanup path; they have no `restart()` control, and older overlays may retain their previous screen. Use an updated kit for games opting into rematches, and explicitly handle `finished` in game status listeners.\n\nThe server owns room state. Read it and react to updates, but do not assign to it or mutate nested values from the browser:\n\n```js\ndraw(room.state);\n\nconst stopState = room.onState((state, tick, serverTime) => {\n draw(state);\n});\n\nconst stopPlayers = room.onPlayers((players) => updateLobby(players));\nconst stopStatus = room.onStatus((status, result, at) => showStatus(status, result, at));\n\nstopState();\nstopPlayers();\nstopStatus();\n```\n\n`room.tick` identifies the latest state. The kit applies structural updates in order for both tick-based and event-only servers. Full state is sent on entry, reconnection or resync when an update does not match the current tick; normal updates, including every hundredth tick, remain diffs. `room.serverTime()` returns milliseconds aligned with the room clock and is kept current by a ping every five seconds.\n\n`room.tickRate` is the current effective server frequency, including reductions caused by the CPU budget; zero means event-only. Updates arrive with state diffs and snapshots, including an empty diff when only the frequency changes. `room.latency` is the smoothed round-trip time in milliseconds, or `null` before the first pong and after a dropped connection until the next pong. Each pong uses 20% of the new RTT and 80% of the previous estimate, with the first sample used directly. Read the property when drawing network status; there is no `onLatency` listener. Spectators expose the same properties; their tick rate follows the delayed state stream.\n\nSend JSON input to `onMessage` in the server definition, and receive JSON sent or broadcast by the server:\n\n```js\nroom.send({ type: \'fire\', target: 3 });\n\nconst stopMessages = room.onMessage((message) => {\n showEvent(message);\n});\n```\n\nThe kit numbers outgoing inputs in increasing order. It automatically reconnects temporary failures with delays of 1, 2, 4, then 8 seconds, for at most the room\'s 60-second grace period. Each attempt gets a fresh room token. A successful reconnect replaces local state with a full server state. `room.send` calls while reconnecting throw an error with `code: "offline"`.\n\nUse `room.input(value)` for continuous controls, including calls from every animation frame:\n\n```js\nroom.input({ type: \'move\', x: axisX, y: axisY });\nroom.onError(({ code }) => {\n if (code === \'rate_limited\') showInputWarning();\n});\n```\n\n`input` copies and keeps only the latest JSON value in one slot. It coalesces updates and sends at most 20 times per second, or at the effective `room.tickRate` when that is lower and positive. With `tickRate: 0`, it still sends at most 20/s. It also waits for budget used by `send`. Values with the same JSON serialization are not resent on the same connection. Combine independent controls into that one value; there is no channel option. On the server it is an ordinary message passed unchanged to `onMessage`, exactly like `send`, with no extra envelope.\n\nDuring reconnection, `input` accepts updates without throwing `offline`. After the new welcome it sends only the latest value, even if it was sent on the previous connection. It never replays intermediate values or old commands. Use `send` for individual actions such as firing or confirming a turn; `send` still throws `offline` during reconnection. Invalid JSON input can throw `invalid_request`. Input stops after leaving, disconnecting intentionally or ending the room.\n\nGame messages are limited to 20 per second per connection. Excess messages are dropped; `room.onError` receives `rate_limited` at most once per second across both budgets. Protocol service messages, including ping, lobby actions, state requests and voice signaling, have a separate 20/s budget with the same drop policy. More than 100 attempts in each of three consecutive one-second windows in either budget closes the connection with 4008 `rate_limited`; the kit does not reconnect automatically after this abuse closure. Abuse windows start with the first message on the connection; a normal or empty window resets the sequence. Oversized game frames close with 4009 `message_too_large`, reported through `room.onError`; malformed frames use 4009 `bad_message`. Neither closure is retried automatically.\n\nCall `room.leave()` for an intentional departure. The kit does not reconnect after leaving, being kicked, the room ending, the published version closing, or the same player opening the room in another tab.\n\n`room.disconnect()` is the other departure: it stops the transport, the retries and voice without sending a leave, so the server keeps the seat under its own persistence and grace rules. It is not reversible on the same object; returning means entering again from the code. The overlay uses it for Leave for now, together with the resume reference.\n\nA room also exposes `room.mode`, `room.countdownAt`, `room.connection`, `room.metadata`, and the `onMetadata` and `onConnection` listeners. `connection` is one of `connecting`, `connected`, `reconnecting`, `disconnected`, `ended`, `closed`, or `replaced`, where `replaced` means the same player opened the room in another tab. Unlike `session.onChange` and `overlay.onChange`, these listeners do not repeat the current value: read the getter first.\n\n`room.onError(listener)` reports technical protocol errors of the room as `{ code, message }`; it is not the place where a game reads its own result.\n\n`room.onScoreQueued(listener)` and `room.queuedScores` cover scores submitted for this player by `server.js`. Each entry is `{ board, player, score, day, submittedAt }`, only the owner\'s connection receives it, and the last 32 are kept. It is a technical notice that the server accepted the score, not a receipt that it is already on the board: read the board back with `c.board.top()` for that.\n\n`await room.requestRole(\'scout\')` asks the server for a role change during a match. It works only while the room is playing, only for a role declared in the manifest, and only when `server.js` defines `onRoleRequest(room, player, role)`; the server approves by calling `room.setRole`. Without that callback nothing changes, and the capability shows as `false` in `c.session.capabilities`. It is not a shortcut for changing roles from the browser.\n\nRoom creation, joining, and matchmaking reject with an `Error` carrying a stable `code`. Common codes are `invalid_request`, `no_server`, `no_match`, `cancelled`, `room_not_found`, `room_full`, `room_playing`, `room_ended`, `rate_limited`, `invalid_ticket`, `internal_error`, and `offline`. `no_server` means the published game has no multiplayer server. When `c.connected` is `false`, `create`, `join`, and `match` reject with `offline`.\n\n- `invalid_role`: a requested role id is malformed or is not declared in the manifest. Request a declared role id.\n- `role_change_unavailable`: the room is disconnected, is not playing, or `server.js` has no `onRoleRequest`. Wait for a connected playing state and provide that callback before offering the action.\n- `role_change_refused`: `onRoleRequest` returned without assigning the requested role. Leave the current role in place, or have the server approve with `room.setRole`.\n- `version_closed`: the room connection ended because its published version closed. Reopen the current game version and enter a current room.\n\nEvery listener call on a room returns a function that removes that listener: `onState`, `onPlayers`, `onStatus`, `onMessage`, `onMetadata`, `onConnection`, `onError`, and `onScoreQueued`.\n\n### Responsive action games\n\n1. Start with `npx @caisual/cli init --arcade my-arena`, a complete English/Italian canvas game with shared rules and tests.\n2. Accumulate `deltaSeconds` on the server and advance shared physics at a fixed 1/60 second step.\n3. Keep positions, collision or range checks, cooldowns and scores authoritative in `server.js`.\n4. Use `room.input()` for continuous controls and `room.send()` for discrete actions such as the starter\'s pulse.\n5. Number commands in the game and publish each player\'s last **applied** `ack` in `room.state`; transport sequence numbers are separate.\n6. Because `input()` coalesces values, send a bounded batch of unacknowledged commands; deduplicate on the server and consume at most one per simulation step.\n7. Predict your own entity with the shared step, replace it with each authoritative snapshot, then replay only commands after `ack`; ease corrections in the drawing only.\n8. Buffer remote samples by the `onState` timestamp and render behind `room.serverTime()`, accounting for RTT in `room.latency`, effective `room.tickRate`, and spectator `delayMs`.\n9. Clear pending controls on reconnection or a new round; keep the HUD inside safe areas and outside `reservedRects`, and end with `standings` plus `keepSetup`/`autoStart` rematches.\n10. Run the generated browser fixture and `/__caisual/players?n=2` with `dev --latency 120 --jitter 40 --loss 2`; see [Testing with a browser](/docs/local-development#testing-with-a-browser).\n\n### Spectators\n\nIn a standard game the overlay offers watching from the menu and the session arrives as `{ kind: \'watch\' }`. Use `c.room.watch(code)` to observe a room without joining it as a player:\n\n```js\nconst view = await c.room.watch(\'ABC234\');\n\ndraw(view.state);\nview.onState((state) => draw(state));\nview.onPlayers((players) => updateRoster(players));\nview.onStatus((status, result) => showStatus(status, result));\nview.onMessage((message) => showEvent(message));\n\nview.leave();\n```\n\nThe returned `Spectate` object exposes `state`, `tick`, `seed`, `status`, `players`, `host`, `code`, `result`, `delayMs`, the four listeners shown above, `serverTime()`, and `leave()`. It receives the room\'s public state, snapshots and updates, player list, status, and messages broadcast by `server.js`. The kit repairs a missed update automatically and reconnects temporary failures for the same 60-second grace period used by players.\n\nPublic room events are delayed by `delayMs`, which defaults to 3000 milliseconds. A game can set `"spectators": { "delayMs": N }` in `caisual.json`, where `N` is from 0 to 30000, or set `"spectators": false` to disable watching.\n\nA spectator has no `you`, `invite()`, `send()`, or voice API. Watching does not add anyone to `room.players`, does not affect roles, teams, player minimums, the host, or room lifetime, and is not visible to `server.js`. `watch()` can reject with `room_not_found`, `room_ended`, `spectators_disabled`, `spectators_full`, `rate_limited`, `offline`, or `invalid_request`.\n\n## Voice\n\nEvery room has a `room.voice` object. Voice is disabled by default and is enabled with the manifest\'s `voice` field.\n\nIn a standard game the overlay\'s voice panel carries Join, Leave, Mute, and the list of who is in the call, with the click the browser requires. A standard game does not draw its own voice buttons. The `room.voice` API below remains for games without the standard overlay and for server-side gain and proximity rules.\n\nA game with its own controls must offer an explicit one, because `join()` must be called from a click or another user gesture so the browser can start audio and, when publishing, request microphone permission.\n\n```js\nconst micButton = document.querySelector(\'#mic\');\nconst voiceList = document.querySelector(\'#voice-list\');\n\nfunction renderVoice(peers = room.voice.peers) {\n voiceList.replaceChildren(...peers.map((peer) => {\n const item = document.createElement(\'li\');\n const player = room.players.find((entry) => entry.id === peer.id);\n item.textContent = `${player?.name ?? peer.id}: ${\n peer.speaking ? \'speaking\' : peer.muted ? \'muted\' : \'quiet\'\n }`;\n return item;\n }));\n micButton.textContent = room.voice.state === \'off\'\n ? \'Join voice\'\n : !room.voice.mic ? \'Listening\' : room.voice.muted ? \'Unmute\' : \'Mute\';\n}\n\nmicButton.addEventListener(\'click\', async () => {\n if (room.voice.state === \'off\') await room.voice.join();\n else if (room.voice.mic) room.voice.mute(!room.voice.muted);\n renderVoice();\n});\n\nroom.voice.onPeers(renderVoice);\nroom.voice.onState(() => renderVoice());\nrenderVoice();\n```\n\n`room.voice.mode` is `none`, `room`, `team`, or `proximity`. In `room` mode, every participant in voice can hear every other participant. In `team` mode, players hear only their team. In `proximity` mode, the room server controls the gain between participants. Call `room.voice.join({ mic: false })` to listen without opening or publishing a microphone. Spectators join in listening mode when they call `join()` without options. A spectator that calls `join({ mic: true })` receives the `spectator` error.\n\nThe room server authorizes every voice track by team and gain. Listening that is no longer allowed is refused or closed.\n\n`room.voice.state` is `off`, `joining`, `on`, or `reconnecting`. `room.voice.mic` is `true` while the local player is publishing. `room.voice.muted` and `room.voice.speaking` describe the local microphone. `room.voice.peers` contains the other voice participants as `{ id, mic, muted, speaking, volume, gain }`. A listening participant has `mic: false`, `muted: true`, and `speaking: false`. `volume` is the local setting and `gain` is the value from the room server. Use `room.voice.setVolume(playerId, volume)` with a value from 0 to 1 to change only local playback.\n\n`room.voice.onPeers(listener)` runs when participants, microphone state, mute state, speaking state, volume, or gain changes. `room.voice.onState(listener)` reports connection state changes. Both return a function that removes the listener.\n\nCall `room.voice.leave()` to stop publishing or listening without leaving the room. `room.voice.mute()` requires an active published microphone and otherwise throws `not_publishing`. `room.leave()` and the end of the room stop voice automatically.\n\n`join()` rejects with an `Error` carrying one of these stable codes: `voice_disabled`, `permission_denied`, `unsupported`, `spectator`, `offline`, or `voice_error`. Voice can reconnect after a temporary room or media connection failure. The state becomes `reconnecting` while the kit retries.\n\n## Server\n\nPut `server.js` next to `caisual.json` and publish it with the game. See [publish.md](./publish.md#multiplayer-server) for the file rules, validation, and publishing flow.\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\n\nexport default defineGame({\n tickRate: 20, // required, an integer from 0 to 60; 0 runs only in response to events\n onCreate(room) {},\n onStart(room) {},\n onJoin(room, player) {},\n onConnection(room, player, connected) {},\n onLeave(room, player, reason) {}, // "left", "timeout", or "kicked"\n onRoleRequest(room, player, role) {}, // approve with room.setRole\n onMessage(room, player, message) {},\n onTick(room, deltaSeconds) {},\n onEnd(room) {},\n onRestart(room) {},\n});\n```\n\n`tickRate` is the only required field: `defineGame` throws a `TypeError` when it is missing or is not an integer from 0 to 60. All callbacks are optional. A player is `{ id, name, guest, role, team, connected }`.\n\n### Callback order\n\n- `onCreate` runs once when the room is first created, before any player joins.\n- `onJoin` runs when a player first enters the room, not when that same member reconnects. Without a lobby, the first player\'s `onJoin` is followed by `onStart`.\n- `onStart` runs when the room changes to `playing`. Without a lobby this is the first player entry. With a lobby it is after the host starts, the three-second countdown finishes, and the room still meets its minimums.\n- `onTick` runs for each active game tick when `tickRate` is greater than zero. `onMessage` runs for accepted client game messages, and `onRoleRequest` runs for an in-match role request when that callback exists. These events are processed serially, so their relative order is the order in which the room processes them.\n- `onConnection(room, player, connected)` runs with `false` when an existing member loses their connection and with `true` when that disconnected member returns. Both `player.connected` and `room.players` are already updated. It does not run for the first entry, a socket replacement while the member is still connected, or a permanent removal. Restoring a room reconciles actual connections: members saved as connected whose sockets are gone receive `false`, and their later return receives `true`. Surviving sockets receive no extra callback.\n- `onLeave` runs only when a player is removed with reason `left`, `timeout`, or `kicked`. A dropped connection calls `onConnection` during the grace period and does not call `onLeave`.\n- `onEnd` runs after the callback that requested `room.end`, with the final result and status `ended` or `finished`. Automatic room endings also use it, except closure of an already `finished` match, which must not run it twice.\n- `onRestart` runs when a rematch starts, after clearing the result and selecting the next status. Readiness is reset by default or restored with `keepSetup`. The fast-rematch and solo rules above override the default host-confirmed flow. With a lobby it prepares `lobby`; without one it prepares `playing` and is immediately followed by `onStart`. It is optional; `room.state` is preserved unless the game changes it.\n\nThere is no `onResume` callback. A sleeping room restores its saved state without calling one. When a scheduled time arrives, the room invokes the method named by `room.schedule`.\n\nFor a turn-based game, store the active player\'s id and pause that turn when the player disconnects. This also works with `tickRate: 0`, because callback state changes are published immediately:\n\n```js\nonConnection(room, player, connected) {\n if (room.state.turnPlayerId === player.id) {\n room.state.turnPaused = !connected;\n }\n},\nonMessage(room, player, message) {\n if (room.state.turnPaused || player.id !== room.state.turnPlayerId) return;\n applyTurn(room, player, message);\n},\n```\n\nIf turns have deadlines, save the remaining time when pausing and calculate a new deadline on return. Scheduled handlers must check whether the turn is still paused or current. Decide separately in `onLeave` how to handle a permanent departure. Secret redelivery remains a client responsibility through `room.onConnection`; see [Hidden information](#hidden-information).\n\nThe room object provides:\n\n```js\nroom.id;\nroom.seed;\nroom.mode;\nroom.status;\nroom.tick;\nroom.tickRate;\nroom.result;\nroom.state;\nroom.players;\nroom.host;\n\nroom.broadcast(message);\nroom.send(playerOrId, message);\nroom.kick(playerOrId);\nroom.setRole(playerOrId, role);\nroom.setTeam(playerOrId, team);\nroom.end(result);\nroom.end(result, { rematch: true });\n\nawait room.save(\'round\', value);\nawait room.load(\'round\');\nawait room.shared.get(\'ship_abc\');\nawait room.shared.set(\'ship_abc\', value);\nawait room.shared.delete(\'ship_abc\');\nawait room.shared.list(\'ship_\');\nawait room.shared.increment(\'visits\', 1);\nroom.schedule(milliseconds, \'methodName\', payload);\nroom.board.submit(playerOrId, \'main\', score, { daily: true });\nroom.board.submit(playerOrId, \'run\', score, { day: room.daily.day });\n\nroom.daily.day;\nroom.daily.seed;\nroom.daily.expiresAt;\nroom.time.now();\n\nroom.voice.mode;\nroom.voice.setGain(listener, speaker, 0.25);\nroom.voice.setProximity(playerA, playerB, 0.5);\n```\n\nSet `room.state` in `onCreate`, then mutate it only in server callbacks. It must remain plain JSON and may be at most 256 KB when serialized. `broadcast` sends a JSON message to everyone; `send` targets one player. `end` records a JSON result and closes the room unless rematch is enabled with `true` or an options object. See [Rematch in the same room](#rematch-in-the-same-room) for readiness, callback order, timer cancellation and the waiting deadline. Room saves use keys with the same format as player save keys and values up to 128 KB. `schedule` names a method on the definition so it can run even after a quiet room resumes. Scores submitted through `room.board` are verified. The room fixes their UTC `day` and millisecond `submittedAt` when `submit` is called, so delayed writes and retries do not move them to another day. Older queued scores without these fields retain the write-time day. Boards default to submission-day scoring. Set `day: "start"` and pass `{ day: room.daily.day }` for the starting day with a 10-minute grace period after midnight. See [Daily challenge](#daily-challenge) for expiry, errors and local day simulation.\n\n### Recognized match results\n\n`room.end(result)` still accepts any JSON. The optional `MatchResult` type is exported by the contracts and by `@caisual/kit` and `@caisual/kit/server`:\n\n```ts\ntype MatchResult = {\n standings: Array<{ playerId: string; score?: number; rank?: number }>;\n winners?: string[];\n draw?: boolean;\n unit?: \'points\' | \'time\' | \'distance\' | string;\n data?: JsonValue;\n} & Record<string, JsonValue>;\n```\n\nOrder `standings` from first place onward. Scores must be finite numbers; ranks are positive safe integers. The overlay uses the supplied order, displays player names and optional scores, and uses `rank` when present. `draw: true` takes precedence; otherwise `winners` identifies the winners. Without `winners`, the first row wins, together with rows sharing its explicit rank. Equal scores alone do not imply a draw. An explicit empty `winners` list means nobody won.\n\nThe end bar shows You won, You lost or Draw only for a participating player; spectators and players missing from the standings see Match finished. Outcome labels and the built-in units are translated in six languages. Custom units are shown as plain text; `time` and `distance` do not convert values or imply a measurement scale.\n\nReading is tolerant: unknown fields are ignored by the overlay, unknown or duplicate players are discarded, invalid scores and ranks are omitted, and winners must occur in the retained standings. With no usable standings the generic end bar remains. **`room.result` keeps the original JSON without normalization**, including `data` and any other game fields. Results are public to room members and spectators, so keep secrets elsewhere.\n\n```js\nroom.end({\n standings: [{ playerId: winner.id, score: 12, rank: 1 }, { playerId: other.id, score: 9, rank: 2 }],\n winners: [winner.id], unit: \'points\', data: { rounds: 3 },\n}, { rematch: true });\n```\n\n### Hidden information\n\nEverything in `room.state` reaches every player and every spectator. Never store cards in hand, secret roles, fog of war, or any other private value there.\n\nKeep secrets in room saves through `room.save` and `room.load`, which are server-only, or in module-level variables in `server.js`, keyed by room when needed. Deliver a secret to one player with `room.send(player, { type: \'hand\', cards })`. `onJoin` runs only on the player\'s first entry, not on reconnection, so the client requests its secrets for the current connection and every later reconnection:\n\n```js\nif (room.connection === \'connected\') room.send({ type: \'hand?\' });\nroom.onConnection((state) => {\n if (state === \'connected\') room.send({ type: \'hand?\' });\n});\n```\n\nThe server answers from `onMessage` with `room.send(player, { type: \'hand\', cards })`. The three-second delay of `c.room.watch` does not protect secrets. It only stops a player from watching an opponent\'s live screen in another tab.\n\nThe browser can change its role or team only while the room is in `lobby`. During a match, the server decides when a player changes role or team with `room.setRole` and `room.setTeam`. Both methods accept a player object or id and immediately update `room.players` for every client.\n\n```js\nonMessage(room, player, message) {\n if (message?.swap === \'captain\') {\n room.setRole(player, \'captain\');\n }\n},\n```\n\n`room.daily.seed` is fixed at room creation and shared by rooms created for that game on the same UTC day. `room.seed` is fixed for one room and is identical on the server and clients, so rooms created on the same day can generate different maps.\n\n### Shared game store\n\n`room.shared` is a server-only JSON key/value store shared by every room of the same game. It is useful when one room must leave data for another room, while `room.save` remains private to one room.\n\nThe following server leaves a ship when a room ends, then loads every previously left ship when another room is created. The room id suffix is used because shared-store keys follow the save-key format.\n\n```js\nexport default defineGame({\n tickRate: 0,\n\n async onCreate(room) {\n const keys = await room.shared.list(\'ship_\');\n room.state = {\n ships: await Promise.all(keys.map((key) => room.shared.get(key))),\n };\n },\n\n async onEnd(room) {\n const roomSuffix = room.id.split(\'.\')[1];\n await room.shared.set(\'ship_\' + roomSuffix, {\n position: room.state.position,\n cargo: room.state.cargo,\n });\n },\n});\n```\n\nThe five methods are asynchronous:\n\n```js\nconst value = await room.shared.get(key); // JSON value, or null\nawait room.shared.set(key, value); // last writer wins\nawait room.shared.delete(key);\nconst keys = await room.shared.list(prefix); // sorted, up to 1024\nconst total = await room.shared.increment(key, 1); // atomic, defaults to 1\n```\n\nKeys contain 1 to 32 lowercase letters, numbers, underscores, or hyphens. Values may be up to 64 KB when serialized, and each game may keep up to 1024 keys. Each room may perform up to 120 shared-store operations per minute. `increment` treats a missing key as zero and rejects unless the existing value, amount, and result are safe integers.\n\nUse the store in `onCreate`, `onStart`, `onEnd`, `onMessage`, or a `schedule` handler. Do not call it on every tick: each call waits for a remote operation, and the CPU budget uses elapsed wall-clock time. Browser clients cannot access this store. Send only the data they need with `room.broadcast` or `room.send`.\n\nFailures reject with an `Error` carrying `store_invalid_key`, `store_too_large`, `store_full`, `store_not_integer`, `store_unavailable`, or `store_rate_limited` in `code`.\n\n`room.voice.setGain(listener, speaker, gain)` controls how much one listener hears one speaker. It is directional, limited to the range from 0 to 1, and rounded to two decimal places. For example, the following setup lets the captain hear everyone while each crew member hears only the captain:\n\n```js\nconst captain = room.players.find((player) => player.role === \'captain\');\nconst crew = room.players.filter((player) => player.id !== captain.id);\n\nfor (const speaker of room.players) {\n room.voice.setGain(captain, speaker, 1);\n}\nfor (const listener of crew) {\n for (const speaker of room.players) {\n room.voice.setGain(listener, speaker, speaker.id === captain.id ? 1 : 0);\n }\n}\n```\n\n`room.voice.setProximity(a, b, gain)` is the symmetric shortcut for setting both directions. Both methods work in `room`, `team`, and `proximity` modes, and do nothing in `none`. In `team` mode, gains remain inside the team and cannot make a player hear another team.\n\nFor position-based audio, update the symmetric gain between players from server-owned positions:\n\n```js\nexport default defineGame({\n tickRate: 20,\n onTick(room) {\n for (const a of room.players) {\n for (const b of room.players) {\n if (a.id >= b.id) continue;\n const pa = room.state.positions[a.id];\n const pb = room.state.positions[b.id];\n const distance = Math.hypot(pa.x - pb.x, pa.y - pb.y);\n room.voice.setProximity(a, b, Math.max(0, 1 - distance / 20));\n }\n }\n },\n});\n```\n\n### Sleeping and cost\n\nPrefer `tickRate: 0` for turn based and party games. A room with a tick loop sleeps automatically after 30 seconds without player input or state changes and wakes on the next game message or player joining. Automatic ping and resync messages do not count as player input. A match with no player input for 10 minutes ends with `{ error: \'idle\' }`. Timers set with `schedule` and the countdown keep working while the room sleeps.\n\n### CPU budget\n\nEvery `onTick` and `onMessage` call is measured. Twenty consecutive calls above 100 ms end the room with `{ error: \'cpu_budget\' }`. If the average over 50 ticks is above 20 ms, the effective `tickRate` is halved, down to a minimum of 5, and clients receive an `error` message with code `tick_rate_reduced`. The optional `tickRate` field in `state` and `snapshot` protocol messages updates client `room.tickRate`; older clients ignore the added field. A frequency change sends a state diff even when its patch is empty.\n\n`room.tickRate` starts at the definition\'s `tickRate` and always reports the current effective frequency. `deltaSeconds` follows that frequency, so a fixed-step simulation must accumulate `deltaSeconds` instead of counting ticks. Measurement uses elapsed wall-clock time, so a slow `await` inside a callback also counts. `room.result` is `null` during a match, contains its result in `finished` or `ended` and inside `onEnd`, and returns to `null` before `onRestart`.\n\n### Persistent rooms\n\nSet `"persistent": true` in `caisual.json` for a room that must survive long breaks. It does not use the normal inactivity ending rule and does not end when every player disconnects. Players remain members until they call `room.leave()` or the server removes them with `room.kick()`. They can use the same room code to return while the game is already playing. The code remains valid while the room lives, and absent members remain in `room.players` with `connected: false`.\n\nA persistent room ends when the server calls `room.end(result)` without rematch, when its two-minute rematch wait expires, after 30 days without player input, entry, or a state change with `{ error: \'expired\' }`, or after five minutes without any members. The standard overlay stores the resume reference itself. Only games without it need to store the client `room.code` with `c.save.set()` and offer Resume. A persistent room incurs cost only while it is awake.\n\n## Limits\n\n- 120 requests per minute per player. Beyond that the kit rejects with `rate_limited`; wait and retry.\n- Saves: 32 keys per player per game, 256 KB per value.\n- Scores: safe integers from 0 upward.\n- Room state: 256 KB of plain JSON.\n- Game messages: 16 KB each and 20/s per connection; excess messages are dropped with at most one `rate_limited` error per second. Service messages have a separate 20/s budget. More than 100 attempts/s in either budget for three consecutive one-second windows closes with 4008. Oversized frames close with 4009 `message_too_large`.\n- Spectators: 100 per room, with a configured delay from 0 to 30 seconds.\n- Voice supports audio only and one voice channel per room.\n- Voice control messages: 64 KB each and 30 operations per 10 seconds per connection. Voice signaling also uses the separate service-message budget; audio traffic does not consume either message budget.\n- Room save values: 128 KB each.\n- Shared game store: 64 KB per JSON value, 1024 keys per game, and 120 operations per minute per room.\n\n## Development\n\nRun `npx @caisual/cli dev` from the game folder, then open the printed portal URL. The preview supplies the normal handshake, so `c.connected` is `true` and the game receives a local guest identity. It also mounts the same standard overlay as the portal. `?lang=` chooses the game preference, resolved against the manifest; `c.player.uiLanguage` follows the overlay fallback. For example, `?lang=ja` gives `c.player.language === "ja"` when declared, with the overlay in English. Friends and parties are marked unavailable locally; everything else, including saves, leaderboards, daily data, invitations, and rooms, works on local data. Opening the portal URL in more browser tabs creates more local players, which makes multiplayer testing possible without publishing.\n\nUse `npx @caisual/cli dev --day 2026-09-04` to pin the UTC day used by client and room daily seeds and local daily leaderboards. The flag accepts only a real date in `YYYY-MM-DD` format; without it, dev uses today\'s UTC date. Real clocks and room timers keep running normally. Scores remain in `.caisual-dev/scores.json` under their assigned day: restarting with another `--day` selects that day\'s board, and returning to a previous day restores its scores. Saves, identities and rooms are shared across these dates; queued room scores keep their assigned day when flushed after a restart. Existing rooms retain their creation context; new rooms follow the selected day. `expiresAt` follows the real clock even with `--day`. Use `c.board.top(\'main\', { day: \'2026-09-04\', guests: true })` to inspect a specific local day. A changed flag takes effect after restarting dev and reloading the game.\n\nOpen the printed `/__caisual/players?n=4` URL for 1 to 8 independent guest frames, each including the game and standard overlay. Use the overlay to create a room and join its code in the other frames. Each frame has **Drop** (1 to 60 seconds, default 3) and **Spectate**, which opens a new guest watching that frame\'s room. Phone frames are 390 x 844; the desktop preset is 960 x 640. Add `&lang=it` to test Italian.\n\n`dev --latency 120 --jitter 40 --loss 2` delays room WebSocket messages in both directions for players and spectators. Latency and jitter are integer milliseconds from 0 to 60000; loss is a percentage from 0 to 100 and may be fractional. Jitter and loss require `--latency`, which may be zero. Jitter varies delay uniformly within plus/minus the supplied value, clamped at zero, and preserves message order. Loss discards whole application messages, including protocol messages, to exercise recovery; it is not a model of TCP packet retransmission. HTTP, matchmaking and audio are unaffected. Drop closes the guest\'s room sockets and prevents successful reconnects for the selected duration; the kit\'s retry schedule can make the return later. The spectator stream also retains its configured game delay.\n\nClient files are read on each request: reload the portal or frame after editing, or rebuild into `client/` first if using a bundler. There is no automatic browser reload. Restart dev after changing `caisual.json`, `server.js`, or any server import (including shared client physics). Rooms restore from `.caisual-dev/`; changing the shape of their state may require creating a fresh room. A busy port error suggests a command with a currently available port.\n\nEvery `init` variant generates Node test and optional Playwright browser scripts. Playwright is a development dependency of the generated game, never of the CLI. See [Local dev](/docs/local-development#testing-with-a-browser) for installation and the full browser workflow.\n\nWhen building the client with a bundler, remember that `client/` is served as-is. Configure Vite, esbuild, or another bundler to write into that folder, for example `vite build --outDir client`, and use relative paths such as `base: \'./\'`.\n\nKeep loading the kit from the `<script type="module">` shown at the beginning of this guide, using `/__caisual/kit/v1.js`, when publishing on Caisual. That URL exists only in `caisual dev` and in the published game.\n\nIf the game has `server.js`, room state is handled locally and stored under `.caisual-dev/` in the game folder. If it has no `server.js`, room creation rejects with `no_server` and the single-player APIs still work.\n\nTo run from any other static server, install `@caisual/kit` from npm and import it with a bundler as `import { caisual } from \'@caisual/kit\'`. In that build standalone mode applies: `c.connected` is `false`, saves use local storage, `submit` returns `accepted: false`, leaderboards are empty, the daily seed is local, and room creation and joining reject with `offline`. The rest of the game logic does not need a different code path.\n\nAfter publishing with `npx @caisual/cli publish`, open the game from its caisual.com page: `c.connected` becomes `true` and every call goes to the portal.\n\n## Manifest\n\nDeclare `"overlay": { "version": 1 }` to get the standard overlay, with an optional `accent` colour. A standard game must declare at least one mode, and every mode needs `execution`, either `local` for a single-player run of exactly one player or `room` for a room backed by `server.js`. `label` names the mode in the standard menu and `instructions` adds one line under it. `roles[].label` and `boards[<id>].label` name roles and boards in the same UI, and `boards[<id>].periods` lists `daily`, `all-time` or both. A mode with matchmaking adds `matchmaking.defaults`, one value for every field of its `key`, so the overlay can start a search on its own.\n\n```json\n{\n "overlay": { "version": 1, "accent": "#397e83" },\n "players": { "min": 2, "max": 4 },\n "lobby": true,\n "boards": { "solo": { "source": "server", "label": "Best run", "periods": ["daily", "all-time"] } },\n "modes": [\n { "id": "practice", "execution": "local", "label": "Practice",\n "instructions": "One run against the clock.",\n "players": { "min": 1, "max": 1 }, "lobby": false },\n { "id": "duel", "execution": "room", "label": "Online",\n "matchmaking": { "key": ["pool"], "defaults": { "pool": "v1" }, "timeoutMs": 12000 } }\n ]\n}\n```\n\nA game without `overlay` keeps its historical flow and draws its own menus. Nothing else changes for it.\n\nNo manifest field is required for identity, saves, leaderboards, or the daily challenge. Use `boards` to make selected leaderboards server-only. A mode may override only `players: { min, max }` and `lobby`; omitted fields inherit the root configuration, and `mode: null` uses the root values. Matchmaking thresholds and room admission use this same resolution. For rooms, set `players` to the supported range and use `lobby`, `persistent`, `spectators`, `roles`, `teams`, and `modes` to describe the setup and lifetime. Set `voice` to `room`, `team`, or `proximity` to enable the corresponding voice mode, or omit it for `none`. A single-player game can keep `players` at `{ "min": 1, "max": 1 }`, `lobby` at `false`, and omit `server.js`. See [publish.md](./publish.md#caisualjson) for every field and the publishing steps.\n\n\n## Required game images\n\nEnglish (`en`) is required in `languages`; three distinct files inside `client/` with no text inside are also required: cover 1536x1024 (3:2), card 1024x1024 and icon 1024x1024, each PNG, JPEG or WebP and at most 2 MB. See [Manifest](https://caisual.com/docs/manifest#required-game-images).\n';
1240
+ var kit_default = '# Caisual game kit\n\nThe kit gives a published game a stable player identity, cloud saves, leaderboards, a daily challenge seed, and multiplayer rooms with server-owned state.\n\nThe kit is a single JavaScript module with no dependencies. It never touches the page: the game keeps its own rendering, input, and UI.\n\n## Load the kit\n\nEvery published game can import the kit from its own origin, without a bundler and without an npm install:\n\n```html\n<script type="module">\n import { caisual } from \'/__caisual/kit/v1.js\';\n\n const c = await caisual.connect();\n console.log(c.player.name);\n</script>\n```\n\nGames built with a bundler can install the same module from npm:\n\n```sh\nnpm install @caisual/kit\n```\n\n```js\nimport { caisual } from \'@caisual/kit\';\n```\n\nBoth forms expose the same API. The module also sets `globalThis.caisual` for classic scripts that load it first.\n\nThe path `/__caisual/` is reserved on every game origin. Do not put game files under it.\n\n## Connect\n\n```js\nconst c = await caisual.connect();\n```\n\n`connect()` completes when the game is running inside caisual.com and has received its player identity, or after a short timeout when it is not. Calling it again returns the same promise.\n\n- `c.connected` is `true` inside caisual.com and `false` when the game runs on its own, for example from a local folder during development or when its files are copied elsewhere.\n- `c.player` is `{ id, name, guest, language, uiLanguage }`. `id` is stable for the player across sessions and across every version of the game. `name` is the account username, or a stable `Guest-XXXX` name derived from the player id. The four-character suffix uses `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, excluding I, O, 0 and 1; it helps distinguish guests but is not a unique identifier. `guest` is `true` for players without an account. `language` selects game strings; `uiLanguage` is the overlay locale. On first sign-in to an account without a player identity, the browser guest is adopted with its existing id, saves and scores. An account that already has a player identity uses that identity instead; guest data is not merged.\n- When not connected, `c.player` has `id: "local"`, `name: "Guest"`, `guest: true`, plus both language fields. If the host answered, its language information is kept; without a handshake, `language` is the normalized `navigator.language` (or `en`) and `uiLanguage` is its overlay fallback.\n\nDo not store the ticket or reimplement the handshake. The kit handles identity, renewal, and retries.\n\n## Game language and strings\n\n`c.player.language` is the game\'s language; `c.player.uiLanguage` is the overlay\'s locale. Use the former for game strings and formatting, or the latter when intentionally aligning text with the overlay.\n\nThe kit resolves the player\'s explicit portal language choice, otherwise `navigator.languages` in order, against `manifest.languages`. For each preference it tries the exact tag and then its parent tags; if none of the preferences match, it uses the game\'s first declared language. The result is always a normalized declared tag. For example, `ja-JP` with `languages: ["en", "ja"]` selects `ja`, while `uiLanguage` is `ja-JP`. The overlay supports English, Italian, Spanish, French, German, Portuguese and Japanese; it keeps regional tags in those families, such as `pt-BR`, and falls back to `en` for other languages. Games may declare languages outside these seven.\n\nA localized portal URL counts as a language choice. The language selector remembers explicit choices, including English; without either, the browser\'s ordered preferences apply. The handshake keeps its legacy `language` field for existing kits, and also sends `uiLanguage`, `languagePreferences` and `gameLanguages`. The kit resolves the game language, including when player services fail after a successful handshake.\n\nWithout a handshake, no manifest is available: `language` is the raw preference from `navigator.language`, normalized as a BCP 47 tag, or `en` if invalid or unavailable. It is not restricted to declared game languages. `uiLanguage` uses the overlay fallback. In `caisual dev`, `?lang=ja` selects `ja` when the manifest declares it, with a Japanese overlay. Without `?lang=`, dev uses `navigator.languages` in order for the game.\n\nPut all game UI strings in flat JSON dictionaries named `client/i18n/<lang>.json`. Use canonical BCP 47 filenames, for example `en.json`, `it.json`, `pt.json`, `pt-BR.json`. Every value is a string; keys are identical across dictionaries. Text can contain named placeholders such as `{n}`.\n\nDeclare supported languages in `caisual.json`, always including `en`. English is required alongside the game\'s own languages; the first entry is the default and may be another language. This complete manifest supports a local game:\n\n```json\n{\n "manifest": 1,\n "cover": "cover.png",\n "card": "card.png",\n "icon": "icon.png",\n "id": "three-lights",\n "name": "Three Lights",\n "platform": "both",\n "languages": ["en", "it"],\n "overlay": { "version": 1 },\n "modes": [{\n "id": "solo",\n "execution": "local",\n "label": { "en": "Solo", "it": "Da solo" },\n "instructions": { "en": "Light up three lights.", "it": "Accendi tre luci." }\n }]\n}\n```\n\n`client/i18n/en.json`:\n\n```json\n{ "score": "Lights: {n} / 3", "light": "Light up", "done": "Complete" }\n```\n\n`client/i18n/it.json`:\n\n```json\n{ "score": "Luci: {n} / 3", "light": "Accendi", "done": "Tutte accese!" }\n```\n\nLoad once during setup, before `c.session.ready()`. The returned function is synchronous and can be used in every draw call:\n\n```html\n<!doctype html>\n<html>\n<head><meta charset="utf-8"><title>Three Lights</title></head>\n<body style="margin:0;min-height:100dvh;display:grid;place-content:center">\n <p id="score"></p>\n <button id="light" disabled></button>\n <script type="module">\n import { caisual } from \'/__caisual/kit/v1.js\';\n const c = await caisual.connect();\n const t = await c.text();\n document.documentElement.lang = c.player.language;\n const score = document.querySelector(\'#score\');\n const light = document.querySelector(\'#light\');\n let n = 0, blocked = false, active = !c.session.capabilities.overlay;\n function draw() {\n score.textContent = n === 3 ? t(\'done\') : t(\'score\', { n });\n light.textContent = t(\'light\');\n light.disabled = blocked || !active || n === 3;\n }\n c.overlay.onChange((view) => { blocked = view.inputBlocked; draw(); });\n c.session.onChange((session) => {\n if (session.kind === \'local\' && session.status === \'playing\') n = 0;\n active = !c.session.capabilities.overlay || (session.kind === \'local\' && session.status === \'playing\');\n draw();\n });\n light.onclick = () => {\n n += 1;\n if (n === 3 && c.session.current.kind === \'local\') c.session.finish();\n draw();\n };\n draw();\n c.session.ready();\n </script>\n</body>\n</html>\n```\n\n`c.text(): Promise<Text>` makes one request to the game\'s own origin, tied to the version currently open. Caisual and `caisual dev` read the matching files and merge them per key: `pt-BR` then `pt` then the manifest\'s default language. Longer tags fall back through their parent tags, such as `zh-Hant-TW`, `zh-Hant`, `zh`. The default file is tried once. If no file contains a key, `t` returns that key. Empty strings are valid translations.\n\nConcurrent and later `c.text()` calls share the same promise and translator for that connection. There are no dependencies, eager downloads, or per-call network requests. Missing files, invalid dictionaries and network failures do not prevent startup. If the Caisual text service is unavailable, for example on a plain static host outside Caisual, the translator returns keys; it does not probe other URLs. Use `caisual dev` to preview the complete convention.\n\n`t(\'score\', { n: 3 })` replaces named placeholders with strings or numbers. An omitted placeholder stays unchanged, such as `{n}`. The result is plain text, with no HTML processing, plural rules or automatic translation. Set `textContent` or draw it on the canvas; do not insert it as HTML. A new document gets the host\'s current language and a fresh translator.\n\n`description`, mode `label` and `instructions`, role `label`, and leaderboard `label` accept a string or a language-to-text object, and resolve from `uiLanguage` in the overlay using the same fallback chain. Descriptions also resolve from the page language in the catalogue, game page, profiles, invitations and metadata. Each description translation must contain 1-500 characters on one line and use a key declared in `languages`. `name` and `tags` keep their existing forms. Existing single-string labels stay unchanged. See [manifest languages and validation](./publish.md#game-translations) for CLI checks and migration from `language`.\n\n## Sessions and the standard overlay\n\nA game that declares `overlay` in `caisual.json` is a standard game: it runs full screen and Caisual draws everything around it. The opening menu groups modes into Single player (`players.max === 1`, including rooms for one) and Multiplayer, omitting tabs for one group and the mode selector for one mode per group. The mode choice, the lobby with roles, teams and ready, invitations, friends and parties, matchmaking, spectators, leaderboards, voice, the end of a match and Play again belong to the platform. The game keeps the field, its own HUD and its own settings.\n\n```json\n{ "overlay": { "version": 1, "accent": "#397e83" } }\n```\n\nTwo objects appear on the connection. `c.session` says which session the game is in, `c.overlay` says when the platform is on top of it.\n\n```js\nconst c = await caisual.connect();\n\nconst stopSession = c.session.onChange((session) => {\n detachGameListeners();\n if (session.kind === \'idle\') return showAttractScene();\n if (session.kind === \'local\') return showLocalRun(session.mode, session.status);\n attachGameListeners(session.room, session.kind === \'watch\');\n draw(session.room.state);\n});\n\nconst stopOverlay = c.overlay.onChange(({ inputBlocked, reservedRects }) => {\n clearHeldKeys();\n setInputEnabled(!inputBlocked);\n placeHudOutside(reservedRects);\n});\n\nawait loadAssetsAndChosenView();\nc.session.ready();\n```\n\n### The session\n\n`c.session.current` reads the session at once. `onChange` repeats the current value immediately to every new listener, returns a function that removes it, and then reports attaches, detaches and the end of a local run. It never fires for a move or a roster change: those stay on the room listeners.\n\n- `{ kind: \'idle\' }`: no session. Show an attract scene, not a menu.\n- `{ kind: \'local\', id, mode, status }`: a run of a mode declared with `"execution": "local"`. `status` is `playing` or `ended`.\n- `{ kind: \'room\', id, room }`: `room` is the `Room` documented below, already attached.\n- `{ kind: \'watch\', id, room }`: `room` is a `Spectate`. Draw it read only.\n\n`id` changes on every attach, so a second local run is distinguishable from the first.\n\n`c.session.ready()` says the game has loaded its assets and installed its listeners. Call it once, at the end of setup: until then the overlay waits instead of starting a session under a game that is still downloading. It is idempotent.\n\n`c.session.finish()` ends a local run and shows the compact end bar with a Play again action. It applies only to `kind: \'local\'`: on a room it fails with `not_local` and on idle it does nothing. The result of an online match comes from the server, never from `finish()`.\n\n`c.session.capabilities` is `{ local, rooms, overlay, requestRole }`. Outside caisual.com `overlay` and `rooms` are `false` while `local` stays `true`, which is the signal to run the game\'s own offline fallback. No fake room is created.\n\nTwo front ends of the same game share one session. Switching view does not call `ready()` again and does not detach the room: the new renderer reads `c.session.current` and draws.\n\n### The overlay on top\n\n`c.overlay.onChange` repeats the current geometry immediately, then on every change:\n\n- `inputBlocked` is `true` while a panel is open. Release held keys and stop reading input, but keep simulating: opening a panel never pauses a room.\n- `reservedRects` is an array of up to eight `{ x, y, width, height }` rectangles in CSS pixels of the game viewport. Keep the game\'s own HUD out of them, and nothing else: never resize, move, or letterbox the field because of them. The overlay sits on top of the game and the game must not shift when a bar or a pill appears. The field under a closed overlay stays visible and clickable.\n\nThe complete `OverlayView` value is `{ inputBlocked, reservedRects, safeArea?: { top, right, bottom, left }, shortcutEnabled? }`.\n\nThe host measures `safeArea: { top, right, bottom, left }` in CSS pixels of the game viewport, accounting for the iframe\'s position, borders and scale. It updates after viewport changes, including rotation. Use these values for HUD margins: `env(safe-area-inset-*)` inside the iframe usually reads zero. The kit also sets `--caisual-safe-top`, `--caisual-safe-right`, `--caisual-safe-bottom` and `--caisual-safe-left` on the game document root:\n\n```css\n.hud {\n top: max(16px, var(--caisual-safe-top, 0px));\n left: max(16px, var(--caisual-safe-left, 0px));\n}\n```\n\n`safeArea` is optional for compatibility with older hosts; treat a missing value as four zeros. Older kits keep receiving their supported geometry fields. During the opening screen, `inputBlocked` is true from the first view, including before the first host measurement.\n\n\n`c.overlay.open(panel)` asks the platform to open one of `home`, `room`, `invite`, `friends`, `voice`, `boards`. It is a request, not a permission: it creates no room and grants nothing. Outside caisual.com it does nothing.\n\nShift+Tab from the field opens the menu and Escape closes it. Text fields inside the game keep their own shortcut.\n\n### What a standard game no longer builds\n\nRemove these and let the overlay do them:\n\n- a start menu with Create, Join or a code field;\n- invitation links, copy buttons and share sheets;\n- the lobby: roster, ready, role and team pickers, the Start button;\n- a matchmaking screen with its cancel button;\n- a friends or party list;\n- voice buttons;\n- a leaderboard screen;\n- an Exit or Back to Caisual button;\n- a Play again button after a match.\n\nThe end bar recognizes optional standings and winners; the game may draw additional result details inside the field. `c.room.create`, `join`, `match` and `watch` stay available for a game that wants its own entry point: under a standard overlay the room they return becomes the current session and the standard controls follow it. Do not attach a second room controller from a second renderer.\n\n### Full screen\n\nA standard game fills the window. `html`, `body` and the game surface are 100% of the viewport: no maximum width, no header, no footer, no editorial frame, and no document scrolling at 1366x768 or 390x844, safe areas included. Only the HUD and compact controls sit over the scene.\n\nAim for the playable field to cover at least 70% of the visible area on desktop and 60% on mobile, counting only what shows or controls the game. A board with a fixed aspect ratio cannot always reach that: a square board on a 1366x768 window tops out near 56% before any HUD. That is the declared geometric exception: the board must then fill at least 90% of the largest rectangle that fits the area left free, and the HUD must have a stated ceiling, typically 48 to 64 pixels on desktop and about 160 pixels of controls on a phone.\n\n### Resume\n\nCaisual keeps one resume reference per game and per player, in a save key it owns. Leaving through the overlay with Leave for now stores the room code and detaches without giving up the seat; the standard menu then offers Resume, which rejoins from that code. Leave room removes the reference and gives up the seat, and so does a terminal room end. A `finished` room waiting for a rematch keeps its resume reference. A network drop keeps it.\n\nA game does not read or write that key, and does not build its own Resume button. A reference that is no longer valid returns the service error and the overlay explains it: it does not retry forever. Resume carries the room code, not a promise to reopen the same published version of the game.\n\n### Voice and leaderboards\n\nIn a standard game the overlay\'s voice panel carries Join, Leave, Mute, and the list of who is in the call, with the click the browser requires. A standard game does not draw its own voice buttons. The `room.voice` API below remains for games without the standard overlay and for server-side gain and proximity rules.\n\nLeaderboards are read by the overlay from the published manifest, using the boards and periods declared there. The overlay reads the official verified scores after a submission and offers Refresh: a game does not need a board screen. Scores are still submitted by the game or, better, by `server.js`.\n\n### Known gaps\n\nThree things are deliberately not in this version, and a game should not work around them:\n\n- resolving the original game version behind a persistent Resume;\n- inviting one friend straight into a room, as opposed to a party;\n- matchmaking for a whole group at once.\n\n## Daily challenge\n\n`c.daily` describes the day selected when the connection opened. `day`, `seed` and `expiresAt` stay fixed for that connection, including across UTC midnight. `expiresAt` is the next UTC midnight in milliseconds on the server clock.\n\n```js\nc.daily.day;\nc.daily.seed;\nc.daily.expiresAt;\nconst random = c.daily.rng();\nconst stop = c.daily.onChange(({ day, seed, expiresAt }) => {\n offerNextDailyRun({ day, seed, expiresAt });\n});\n```\n\n`onChange` reports a new `{ day, seed, expiresAt }` when the current UTC day changes. It returns an unsubscribe function and does not immediately replay the initial value. A suspended browser or a connection failure can delay the notification; the kit retries and reports the latest day when it can. The event does not mutate `c.daily` or reseed either generator. `rng()` always creates a fresh generator from the connection\'s original seed, and `random()` keeps advancing the original shared generator. Use the event\'s context explicitly for a new daily run, or open a new document. Repeated `connect()` calls return the same connection.\n\n`day` is a UTC date such as `"2026-09-04"`; `seed` is an unsigned 32-bit integer shared by all players of that game on that day. Both generators produce values in [0, 1). `c.time.now()` is in milliseconds aligned with the portal clock. Without a connection, daily data uses the local clock and hostname, with the same listener and generator behavior.\n\nOn the server, `room.daily` is `{ day, seed, expiresAt }`, fixed at **room creation**, persisted across sleep and rematches. Its seed matches a client connection opened for that game on the creation day. A client connected yesterday can therefore have a different `c.daily` from a room created today: send the room\'s daily context through game state when rendering its course. To switch a persistent or rematched room to a new daily course, create a new room.\n\nBoards accept `"day": "submit" | "start"` in the manifest. Omission means `submit`, preserving submission-day scoring. With `start`, which requires `source: "server"`, submit `room.board.submit(player, \'run\', score, { day: room.daily.day })`. An explicit `day` implies a daily score. It must equal the room\'s creation day and arrive no later than **10 minutes after `room.daily.expiresAt`**, including the exact boundary. A run started at 23:58 and submitted at 00:04 is attributed to its starting day. The all-time score is a separate submission without `day` or `daily`.\n\n| Error code | Meaning |\n| --- | --- |\n| `board_day_required` | A daily score on a start-day board omitted `day`. |\n| `board_day_mismatch` | `day` differs from `room.daily.day`. |\n| `board_day_expired` | More than 10 minutes have elapsed after the room\'s daily expiry. |\n| `board_day_policy` | An explicit day was used on a submit-day board or with `daily: false`. |\n\nThese server calls throw synchronously; handle expected errors inside the callback if the game should continue. Accepted scores keep their assigned day and submission timestamp across delayed writes and retries. Client scores continue to use the day when the portal receives them and cannot choose a day.\n\n`caisual dev --day YYYY-MM-DD` fixes the simulated day and seed for new connections and rooms. Clocks stay real: `expiresAt` is the next **real** UTC midnight at connection or room creation, so comparing it with `c.time.now()` or `room.time.now()` remains valid even for a simulated date. Restored rooms keep their original daily context. Restart dev and reload to change the flag; create a new room for the newly selected day.\n\n## Saves\n\nEach player has up to 64 saves per game. A save is any JSON value up to 256 KB when serialized.\n\n```js\nawait c.save.set(\'slot1\', { level: 3, coins: 120 }); // -> { key, bytes, updatedAt }\nconst data = await c.save.get(\'slot1\'); // -> the value, or null\nawait c.save.remove(\'slot1\');\nconst saves = await c.save.list(); // -> [{ key, bytes, updatedAt }]\n```\n\n- Keys use 1 to 32 characters: lowercase letters, digits, `_` or `-`, starting with a letter or digit.\n- `updatedAt` is a millisecond timestamp.\n- Saves are per player and per game. Another game cannot read them.\n- When not connected, saves go to the browser\'s local storage on the game origin.\n\nErrors reject the promise with an `Error` whose `code` is one of `invalid_request`, `not_found`, `save_limit`, `payload_too_large`, `rate_limited`, `invalid_ticket`, `internal_error`, or `offline`.\n\n## Leaderboards\n\nA leaderboard is identified by a board id chosen by the game. Scores are non-negative integers and higher is better. Each player keeps one entry per board, and one per board per day for daily boards: the best score is kept.\n\n```js\nconst result = await c.board.submit(\'main\', 1234);\n// -> { accepted: true, best: 1234, rank: 7, day: null, verified: false }\n\nconst daily = await c.board.submit(\'main\', 1234, { daily: true });\n// -> { accepted: true, best: 1234, rank: 7, day: "2026-09-04", verified: false }\n\nconst top = await c.board.top(\'main\', { daily: true, limit: 10 });\n// -> { day: "2026-09-04", entries: [{ rank, name, score, guest, me, verified }], me: { rank, score, verified } | null }\n```\n\n- Board ids use the same format as save keys.\n- `submit` never rejects because of connectivity. When the game is not connected it resolves `{ accepted: false, reason: "offline" }`.\n- `best` is the score kept for this player after the submission, which can be higher than the submitted one.\n- `rank` counts players with a strictly higher score. Ties are ordered by who reached the score first.\n- Accounts and guests are ranked separately. `top()` returns account players by default; pass `guests: true` to list guests instead. `me` always refers to the current player within their own category, even beyond `limit`.\n- `limit` is 1 to 100 and defaults to 10.\n- Pass `day: "2026-09-06"` to `top()` to read exactly that UTC day, even after midnight. A day implies the daily filter. A date that is not a real `YYYY-MM-DD` is rejected.\n- `verified` is `true` when the kept score came from the room server. Browser scores cannot replace a verified score.\n- Add `"boards": { "main": { "source": "server" } }` to `caisual.json` for a server-only board. It accepts scores only from `room.board.submit` in `server.js`.\n- An omitted board has `source: "client"`. Browser submissions keep working for existing games.\n- A browser submission to a server-only board rejects with `board_server_only`.\n\n### Verified scores in a single-player game\n\nA verified solo game uses a `room` mode with one place. The server validates each move and computes the score. This complete example accepts each of three cells once; a client cannot submit a score or claim the same cell twice. The standard overlay provides Play again. The optional client button below calls the same `restart()` API when no overlay is available.\n\n`caisual.json`:\n\n```json\n{\n "manifest": 1,\n "cover": "cover.png",\n "card": "card.png",\n "icon": "icon.png", "id": "verified-cells", "name": "Verified Cells", "platform": "both",\n "languages": ["en"], "overlay": { "version": 1 },\n "modes": [{ "id": "solo", "execution": "room", "players": { "min": 1, "max": 1 }, "lobby": false }],\n "boards": { "run": { "source": "server", "day": "start", "label": "Cells", "periods": ["daily", "all-time"] } }\n}\n```\n\n`server.js`:\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\n\nfunction reset(room) {\n room.state = { cells: [false, false, false], score: 0, daily: { ...room.daily } };\n}\nexport default defineGame({\n tickRate: 0,\n onCreate: reset,\n onRestart: reset,\n onMessage(room, player, message) {\n if (room.status !== \'playing\' || player.role === \'spectator\') return;\n const cell = message?.cell;\n if (!Number.isInteger(cell) || cell < 0 || cell > 2 || room.state.cells[cell]) return;\n room.state.cells[cell] = true;\n room.state.score += 1;\n if (room.state.score !== 3) return;\n const result = { standings: [{ playerId: player.id, score: room.state.score }], winners: [player.id], unit: \'points\' };\n room.board.submit(player, \'run\', room.state.score);\n if (room.time.now() > room.daily.expiresAt + 10 * 60_000) {\n // La corsa scaduta conserva il record assoluto; una stanza nuova scegliera\' il nuovo giorno.\n room.end({ ...result, data: { dailyExpired: true } });\n return;\n }\n room.board.submit(player, \'run\', room.state.score, { day: room.daily.day });\n room.end(result, { rematch: true });\n },\n});\n```\n\n`client/index.html`:\n\n```html\n<!doctype html>\n<html lang="en">\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">\n<title>Verified Cells</title>\n<style>body{margin:0;min-height:100dvh;display:grid;place-content:center;gap:12px;background:#000000;color:#ffffff;font:16px system-ui}button{min-width:48px;min-height:48px}</style>\n<p id="score"></p><div id="cells"></div><button id="again" hidden>Play again</button>\n<script type="module">\n import { caisual } from \'/__caisual/kit/v1.js\';\n const c = await caisual.connect();\n const score = document.querySelector(\'#score\'), cells = document.querySelector(\'#cells\'), again = document.querySelector(\'#again\');\n let room = null, blocked = false, stops = [];\n function draw() {\n cells.replaceChildren();\n if (!room) { score.textContent = \'Choose Play to start.\'; again.hidden = true; return; }\n score.textContent = `${room.state.score} / 3`;\n room.state.cells.forEach((claimed, cell) => {\n const button = document.createElement(\'button\'); button.textContent = claimed ? \'\u2713\' : String(cell + 1);\n button.disabled = blocked || room.connection !== \'connected\' || room.status !== \'playing\' || claimed;\n button.onclick = () => room.send({ cell }); cells.append(button);\n });\n again.hidden = c.session.capabilities.overlay || room.status !== \'finished\';\n again.disabled = blocked || room.connection !== \'connected\';\n }\n again.onclick = () => { again.disabled = true; room.restart(); };\n c.overlay.onChange((view) => { blocked = view.inputBlocked; draw(); });\n function attach(next) {\n stops.forEach((stop) => stop()); stops = []; room = next;\n if (room) stops.push(room.onState(draw), room.onStatus(draw), room.onConnection(draw));\n draw();\n }\n c.session.onChange((session) => attach(session.kind === \'room\' ? session.room : null));\n c.session.ready();\n if (!c.session.capabilities.overlay && c.connected) attach(await c.room.create({ mode: \'solo\' }));\n</script>\n</html>\n```\n\n`room.board.submit(\'run\', score, { day: room.daily.day })` is also accepted in a solo room: the sole non-spectator member is implicit. In multiplayer, always pass `player` or its id first; omitting it throws `board_player_required`. `room.board.submit` queues the score; use `room.onScoreQueued` and the overlay\'s board refresh to observe it, not a state transition as a storage receipt. A rematch keeps the room\'s creation day; a new room selects the current day.\n\n## Device\n\n`c.device` contains the browser and device report collected while `connect()` runs:\n\n```ts\ninterface DeviceReport {\n webgl2: boolean;\n webgpu: boolean;\n wasm: boolean;\n threads: boolean;\n isolated: boolean;\n gpu: \'hardware\' | \'software\' | \'none\';\n memoryMb: number | null;\n cores: number | null;\n mobile: boolean;\n tier: \'low\' | \'mid\' | \'high\';\n}\n```\n\n`device.isolated` measures the browser\'s actual `crossOriginIsolated` state. Games embedded in the portal report `false`; shared memory and threaded WebAssembly are unavailable there. This runtime probe does not enable isolation.\n\n\nUse capability fields to choose a renderer, then use `tier` to reduce pixel ratio and quality on smaller devices:\n\n```js\nconst renderer = c.device.webgpu\n ? createWebGpuRenderer()\n : createWebGl2Renderer();\n\nconst pixelRatio = c.device.tier === \'high\' ? devicePixelRatio : 1;\nconst quality = c.device.tier === \'low\' ? \'low\' : \'high\';\nrenderer.configure({ pixelRatio, quality });\n```\n\nThe probe takes at most 1.5 seconds. `memoryMb` and `cores` are `null` when the browser does not expose them. The report stays in the browser and is not saved or sent to Caisual.\n\n### Two front ends, one game\n\nKeep one `client/index.html`, one game ID and one server. With `platform: "both"`, choose separate front ends in that entry without navigating or adding another iframe:\n```js\nimport { caisual } from \'/__caisual/kit/v1.js\';\nconst c = await caisual.connect();\nlet preference = null;\ntry { preference = localStorage.getItem(\'layout\'); } catch {}\nconst touch = preference === \'touch\' || (preference !== \'desktop\' && (c.device.mobile || matchMedia(\'(pointer: coarse)\').matches));\nconst screen = touch ? await import(\'./touch/main.js\') : await import(\'./desktop/main.js\');\nscreen.mount({ c, root: document.querySelector(\'#app\') });\n```\nOffer a manual layout choice, persist it when storage is available, and keep rules and room connections shared. Both front ends use relative asset paths inside `client/`.\n\n## Game version changes\n\nNew games use the current version. Existing rooms retain their version for their whole life, including invitations, typed codes, friends, spectators and Resume. Matchmaking never mixes versions; an accepted reservation can finish on its original version. Saves, boards and `room.shared` remain per game, with data compatibility handled by the creator. See [Versions and rollback](./publish.md#versions-and-rollback).\n\nA room create or match operation rejects with `version_outdated` and `currentVersion` when the loaded game is no longer current. A join, watch or reconnect rejects with `version_mismatch` and `roomVersion` when the room uses another version. The standard overlay shows **This game was updated** with **Reload game** for outdated pages; a mismatched code or Resume reloads the portal with the room invitation, preserving watch intent for spectators.\n\nCustom menus can subscribe with `c.room.onError(listener)`, which returns an unsubscribe function. The error has `code`, `message`, and the applicable version number. The operation also rejects with that error. `c.room.reload()` reloads the portal on the current game after `version_outdated`, or on the last mismatched room after `version_mismatch`. Show a button calling it after either error. This works without the standard overlay.\n\n`caisual dev` always uses game version 1 and does not simulate version changes.\n\n## Rooms\n\nA room brings players into the same running game. Creating and joining require a published `server.js`; single-player games can ignore `c.room`.\n\nIn a standard game the overlay creates, joins, matches and watches on the player\'s behalf, and hands the game the room through `c.session`. The calls below stay available, and their result becomes the current session. Read them for what the room object offers; do not rebuild the entry screens around them. An optional [MatchResult](#recognized-match-results) in `room.end` lets the overlay display winners and standings while `room.result` keeps the original JSON.\n\n```js\nconst c = await caisual.connect();\n\nc.room.invited; // invitation code from the game page, or null\n\nconst room = await c.room.create({ mode: null });\n// Or join the invitation that opened the game:\nconst invitedRoom = await c.room.join();\n// Or enter a code supplied by the player:\nconst codedRoom = await c.room.join(\'ABC234\');\n\nroom.code;\nroom.seed; // unsigned 32-bit integer fixed for this room\nroom.tickRate;\nroom.latency;\nroom.invite(); // { code: "ABC234", url: "https://caisual.com/r/ABC234" }\n```\n\nPass a mode id from the manifest to `create({ mode })`, or `null` to use the root configuration. Optional `players` and `lobby` on that mode replace the root values; joining keeps the configuration of the room being joined. `join()` uses `c.room.invited`; without an invitation, pass the six-character code explicitly. A standard game does not need `invite()`: the overlay owns the invitation panel and the copy action.\n\n### Crew\n\nThe kit automatically reports the player\'s current room to the Caisual portal, so the player\'s friends can join with one click. The game does not need to send or handle anything for this. There is no `c.crew` API in this version. In a standard game the friends and party list is a panel of the overlay, so there is nothing to draw either.\n\n### Matchmaking\n\nUse `c.room.match()` to find players who requested the same mode and key. The key must contain exactly the fields declared by that mode\'s `matchmaking.key` in `caisual.json`.\n\n```js\nconst room = await c.room.match({\n mode: \'daily\',\n key: { day: c.daily.day, stage: 3 },\n onWaiting({ players, min, max }) {\n showQueue(`${players}/${max} players, ${min} required`);\n },\n});\n```\n\nMatchmaking uses the selected mode\'s resolved `players` and `lobby`, and that mode\'s `matchmaking.timeoutMs`. A room opens as soon as the queue reaches the resolved `players.max`. When `matchmaking.timeoutMs` expires, it also opens if at least `players.min` players are waiting. Otherwise the promise rejects with `no_match`, and the game should offer the player another option. A new search first tries to fill a matching room that is already open and can still accept players.\n\nPass an `AbortSignal` as `signal` to let the player cancel a search. Cancellation rejects with `cancelled`. In a standard game the search screen, its Cancel button and the lobby that follows are the overlay\'s: declare `matchmaking.defaults` in the mode and the player can start a search from the standard menu without the game passing a key.\n\nRoom status is one of:\n\n- `lobby`: players are joining and choosing their setup.\n- `countdown`: the lobby has accepted `start()` and play begins at the announced server time.\n- `playing`: the game server is running the match.\n- `finished`: the match is over and the room is waiting for a rematch, with sockets open and `room.result` available.\n- `ended`: the match or connection has ended. `room.result` contains the result last reported by the room. A definitive connection closure uses `{ closed: 4003 }` when the player was kicked, `{ closed: 4004 }` when the room ended, `{ closed: 4005 }` when the published version closed, or `{ closed: 4006 }` when the same player opened the room in another tab.\n\nThe current lobby data is available directly:\n\n```js\nroom.players; // [{ id, name, guest, role, team, ready, connected }]\nroom.you; // this player\'s id\nroom.host; // the current host\'s id, or null\n\nroom.ready(true);\nroom.setRole(\'captain\');\nroom.setTeam(1);\n\nif (room.you === room.host) room.start();\n```\n\nIn a standard game the overlay calls these four for the player: read `room.players` to draw the field, not to build a roster panel. `ready`, role, team, and `start()` are lobby actions. Starting requires the host, every connected player to be ready, and the player, role, and team minimums from the manifest. Calling `start()` begins a three-second countdown. A role or team change clears that player\'s ready state. The built-in `spectator` role is still a player slot for setups such as a shared screen with phone controllers. Use `watch()` for someone who only observes and does not occupy a player slot.\n\n### Rematch in the same room\n\nThe server opts in per match with `room.end(result, { rematch: true })`. The room becomes `finished`, keeps its code, members, state and open connections, and exposes the result through `room.result` and `onStatus(\'finished\', result, at)`. Voice and spectator streams remain connected. `room.end(result)` or `{ rematch: false }` still ends the room permanently with status `ended` and close code 4004.\n\nWith `rematch: true` in a multiplayer room, each connected player calls `room.restart()` once to become ready for the rematch. In `finished`, `room.players[].ready` means rematch readiness. Once every connected non-spectator player is ready and the mode\'s `players.min` is met, the host calls `room.restart()` again to confirm. The first host call only registers readiness. A host with the built-in `spectator` role does not register readiness and can confirm once the players are ready. Repeated non-host calls do nothing. Calling outside `finished` fails with `rematch_unavailable`; an early host confirmation reports `players_not_ready` through `room.onError`.\n\nThe standard overlay handles these calls with Play again, the readiness count and names, and Start rematch for the host. Games without the overlay can call `restart()` directly. No member is removed for declining. With `rematch: true`, there is no automatic start after a departure: the current host must confirm.\n\nAt confirmation, the kit clears the result to `null` and all readiness flags, then calls optional `onRestart(room)` with status `lobby` when the mode has a lobby, or `playing` otherwise. **The kit does not reset `room.state`.** Reset match data in `onRestart`, keeping series scores or other data as needed. With a lobby, players choose their setup and get ready again before the normal countdown and `onStart`. Without a lobby, `onStart` follows `onRestart` immediately. Clients receive the new state and an `onStatus` transition with a null result. The room identity, seed and tick sequence are retained; `session.onChange` does not create a new session for a rematch. Listen to `room.onState` and `room.onStatus`.\n\n### Fast rematches and solo rooms\n\nThe existing `rematch: true` flow above is unchanged for multiplayer rooms. To shorten it, use:\n\n```js\nroom.end(result, { rematch: { keepSetup: true, autoStart: true } });\n```\n\nBoth flags default to `false`. `keepSetup` restores the previous readiness flags, keeps roles and teams, and skips the lobby: `onRestart` runs with status `countdown`, followed by the usual three-second countdown and `onStart`. The result clears before `onRestart`. Even a mode without a lobby gets this countdown. Readiness during `finished` is still consent for the next match, separate from the previous setup. Player, role and team minimums are rechecked during the countdown; if they fail, the room returns to the lobby so the setup can be repaired.\n\n`autoStart` starts the rematch when all connected non-spectator players have accepted and the mode\'s minimum is met, including after a departure leaves that condition satisfied. The overlay shows the readiness count and names without a host confirmation button or wait-for-host text. With `autoStart`, the host can also call `restart()` again to confirm early once at least `players.min` connected non-spectator players have accepted. Members who have not accepted remain in the room and join the next phase; they are not removed. With `autoStart` alone, the next phase is the usual lobby or immediate play; combine it with `keepSetup` to skip another ready/start cycle.\n\nA room whose resolved `players.max` is `1` is a solo room, including when it inherits `lobby: true`: it starts on entry and shows no lobby, invitation, code or wait-for-host prompt. In `finished`, one `room.restart()` immediately runs `onRestart`, then `onStart`, without consent or host confirmation or a countdown. The standard Play again button makes that one call. After a terminal end it creates a new solo room without opening or copying an invite. No additional manifest field is needed; `caisual init` without `--multiplayer` still creates a local game.\n\nThe waiting rules are:\n\n- All pending `room.schedule` handlers are cancelled when the match finishes, including handlers already due in the same batch. Scheduling during `finished` has no effect. Schedule new work in `onRestart` or `onStart`. Tick callbacks stop immediately; game input during the wait is discarded, and queued continuous input is cleared on the client.\n- `onEnd` runs once for the completed match, with status `finished` and its result. A subsequent timeout only closes the room and does not call `onEnd` again. Restart does not resubmit or clear pending board scores: each submission is drained once from the existing score queue. The game must not submit the previous match\'s scores again in `onRestart`.\n- Disconnecting clears that member\'s readiness and transfers the host to the oldest connected member. Disconnected players do not count toward readiness or the minimum. The usual 60-second reconnection grace applies; persistent members keep their seats after it. Rejoining requires a new readiness call. Connection and departure callbacks continue during the wait.\n- New members may join by invitation during the wait, up to the resolved `players.max`, and start unready. A lobby mode therefore reopens admission while `finished`; without a lobby, admission continues as during play. Disconnected members still occupy seats until removed. Roles and teams retain their existing capacity rules.\n- `watch()` spectators do not vote or occupy seats and keep following the same delayed stream across matches. Members with role `spectator` occupy a seat but do not vote or count toward the rematch minimum.\n- The room closes with 4004 exactly two minutes after entering `finished` if no restart is confirmed, retaining the last result. Readiness, joins and pings do not extend this deadline. It also applies to persistent rooms and survives sleep or restoration; it does not depend on active ticks.\n\n`finished` is a new status, not a terminal connection state. Older room clients receive the unfamiliar status and keep their sockets open instead of taking their `ended` cleanup path; they have no `restart()` control, and older overlays may retain their previous screen. Use an updated kit for games opting into rematches, and explicitly handle `finished` in game status listeners.\n\nThe server owns room state. Read it and react to updates, but do not assign to it or mutate nested values from the browser:\n\n```js\ndraw(room.state);\n\nconst stopState = room.onState((state, tick, serverTime) => {\n draw(state);\n});\n\nconst stopPlayers = room.onPlayers((players) => updateLobby(players));\nconst stopStatus = room.onStatus((status, result, at) => showStatus(status, result, at));\n\nstopState();\nstopPlayers();\nstopStatus();\n```\n\n`room.tick` identifies the latest state. The kit applies structural updates in order for both tick-based and event-only servers. Full state is sent on entry, reconnection or resync when an update does not match the current tick; normal updates, including every hundredth tick, remain diffs. `room.serverTime()` returns milliseconds aligned with the room clock and is kept current by a ping every five seconds.\n\n`room.tickRate` is the current effective server frequency, including reductions caused by the CPU budget; zero means event-only. Updates arrive with state diffs and snapshots, including an empty diff when only the frequency changes. `room.latency` is the smoothed round-trip time in milliseconds, or `null` before the first pong and after a dropped connection until the next pong. Each pong uses 20% of the new RTT and 80% of the previous estimate, with the first sample used directly. Read the property when drawing network status; there is no `onLatency` listener. Spectators expose the same properties; their tick rate follows the delayed state stream.\n\nSend JSON input to `onMessage` in the server definition, and receive JSON sent or broadcast by the server:\n\n```js\nroom.send({ type: \'fire\', target: 3 });\n\nconst stopMessages = room.onMessage((message) => {\n showEvent(message);\n});\n```\n\nThe kit numbers outgoing inputs in increasing order. It automatically reconnects temporary failures with delays of 1, 2, 4, then 8 seconds, for at most the room\'s 60-second grace period. Each attempt gets a fresh room token. A successful reconnect replaces local state with a full server state. `room.send` calls while reconnecting throw an error with `code: "offline"`.\n\nUse `room.input(value)` for continuous controls, including calls from every animation frame:\n\n```js\nroom.input({ type: \'move\', x: axisX, y: axisY });\nroom.onError(({ code }) => {\n if (code === \'rate_limited\') showInputWarning();\n});\n```\n\n`input` copies and keeps only the latest JSON value in one slot. It coalesces updates and sends at most 30 times per second, or at the effective `room.tickRate` when that is lower and positive. With `tickRate: 0`, it still sends at most 30/s. It also waits for budget used by `send`. Values with the same JSON serialization are not resent on the same connection. Combine independent controls into that one value; there is no channel option. On the server it is an ordinary message passed unchanged to `onMessage`, exactly like `send`, with no extra envelope.\n\nDuring reconnection, `input` accepts updates without throwing `offline`. After the new welcome it sends only the latest value, even if it was sent on the previous connection. It never replays intermediate values or old commands. Use `send` for individual actions such as firing or confirming a turn; `send` still throws `offline` during reconnection. Invalid JSON input can throw `invalid_request`. Input stops after leaving, disconnecting intentionally or ending the room.\n\nGame messages are limited to 64 KB per frame in either direction and 30 per second per connection. Incoming service frames also have a 64 KB limit; state synchronization carries the separately limited room state. Excess messages are dropped; `room.onError` receives `rate_limited` at most once per second across both budgets. Protocol service messages, including ping, lobby actions, state requests and voice signaling, have a separate 30/s budget with the same drop policy. More than 150 attempts in each of three consecutive one-second windows in either budget closes the connection with 4008 `rate_limited`; the kit does not reconnect automatically after this abuse closure. Abuse windows start with the first message on the connection; a normal or empty window resets the sequence. Oversized game frames close with 4009 `message_too_large`, reported through `room.onError`; malformed frames use 4009 `bad_message`. Neither closure is retried automatically.\n\nCall `room.leave()` for an intentional departure. The kit does not reconnect after leaving, being kicked, the room ending, the published version closing, or the same player opening the room in another tab.\n\n`room.disconnect()` is the other departure: it stops the transport, the retries and voice without sending a leave, so the server keeps the seat under its own persistence and grace rules. It is not reversible on the same object; returning means entering again from the code. The overlay uses it for Leave for now, together with the resume reference.\n\nA room also exposes `room.mode`, `room.countdownAt`, `room.connection`, `room.metadata`, and the `onMetadata` and `onConnection` listeners. `connection` is one of `connecting`, `connected`, `reconnecting`, `disconnected`, `ended`, `closed`, or `replaced`, where `replaced` means the same player opened the room in another tab. Unlike `session.onChange` and `overlay.onChange`, these listeners do not repeat the current value: read the getter first.\n\n`room.onError(listener)` reports technical protocol errors of the room as `{ code, message }`; it is not the place where a game reads its own result.\n\n`room.onScoreQueued(listener)` and `room.queuedScores` cover scores submitted for this player by `server.js`. Each entry is `{ board, player, score, day, submittedAt }`, only the owner\'s connection receives it, and the last 32 are kept. It is a technical notice that the server accepted the score, not a receipt that it is already on the board: read the board back with `c.board.top()` for that.\n\n`await room.requestRole(\'scout\')` asks the server for a role change during a match. It works only while the room is playing, only for a role declared in the manifest, and only when `server.js` defines `onRoleRequest(room, player, role)`; the server approves by calling `room.setRole`. Without that callback nothing changes, and the capability shows as `false` in `c.session.capabilities`. It is not a shortcut for changing roles from the browser.\n\nRoom creation, joining, and matchmaking reject with an `Error` carrying a stable `code`. Common codes are `invalid_request`, `no_server`, `no_match`, `cancelled`, `room_not_found`, `room_full`, `room_playing`, `room_ended`, `rate_limited`, `invalid_ticket`, `internal_error`, and `offline`. `no_server` means the published game has no multiplayer server. When `c.connected` is `false`, `create`, `join`, and `match` reject with `offline`.\n\n- `invalid_role`: a requested role id is malformed or is not declared in the manifest. Request a declared role id.\n- `role_change_unavailable`: the room is disconnected, is not playing, or `server.js` has no `onRoleRequest`. Wait for a connected playing state and provide that callback before offering the action.\n- `role_change_refused`: `onRoleRequest` returned without assigning the requested role. Leave the current role in place, or have the server approve with `room.setRole`.\n- `version_closed`: the room connection ended because its published version closed. Reopen the current game version and enter a current room.\n\nEvery listener call on a room returns a function that removes that listener: `onState`, `onPlayers`, `onStatus`, `onMessage`, `onMetadata`, `onConnection`, `onError`, and `onScoreQueued`.\n\n### Responsive action games\n\n1. Start with `npx @caisual/cli init --arcade my-arena`, a complete English/Italian canvas game with shared rules and tests.\n2. Accumulate `deltaSeconds` on the server and advance shared physics at a fixed 1/60 second step.\n3. Keep positions, collision or range checks, cooldowns and scores authoritative in `server.js`.\n4. Use `room.input()` for continuous controls and `room.send()` for discrete actions such as the starter\'s pulse.\n5. Number commands in the game and publish each player\'s last **applied** `ack` in `room.state`; transport sequence numbers are separate.\n6. Because `input()` coalesces values, send a bounded batch of unacknowledged commands; deduplicate on the server and consume at most one per simulation step.\n7. Predict your own entity with the shared step, replace it with each authoritative snapshot, then replay only commands after `ack`; ease corrections in the drawing only.\n8. Buffer remote samples by the `onState` timestamp and render behind `room.serverTime()`, accounting for RTT in `room.latency`, effective `room.tickRate`, and spectator `delayMs`.\n9. Clear pending controls on reconnection or a new round; keep the HUD inside safe areas and outside `reservedRects`, and end with `standings` plus `keepSetup`/`autoStart` rematches.\n10. Run the generated browser fixture and `/__caisual/players?n=2` with `dev --latency 120 --jitter 40 --loss 2`; see [Testing with a browser](/docs/local-development#testing-with-a-browser).\n\n### Spectators\n\nIn a standard game the overlay offers watching from the menu and the session arrives as `{ kind: \'watch\' }`. Use `c.room.watch(code)` to observe a room without joining it as a player:\n\n```js\nconst view = await c.room.watch(\'ABC234\');\n\ndraw(view.state);\nview.onState((state) => draw(state));\nview.onPlayers((players) => updateRoster(players));\nview.onStatus((status, result) => showStatus(status, result));\nview.onMessage((message) => showEvent(message));\n\nview.leave();\n```\n\nThe returned `Spectate` object exposes `state`, `tick`, `seed`, `status`, `players`, `host`, `code`, `result`, `delayMs`, the four listeners shown above, `serverTime()`, and `leave()`. It receives the room\'s public state, snapshots and updates, player list, status, and messages broadcast by `server.js`. The kit repairs a missed update automatically and reconnects temporary failures for the same 60-second grace period used by players.\n\nPublic room events are delayed by `delayMs`, which defaults to 3000 milliseconds. A game can set `"spectators": { "delayMs": N }` in `caisual.json`, where `N` is from 0 to 30000, or set `"spectators": false` to disable watching.\n\nA spectator has no `you`, `invite()`, `send()`, or voice API. Watching does not add anyone to `room.players`, does not affect roles, teams, player minimums, the host, or room lifetime, and is not visible to `server.js`. `watch()` can reject with `room_not_found`, `room_ended`, `spectators_disabled`, `spectators_full`, `rate_limited`, `offline`, or `invalid_request`.\n\n### Replays\n\n`"replays": true` in the manifest opts room matches into recording. The default is false. A replay link at `/g/<slug>/replay/<id>` opens the recorded version in a `kind: \'watch\'` session, with `room.role === \'spectator\'` and `room.replay === true`. Live views have `replay === false`. During replay, `c.room` also exposes the spectator reading fields and `c.room.watch()` returns the recording. The overlay supplies play/pause, seek and 0.5/1/2/4 speed. `serverTime()` follows that playback clock; listeners update on backward seeks as well.\n\nThe finished match panel offers Watch replay and Copy link after the archive is ready. Recordings include shared state, public names and roles, status and result. Private messages, voice and raw inputs are excluded. A 10 MiB or 30-minute limit produces a partial recording. Links expire 30 days after the start; deleting a game removes its replays. See [Spectators](/docs/spectators#replays) for the renderer flow.\n\n## Voice\n\nEvery room has a `room.voice` object. Voice is disabled by default and is enabled with the manifest\'s `voice` field.\n\nIn a standard game the overlay\'s voice panel carries Join, Leave, Mute, and the list of who is in the call, with the click the browser requires. A standard game does not draw its own voice buttons. The `room.voice` API below remains for games without the standard overlay and for server-side gain and proximity rules.\n\nA game with its own controls must offer an explicit one, because `join()` must be called from a click or another user gesture so the browser can start audio and, when publishing, request microphone permission.\n\n```js\nconst micButton = document.querySelector(\'#mic\');\nconst voiceList = document.querySelector(\'#voice-list\');\n\nfunction renderVoice(peers = room.voice.peers) {\n voiceList.replaceChildren(...peers.map((peer) => {\n const item = document.createElement(\'li\');\n const player = room.players.find((entry) => entry.id === peer.id);\n item.textContent = `${player?.name ?? peer.id}: ${\n peer.speaking ? \'speaking\' : peer.muted ? \'muted\' : \'quiet\'\n }`;\n return item;\n }));\n micButton.textContent = room.voice.state === \'off\'\n ? \'Join voice\'\n : !room.voice.mic ? \'Listening\' : room.voice.muted ? \'Unmute\' : \'Mute\';\n}\n\nmicButton.addEventListener(\'click\', async () => {\n if (room.voice.state === \'off\') await room.voice.join();\n else if (room.voice.mic) room.voice.mute(!room.voice.muted);\n renderVoice();\n});\n\nroom.voice.onPeers(renderVoice);\nroom.voice.onState(() => renderVoice());\nrenderVoice();\n```\n\n`room.voice.mode` is `none`, `room`, `team`, or `proximity`. In `room` mode, every participant in voice can hear every other participant. In `team` mode, players hear only their team. In `proximity` mode, the room server controls the gain between participants. Call `room.voice.join({ mic: false })` to listen without opening or publishing a microphone. Spectators join in listening mode when they call `join()` without options. A spectator that calls `join({ mic: true })` receives the `spectator` error.\n\nThe room server authorizes every voice track by team and gain. Listening that is no longer allowed is refused or closed.\n\n`room.voice.state` is `off`, `joining`, `on`, or `reconnecting`. `room.voice.mic` is `true` while the local player is publishing. `room.voice.muted` and `room.voice.speaking` describe the local microphone. `room.voice.peers` contains the other voice participants as `{ id, mic, muted, speaking, volume, gain }`. A listening participant has `mic: false`, `muted: true`, and `speaking: false`. `volume` is the local setting and `gain` is the value from the room server. Use `room.voice.setVolume(playerId, volume)` with a value from 0 to 1 to change only local playback.\n\n`room.voice.onPeers(listener)` runs when participants, microphone state, mute state, speaking state, volume, or gain changes. `room.voice.onState(listener)` reports connection state changes. Both return a function that removes the listener.\n\nCall `room.voice.leave()` to stop publishing or listening without leaving the room. `room.voice.mute()` requires an active published microphone and otherwise throws `not_publishing`. `room.leave()` and the end of the room stop voice automatically.\n\n`join()` rejects with an `Error` carrying one of these stable codes: `voice_disabled`, `permission_denied`, `unsupported`, `spectator`, `offline`, or `voice_error`. Voice can reconnect after a temporary room or media connection failure. The state becomes `reconnecting` while the kit retries.\n\n## Server\n\nPut `server.js` next to `caisual.json` and publish it with the game. See [publish.md](./publish.md#multiplayer-server) for the file rules, validation, and publishing flow.\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\n\nexport default defineGame({\n tickRate: 20, // required, an integer from 0 to 60; 0 runs only in response to events\n onCreate(room) {},\n onStart(room) {},\n onJoin(room, player) {},\n onConnection(room, player, connected) {},\n onLeave(room, player, reason) {}, // "left", "timeout", or "kicked"\n onRoleRequest(room, player, role) {}, // approve with room.setRole\n onMessage(room, player, message) {},\n onTick(room, deltaSeconds) {},\n onEnd(room) {},\n onRestart(room) {},\n});\n```\n\n`tickRate` is the only required field: `defineGame` throws a `TypeError` when it is missing or is not an integer from 0 to 60. All callbacks are optional. A player is `{ id, name, guest, role, team, connected }`.\n\n### Callback order\n\n- `onCreate` runs once when the room is first created, before any player joins.\n- `onJoin` runs when a player first enters the room, not when that same member reconnects. Without a lobby, the first player\'s `onJoin` is followed by `onStart`.\n- `onStart` runs when the room changes to `playing`. Without a lobby this is the first player entry. With a lobby it is after the host starts, the three-second countdown finishes, and the room still meets its minimums.\n- `onTick` runs for each active game tick when `tickRate` is greater than zero. `onMessage` runs for accepted client game messages, and `onRoleRequest` runs for an in-match role request when that callback exists. These events are processed serially, so their relative order is the order in which the room processes them.\n- `onConnection(room, player, connected)` runs with `false` when an existing member loses their connection and with `true` when that disconnected member returns. Both `player.connected` and `room.players` are already updated. It does not run for the first entry, a socket replacement while the member is still connected, or a permanent removal. Restoring a room reconciles actual connections: members saved as connected whose sockets are gone receive `false`, and their later return receives `true`. Surviving sockets receive no extra callback.\n- `onLeave` runs only when a player is removed with reason `left`, `timeout`, or `kicked`. A dropped connection calls `onConnection` during the grace period and does not call `onLeave`.\n- `onEnd` runs after the callback that requested `room.end`, with the final result and status `ended` or `finished`. Automatic room endings also use it, except closure of an already `finished` match, which must not run it twice.\n- `onRestart` runs when a rematch starts, after clearing the result and selecting the next status. Readiness is reset by default or restored with `keepSetup`. The fast-rematch and solo rules above override the default host-confirmed flow. With a lobby it prepares `lobby`; without one it prepares `playing` and is immediately followed by `onStart`. It is optional; `room.state` is preserved unless the game changes it.\n\nThere is no `onResume` callback. A sleeping room restores its saved state without calling one. When a scheduled time arrives, the room invokes the method named by `room.schedule`.\n\nFor a turn-based game, store the active player\'s id and pause that turn when the player disconnects. This also works with `tickRate: 0`, because callback state changes are published immediately:\n\n```js\nonConnection(room, player, connected) {\n if (room.state.turnPlayerId === player.id) {\n room.state.turnPaused = !connected;\n }\n},\nonMessage(room, player, message) {\n if (room.state.turnPaused || player.id !== room.state.turnPlayerId) return;\n applyTurn(room, player, message);\n},\n```\n\nIf turns have deadlines, save the remaining time when pausing and calculate a new deadline on return. Scheduled handlers must check whether the turn is still paused or current. Decide separately in `onLeave` how to handle a permanent departure. Secret redelivery remains a client responsibility through `room.onConnection`; see [Hidden information](#hidden-information).\n\nThe room object provides:\n\n```js\nroom.id;\nroom.seed;\nroom.mode;\nroom.status;\nroom.tick;\nroom.tickRate;\nroom.result;\nroom.state;\nroom.players;\nroom.host;\n\nroom.broadcast(message);\nroom.send(playerOrId, message);\nroom.kick(playerOrId);\nroom.setRole(playerOrId, role);\nroom.setTeam(playerOrId, team);\nroom.end(result);\nroom.end(result, { rematch: true });\n\nawait room.save(\'round\', value);\nawait room.load(\'round\');\nawait room.shared.get(\'ship_abc\');\nawait room.shared.set(\'ship_abc\', value);\nawait room.shared.delete(\'ship_abc\');\nawait room.shared.list(\'ship_\');\nawait room.shared.increment(\'visits\', 1);\nroom.schedule(milliseconds, \'methodName\', payload);\nroom.board.submit(playerOrId, \'main\', score, { daily: true });\nroom.board.submit(playerOrId, \'run\', score, { day: room.daily.day });\n\nroom.daily.day;\nroom.daily.seed;\nroom.daily.expiresAt;\nroom.time.now();\n\nroom.voice.mode;\nroom.voice.setGain(listener, speaker, 0.25);\nroom.voice.setProximity(playerA, playerB, 0.5);\n```\n\nSet `room.state` in `onCreate`, then mutate it only in server callbacks. It must remain plain JSON and may be at most 512 KB when serialized. `broadcast` sends a JSON message to everyone; `send` targets one player. `end` records a JSON result and closes the room unless rematch is enabled with `true` or an options object. See [Rematch in the same room](#rematch-in-the-same-room) for readiness, callback order, timer cancellation and the waiting deadline. Room saves use keys with the same format as player save keys and values up to 256 KB. `schedule` names a method on the definition so it can run even after a quiet room resumes. Scores submitted through `room.board` are verified. The room fixes their UTC `day` and millisecond `submittedAt` when `submit` is called, so delayed writes and retries do not move them to another day. Older queued scores without these fields retain the write-time day. Boards default to submission-day scoring. Set `day: "start"` and pass `{ day: room.daily.day }` for the starting day with a 10-minute grace period after midnight. See [Daily challenge](#daily-challenge) for expiry, errors and local day simulation.\n\n### Recognized match results\n\n`room.end(result)` still accepts any JSON. The optional `MatchResult` type is exported by the contracts and by `@caisual/kit` and `@caisual/kit/server`:\n\n```ts\ntype MatchResult = {\n standings: Array<{ playerId: string; score?: number; rank?: number }>;\n winners?: string[];\n draw?: boolean;\n unit?: \'points\' | \'time\' | \'distance\' | string;\n data?: JsonValue;\n} & Record<string, JsonValue>;\n```\n\nOrder `standings` from first place onward. Scores must be finite numbers; ranks are positive safe integers. The overlay uses the supplied order, displays player names and optional scores, and uses `rank` when present. `draw: true` takes precedence; otherwise `winners` identifies the winners. Without `winners`, the first row wins, together with rows sharing its explicit rank. Equal scores alone do not imply a draw. An explicit empty `winners` list means nobody won.\n\nThe end bar shows You won, You lost or Draw only for a participating player; spectators and players missing from the standings see Match finished. Outcome labels and the built-in units are translated in six languages. Custom units are shown as plain text; `time` and `distance` do not convert values or imply a measurement scale.\n\nReading is tolerant: unknown fields are ignored by the overlay, unknown or duplicate players are discarded, invalid scores and ranks are omitted, and winners must occur in the retained standings. With no usable standings the generic end bar remains. **`room.result` keeps the original JSON without normalization**, including `data` and any other game fields. Results are public to room members and spectators, so keep secrets elsewhere.\n\n```js\nroom.end({\n standings: [{ playerId: winner.id, score: 12, rank: 1 }, { playerId: other.id, score: 9, rank: 2 }],\n winners: [winner.id], unit: \'points\', data: { rounds: 3 },\n}, { rematch: true });\n```\n\n### Hidden information\n\nEverything in `room.state` reaches every player and every spectator. Never store cards in hand, secret roles, fog of war, or any other private value there.\n\nKeep secrets in room saves through `room.save` and `room.load`, which are server-only, or in module-level variables in `server.js`, keyed by room when needed. Deliver a secret to one player with `room.send(player, { type: \'hand\', cards })`. `onJoin` runs only on the player\'s first entry, not on reconnection, so the client requests its secrets for the current connection and every later reconnection:\n\n```js\nif (room.connection === \'connected\') room.send({ type: \'hand?\' });\nroom.onConnection((state) => {\n if (state === \'connected\') room.send({ type: \'hand?\' });\n});\n```\n\nThe server answers from `onMessage` with `room.send(player, { type: \'hand\', cards })`. The three-second delay of `c.room.watch` does not protect secrets. It only stops a player from watching an opponent\'s live screen in another tab.\n\nThe browser can change its role or team only while the room is in `lobby`. During a match, the server decides when a player changes role or team with `room.setRole` and `room.setTeam`. Both methods accept a player object or id and immediately update `room.players` for every client.\n\n```js\nonMessage(room, player, message) {\n if (message?.swap === \'captain\') {\n room.setRole(player, \'captain\');\n }\n},\n```\n\n`room.daily.seed` is fixed at room creation and shared by rooms created for that game on the same UTC day. `room.seed` is fixed for one room and is identical on the server and clients, so rooms created on the same day can generate different maps.\n\n### WebAssembly on the server\n\nUse WebAssembly only to bring an existing engine, such as physics, pathfinding or a Rust simulation. For new game logic, start with JavaScript.\n\n```js\nimport { defineGame } from \'@caisual/kit/server\';\nimport engine from \'./physics/add.wasm\';\n\nconst engines = new WeakMap();\nfunction instance(room) {\n if (!engines.has(room)) engines.set(room, new WebAssembly.Instance(engine, {}));\n return engines.get(room);\n}\n\nexport default defineGame({\n tickRate: 0,\n onCreate(room) {\n room.state = { sum: instance(room).exports.add(19, 23) };\n },\n onMessage(room) {\n room.state.sum = instance(room).exports.add(room.state.sum, 1);\n },\n});\n```\n\nA default import such as `import engine from \'./physics/add.wasm\'` returns an already compiled `WebAssembly.Module`. Instantiate it in `onCreate` or on first use with `new WebAssembly.Instance(engine, imports)`. Paths must start with `./`, stay inside the game folder and contain no `..` segments. Named, namespace and dynamic imports of `.wasm` are not supported. Shared memory and threads are not supported.\n\nThe room server accepts at most **8 `.wasm` files, 8,000,000 bytes per file and 16,000,000 bytes in total**, in addition to the 4,000,000-byte `server.js` limit. The CLI discovers them from the bundle and uploads them privately alongside the server, checking size and SHA-256. There is no manifest change; `requires.wasm` describes the browser client only. `caisual check` and publish enforce these limits, and `caisual dev` compiles the same files locally. Restart dev after changing a binary.\n\nBudget for compilation when the room wakes: a large binary makes resumption slower. The platform may reuse compiled code for the same game version, but reuse is not guaranteed. Instances and their memory are temporary, so recreate an instance on first use after a wake and restore any engine state from room JSON state or room saves. `onCreate` does not run again after a wake. Never put a module, instance or binary memory in `room.state`.\n\n\n\n### Shared game store\n\n`room.shared` is a server-only JSON key/value store shared by every room of the same game. It is useful when one room must leave data for another room, while `room.save` remains private to one room.\n\nThe following server leaves a ship when a room ends, then loads every previously left ship when another room is created. The room id suffix is used because shared-store keys follow the save-key format.\n\n```js\nexport default defineGame({\n tickRate: 0,\n\n async onCreate(room) {\n const keys = await room.shared.list(\'ship_\');\n room.state = {\n ships: await Promise.all(keys.map((key) => room.shared.get(key))),\n };\n },\n\n async onEnd(room) {\n const roomSuffix = room.id.split(\'.\')[1];\n await room.shared.set(\'ship_\' + roomSuffix, {\n position: room.state.position,\n cargo: room.state.cargo,\n });\n },\n});\n```\n\nThe five methods are asynchronous:\n\n```js\nconst value = await room.shared.get(key); // JSON value, or null\nawait room.shared.set(key, value); // last writer wins\nawait room.shared.delete(key);\nconst keys = await room.shared.list(prefix); // sorted, up to 4096\nconst total = await room.shared.increment(key, 1); // atomic, defaults to 1\n```\n\nKeys contain 1 to 32 lowercase letters, numbers, underscores, or hyphens. Values may be up to 256 KB when serialized, and each game may keep up to 4096 keys. Each room may perform up to 120 shared-store operations per minute. `increment` treats a missing key as zero and rejects unless the existing value, amount, and result are safe integers.\n\nUse the store in `onCreate`, `onStart`, `onEnd`, `onMessage`, or a `schedule` handler. Do not call it on every tick: each call waits for a remote operation, and the CPU budget uses elapsed wall-clock time. Browser clients cannot access this store. Send only the data they need with `room.broadcast` or `room.send`.\n\nFailures reject with an `Error` carrying `store_invalid_key`, `store_too_large`, `store_full`, `store_not_integer`, `store_unavailable`, or `store_rate_limited` in `code`.\n\n`room.voice.setGain(listener, speaker, gain)` controls how much one listener hears one speaker. It is directional, limited to the range from 0 to 1, and rounded to two decimal places. For example, the following setup lets the captain hear everyone while each crew member hears only the captain:\n\n```js\nconst captain = room.players.find((player) => player.role === \'captain\');\nconst crew = room.players.filter((player) => player.id !== captain.id);\n\nfor (const speaker of room.players) {\n room.voice.setGain(captain, speaker, 1);\n}\nfor (const listener of crew) {\n for (const speaker of room.players) {\n room.voice.setGain(listener, speaker, speaker.id === captain.id ? 1 : 0);\n }\n}\n```\n\n`room.voice.setProximity(a, b, gain)` is the symmetric shortcut for setting both directions. Both methods work in `room`, `team`, and `proximity` modes, and do nothing in `none`. In `team` mode, gains remain inside the team and cannot make a player hear another team.\n\nFor position-based audio, update the symmetric gain between players from server-owned positions:\n\n```js\nexport default defineGame({\n tickRate: 20,\n onTick(room) {\n for (const a of room.players) {\n for (const b of room.players) {\n if (a.id >= b.id) continue;\n const pa = room.state.positions[a.id];\n const pb = room.state.positions[b.id];\n const distance = Math.hypot(pa.x - pb.x, pa.y - pb.y);\n room.voice.setProximity(a, b, Math.max(0, 1 - distance / 20));\n }\n }\n },\n});\n```\n\n### Sleeping and cost\n\nPrefer `tickRate: 0` for turn based and party games. A room with a tick loop sleeps automatically after 30 seconds without player input or state changes and wakes on the next game message or player joining. Automatic ping and resync messages do not count as player input. A match with no player input for 10 minutes ends with `{ error: \'idle\' }`. Timers set with `schedule` and the countdown keep working while the room sleeps.\n\n### CPU budget\n\nEvery `onTick` and `onMessage` call is measured. Twenty consecutive calls above 100 ms end the room with `{ error: \'cpu_budget\' }`. If the average over 50 ticks is above 20 ms, the effective `tickRate` is halved, down to a minimum of 5, and clients receive an `error` message with code `tick_rate_reduced`. The optional `tickRate` field in `state` and `snapshot` protocol messages updates client `room.tickRate`; older clients ignore the added field. A frequency change sends a state diff even when its patch is empty.\n\n`room.tickRate` starts at the definition\'s `tickRate` and always reports the current effective frequency. `deltaSeconds` follows that frequency, so a fixed-step simulation must accumulate `deltaSeconds` instead of counting ticks. Measurement uses elapsed wall-clock time, so a slow `await` inside a callback also counts. `room.result` is `null` during a match, contains its result in `finished` or `ended` and inside `onEnd`, and returns to `null` before `onRestart`.\n\n### Persistent rooms\n\nSet `"persistent": true` in `caisual.json` for a room that must survive long breaks. It does not use the normal inactivity ending rule and does not end when every player disconnects. Players remain members until they call `room.leave()` or the server removes them with `room.kick()`. They can use the same room code to return while the game is already playing. The code remains valid while the room lives, and absent members remain in `room.players` with `connected: false`.\n\nA persistent room ends when the server calls `room.end(result)` without rematch, when its two-minute rematch wait expires, after 30 days without player input, entry, or a state change with `{ error: \'expired\' }`, or after five minutes without any members. The standard overlay stores the resume reference itself. Only games without it need to store the client `room.code` with `c.save.set()` and offer Resume. A persistent room incurs cost only while it is awake.\n\n## Limits\n\n- 120 requests per minute per player. Beyond that the kit rejects with `rate_limited`; wait and retry.\n- Saves: 64 keys per player per game, 256 KB per value.\n- Scores: safe integers from 0 upward.\n- Room state: 512 KB of plain JSON.\n- Game messages: 64 KB each and 30/s per connection; excess messages are dropped with at most one `rate_limited` error per second. Service messages have a separate 30/s budget. More than 150 attempts/s in either budget for three consecutive one-second windows closes with 4008. Oversized frames close with 4009 `message_too_large`.\n- Spectators: 100 per room, with a configured delay from 0 to 30 seconds.\n- Voice supports audio only and one voice channel per room.\n- Voice control messages: 64 KB each and 30 operations per 10 seconds per connection. Voice signaling also uses the separate service-message budget; audio traffic does not consume either message budget.\n- Room save values: 256 KB each.\n- Shared game store: 256 KB per JSON value, 4096 keys per game, and 120 operations per minute per room.\n\n## Development\n\nRun `npx @caisual/cli dev` from the game folder, then open the printed portal URL. The preview supplies the normal handshake, so `c.connected` is `true` and the game receives a local guest identity. It also mounts the same standard overlay as the portal. `?lang=` chooses the game preference, resolved against the manifest; `c.player.uiLanguage` follows the overlay fallback. For example, `?lang=ja` gives `c.player.language === "ja"` when declared, with the overlay in English. Friends and parties are marked unavailable locally; everything else, including saves, leaderboards, daily data, invitations, and rooms, works on local data. Opening the portal URL in more browser tabs creates more local players, which makes multiplayer testing possible without publishing.\n\nUse `npx @caisual/cli dev --day 2026-09-04` to pin the UTC day used by client and room daily seeds and local daily leaderboards. The flag accepts only a real date in `YYYY-MM-DD` format; without it, dev uses today\'s UTC date. Real clocks and room timers keep running normally. Scores remain in `.caisual-dev/scores.json` under their assigned day: restarting with another `--day` selects that day\'s board, and returning to a previous day restores its scores. Saves, identities and rooms are shared across these dates; queued room scores keep their assigned day when flushed after a restart. Existing rooms retain their creation context; new rooms follow the selected day. `expiresAt` follows the real clock even with `--day`. Use `c.board.top(\'main\', { day: \'2026-09-04\', guests: true })` to inspect a specific local day. A changed flag takes effect after restarting dev and reloading the game.\n\nOpen the printed `/__caisual/players?n=4` URL for 1 to 8 independent guest frames, each including the game and standard overlay. Use the overlay to create a room and join its code in the other frames. Each frame has **Drop** (1 to 60 seconds, default 3) and **Spectate**, which opens a new guest watching that frame\'s room. Phone frames are 390 x 844; the desktop preset is 960 x 640. Add `&lang=it` to test Italian.\n\n`dev --latency 120 --jitter 40 --loss 2` delays room WebSocket messages in both directions for players and spectators. Latency and jitter are integer milliseconds from 0 to 60000; loss is a percentage from 0 to 100 and may be fractional. Jitter and loss require `--latency`, which may be zero. Jitter varies delay uniformly within plus/minus the supplied value, clamped at zero, and preserves message order. Loss discards whole application messages, including protocol messages, to exercise recovery; it is not a model of TCP packet retransmission. HTTP, matchmaking and audio are unaffected. Drop closes the guest\'s room sockets and prevents successful reconnects for the selected duration; the kit\'s retry schedule can make the return later. The spectator stream also retains its configured game delay.\n\nClient files are read on each request: reload the portal or frame after editing, or rebuild into `client/` first if using a bundler. There is no automatic browser reload. Restart dev after changing `caisual.json`, `server.js`, or any server import (including shared client physics). Rooms restore from `.caisual-dev/`; changing the shape of their state may require creating a fresh room. A busy port error suggests a command with a currently available port.\n\nEvery `init` variant generates Node test and optional Playwright browser scripts. Playwright is a development dependency of the generated game, never of the CLI. See [Local dev](/docs/local-development#testing-with-a-browser) for installation and the full browser workflow.\n\nWhen building the client with a bundler, remember that `client/` is served as-is. Configure Vite, esbuild, or another bundler to write into that folder, for example `vite build --outDir client`, and use relative paths such as `base: \'./\'`.\n\nKeep loading the kit from the `<script type="module">` shown at the beginning of this guide, using `/__caisual/kit/v1.js`, when publishing on Caisual. That URL exists only in `caisual dev` and in the published game.\n\nIf the game has `server.js`, room state is handled locally and stored under `.caisual-dev/` in the game folder. If it has no `server.js`, room creation rejects with `no_server` and the single-player APIs still work.\n\nTo run from any other static server, install `@caisual/kit` from npm and import it with a bundler as `import { caisual } from \'@caisual/kit\'`. In that build standalone mode applies: `c.connected` is `false`, saves use local storage, `submit` returns `accepted: false`, leaderboards are empty, the daily seed is local, and room creation and joining reject with `offline`. The rest of the game logic does not need a different code path.\n\nAfter publishing with `npx @caisual/cli publish`, open the game from its caisual.com page: `c.connected` becomes `true` and every call goes to the portal.\n\n## Manifest\n\nDeclare `"overlay": { "version": 1 }` to get the standard overlay, with an optional `accent` colour. A standard game must declare at least one mode, and every mode needs `execution`, either `local` for a single-player run of exactly one player or `room` for a room backed by `server.js`. `label` names the mode in the standard menu and `instructions` adds one line under it. `roles[].label` and `boards[<id>].label` name roles and boards in the same UI, and `boards[<id>].periods` lists `daily`, `all-time` or both. A mode with matchmaking adds `matchmaking.defaults`, one value for every field of its `key`, so the overlay can start a search on its own.\n\n```json\n{\n "overlay": { "version": 1, "accent": "#397e83" },\n "players": { "min": 2, "max": 4 },\n "lobby": true,\n "boards": { "solo": { "source": "server", "label": "Best run", "periods": ["daily", "all-time"] } },\n "modes": [\n { "id": "practice", "execution": "local", "label": "Practice",\n "instructions": "One run against the clock.",\n "players": { "min": 1, "max": 1 }, "lobby": false },\n { "id": "duel", "execution": "room", "label": "Online",\n "matchmaking": { "key": ["pool"], "defaults": { "pool": "v1" }, "timeoutMs": 12000 } }\n ]\n}\n```\n\nA game without `overlay` keeps its historical flow and draws its own menus. Nothing else changes for it.\n\nNo manifest field is required for identity, saves, leaderboards, or the daily challenge. Use `boards` to make selected leaderboards server-only. A mode may override only `players: { min, max }` and `lobby`; omitted fields inherit the root configuration, and `mode: null` uses the root values. Matchmaking thresholds and room admission use this same resolution. For rooms, set `players` to the supported range and use `lobby`, `persistent`, `spectators`, `roles`, `teams`, and `modes` to describe the setup and lifetime. Set `voice` to `room`, `team`, or `proximity` to enable the corresponding voice mode, or omit it for `none`. A single-player game can keep `players` at `{ "min": 1, "max": 1 }`, `lobby` at `false`, and omit `server.js`. See [publish.md](./publish.md#caisualjson) for every field and the publishing steps.\n\n\n## Required game images\n\nEnglish (`en`) is required in `languages`; three distinct files inside `client/` with no text inside are also required: cover 1536x1024 (3:2), card 1024x1024 and icon 1024x1024, each PNG, JPEG or WebP and at most 2 MB. See [Manifest](https://caisual.com/docs/manifest#required-game-images).\n';
1057
1241
 
1058
1242
  // src/dev.ts
1059
- import { createHash as createHash2, createHmac, randomBytes, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
1243
+ import { createHash as createHash3, createHmac, randomBytes, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
1060
1244
  import { promises as fs3 } from "node:fs";
1061
1245
  import { createServer } from "node:http";
1062
- import { basename, dirname as dirname2, extname, join as join3, relative as relative2, resolve, sep as sep2 } from "node:path";
1246
+ import { basename, dirname as dirname2, extname, join as join3, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
1063
1247
 
1064
1248
  // ../kit/dist/node.js
1065
1249
  import { randomUUID } from "node:crypto";
1066
1250
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
1067
1251
  import { dirname } from "node:path";
1068
1252
  import { performance } from "node:perf_hooks";
1069
- import { createHash } from "node:crypto";
1253
+ import { createHash as createHash2 } from "node:crypto";
1070
1254
  import { EventEmitter } from "node:events";
1071
1255
  var NOMI_RISERVATI2 = [
1072
1256
  "www",
@@ -1111,13 +1295,18 @@ function modalitaLocale2(manifest, mode) {
1111
1295
  }
1112
1296
  var MASSIMO_SPETTATORI = 100;
1113
1297
  var RITARDO_SPETTATORI_MS2 = 3e3;
1298
+ var REPLAY_MAX_BYTES2 = 10 * 1024 * 1024;
1299
+ var REPLAY_MAX_DURATION_MS2 = 30 * 60 * 1e3;
1300
+ var REPLAY_CHUNK_BYTES2 = 512 * 1024;
1301
+ var REPLAY_RETENTION_MS2 = 30 * 24 * 60 * 60 * 1e3;
1114
1302
  function validBoardDay2(value) {
1115
1303
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
1116
1304
  const at = Date.parse(`${value}T00:00:00Z`);
1117
1305
  return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;
1118
1306
  }
1119
- var MESSAGGI_GIOCO_AL_SECONDO = 20;
1120
- var MESSAGGI_SERVIZIO_AL_SECONDO = 20;
1307
+ var MASSIMO_BYTE_FRAME_STANZA2 = 64 * 1024;
1308
+ var MESSAGGI_GIOCO_AL_SECONDO = 30;
1309
+ var MESSAGGI_SERVIZIO_AL_SECONDO = 30;
1121
1310
  var LimiteMessaggiStanza = class {
1122
1311
  connessioni = /* @__PURE__ */ new Map();
1123
1312
  delete(connessione) {
@@ -1386,10 +1575,10 @@ function seedStanza(roomId) {
1386
1575
  }
1387
1576
  var CHIAVE_NUCLEO = "nucleo";
1388
1577
  var PREFISSO_SAVE = "save:";
1389
- var LIMITE_FRAME = 16 * 1024;
1390
- var LIMITE_STATO = 256 * 1024;
1391
- var LIMITE_SAVE = 128 * 1024;
1392
- var LIMITE_DEPOSITO = 64 * 1024;
1578
+ var LIMITE_FRAME = MASSIMO_BYTE_FRAME_STANZA2;
1579
+ var LIMITE_STATO = 512 * 1024;
1580
+ var LIMITE_SAVE = 256 * 1024;
1581
+ var LIMITE_DEPOSITO = 256 * 1024;
1393
1582
  var LIMITE_OPERAZIONI_DEPOSITO = 120;
1394
1583
  var GRAZIA_MS = 6e4;
1395
1584
  var STANZA_VUOTA_MS = 5 * 6e4;
@@ -1399,6 +1588,10 @@ var GRAZIA_GIORNALIERA_MS = 10 * 6e4;
1399
1588
  var RIPOSO_TICK_MS = 3e4;
1400
1589
  var INATTIVITA_MS = 10 * 6e4;
1401
1590
  var SCADENZA_PERSISTENTE_MS = 30 * 24 * 60 * 6e4;
1591
+ var BLOCCO_INATTIVITA_MS = 3e4;
1592
+ function scadenzaInattivita(ultimoEvento, durata) {
1593
+ return Math.ceil((ultimoEvento + durata) / BLOCCO_INATTIVITA_MS) * BLOCCO_INATTIVITA_MS;
1594
+ }
1402
1595
  var CHIAVE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
1403
1596
  var PREFISSO_CHIAVE = /^[a-z0-9_-]{0,32}$/;
1404
1597
  var GAME_DEFINITION = /* @__PURE__ */ Symbol.for("@caisual/kit/game-definition");
@@ -1776,6 +1969,7 @@ var NucleoStanza = class _NucleoStanza {
1776
1969
  }
1777
1970
  if (primaConnessione) {
1778
1971
  await this.chiama(this.definizione.onStart, this.room);
1972
+ this.iniziaReplay();
1779
1973
  this.inviaStatus(ora);
1780
1974
  }
1781
1975
  await this.concludiEvento();
@@ -1995,6 +2189,7 @@ var NucleoStanza = class _NucleoStanza {
1995
2189
  this.dati.ultimoInputAt = ora;
1996
2190
  this.dati.countdownAt = null;
1997
2191
  await this.chiama(this.definizione.onStart, this.room);
2192
+ this.iniziaReplay();
1998
2193
  this.inviaStatus(ora);
1999
2194
  }
2000
2195
  }
@@ -2219,6 +2414,7 @@ var NucleoStanza = class _NucleoStanza {
2219
2414
  await this.applicaAzioni();
2220
2415
  if (dati.status === "playing") {
2221
2416
  await this.chiama(this.definizione.onStart, this.room);
2417
+ this.iniziaReplay();
2222
2418
  }
2223
2419
  await this.concludiEvento();
2224
2420
  if (dati.status === "lobby" || dati.status === "playing" || dati.status === "countdown") {
@@ -2372,7 +2568,7 @@ var NucleoStanza = class _NucleoStanza {
2372
2568
  const json = analizzaJson(value);
2373
2569
  if (!json.ok) throw new TypeError("Saved values must be valid JSON.");
2374
2570
  if (json.bytes > LIMITE_SAVE) {
2375
- throw new RangeError("Saved values must be at most 131072 bytes.");
2571
+ throw new RangeError("Saved values must be at most 262144 bytes.");
2376
2572
  }
2377
2573
  await this.adattatore.storage.put(PREFISSO_SAVE + key, json.valore);
2378
2574
  }
@@ -2443,7 +2639,7 @@ var NucleoStanza = class _NucleoStanza {
2443
2639
  if (!json.ok || json.bytes > LIMITE_DEPOSITO) {
2444
2640
  throw erroreConCodice(
2445
2641
  "store_too_large",
2446
- "Shared store values must be valid JSON of at most 65536 bytes."
2642
+ "Shared store values must be valid JSON of at most 262144 bytes."
2447
2643
  );
2448
2644
  }
2449
2645
  await this.usaDeposito((deposito) => deposito.set(key, json.valore));
@@ -2458,7 +2654,7 @@ var NucleoStanza = class _NucleoStanza {
2458
2654
  if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string" || !CHIAVE.test(key))) {
2459
2655
  throw erroreConCodice("store_unavailable", "The shared store returned invalid keys.");
2460
2656
  }
2461
- return [...keys].sort().slice(0, 1024);
2657
+ return [...keys].sort().slice(0, 4096);
2462
2658
  }
2463
2659
  async incrementaDeposito(key, amount) {
2464
2660
  this.verificaChiaveDeposito(key);
@@ -2471,18 +2667,24 @@ var NucleoStanza = class _NucleoStanza {
2471
2667
  }
2472
2668
  return value;
2473
2669
  }
2474
- broadcastCreatore(message) {
2670
+ messaggioCreatore(message) {
2475
2671
  const json = analizzaJson(message);
2476
2672
  if (!json.ok) throw new TypeError("Messages must be valid JSON.");
2477
- this.broadcast({ t: "msg", m: json.valore });
2673
+ const frame = { t: "msg", m: json.valore };
2674
+ if (new TextEncoder().encode(JSON.stringify(frame)).byteLength > LIMITE_FRAME) {
2675
+ throw new RangeError("Game message frames must be at most 65536 bytes.");
2676
+ }
2677
+ return frame;
2678
+ }
2679
+ broadcastCreatore(message) {
2680
+ this.broadcast(this.messaggioCreatore(message));
2478
2681
  }
2479
2682
  inviaCreatore(playerId, message) {
2480
- const json = analizzaJson(message);
2481
- if (!json.ok) throw new TypeError("Messages must be valid JSON.");
2683
+ const frame = this.messaggioCreatore(message);
2482
2684
  const player = this.richiediDati().giocatori.find((item) => item.id === playerId);
2483
2685
  if (player === void 0) throw new Error("Player not found.");
2484
2686
  if (player.connected && player.connessione !== null) {
2485
- this.adattatore.invia(player.connessione, { t: "msg", m: json.valore });
2687
+ this.adattatore.invia(player.connessione, frame);
2486
2688
  }
2487
2689
  }
2488
2690
  broadcast(message) {
@@ -2577,6 +2779,13 @@ var NucleoStanza = class _NucleoStanza {
2577
2779
  dati.statoSincronizzato = copiaJson(dati.state);
2578
2780
  dati.tickSincronizzato = dati.tick;
2579
2781
  }
2782
+ iniziaReplay() {
2783
+ if (!this.manifest.replays || !this.adattatore.inizioReplay) return;
2784
+ const json = analizzaJson(this.richiediDati().state);
2785
+ if (!json.ok || json.bytes > LIMITE_STATO) return;
2786
+ this.inviaSnapshotTutti();
2787
+ this.adattatore.inizioReplay(this.fotografia());
2788
+ }
2580
2789
  inviaSnapshotTutti() {
2581
2790
  const dati = this.richiediDati();
2582
2791
  const stato = analizzaJson(dati.state);
@@ -2833,10 +3042,13 @@ var NucleoStanza = class _NucleoStanza {
2833
3042
  if (this.dati.status === "finished") prossime.push(this.dati.rivincitaFinoA ?? this.adattatore.ora());
2834
3043
  if (this.manifest.persistent === true) {
2835
3044
  prossime.push(
2836
- Math.max(this.dati.ultimoInputAt, this.dati.ultimoCambioStatoAt) + SCADENZA_PERSISTENTE_MS
3045
+ scadenzaInattivita(
3046
+ Math.max(this.dati.ultimoInputAt, this.dati.ultimoCambioStatoAt),
3047
+ SCADENZA_PERSISTENTE_MS
3048
+ )
2837
3049
  );
2838
3050
  } else if (this.dati.status === "playing") {
2839
- prossime.push(this.dati.ultimoInputAt + INATTIVITA_MS);
3051
+ prossime.push(scadenzaInattivita(this.dati.ultimoInputAt, INATTIVITA_MS));
2840
3052
  }
2841
3053
  if (this.dati.countdownAt !== null) prossime.push(this.dati.countdownAt);
2842
3054
  for (const player of this.dati.giocatori) {
@@ -3025,7 +3237,7 @@ function acceptNodeWebSocket(request, socket, head = Buffer.alloc(0)) {
3025
3237
  if (request.method !== "GET" || request.headers.upgrade?.toLowerCase() !== "websocket" || !contieneToken(request.headers.connection, "upgrade") || request.headers["sec-websocket-version"] !== "13" || !chiaveValida(key)) {
3026
3238
  throw new TypeError("The WebSocket upgrade request is invalid.");
3027
3239
  }
3028
- const accept = createHash("sha1").update(key + GUID_WEBSOCKET).digest("base64");
3240
+ const accept = createHash2("sha1").update(key + GUID_WEBSOCKET).digest("base64");
3029
3241
  socket.write(
3030
3242
  `HTTP/1.1 101 Switching Protocols\r
3031
3243
  Upgrade: websocket\r
@@ -3388,7 +3600,7 @@ var StanzaNode = class {
3388
3600
  return;
3389
3601
  }
3390
3602
  const message = typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
3391
- if (Buffer.byteLength(frame, "utf8") > (message?.t === "voice" ? 64 : 16) * 1024) {
3603
+ if (Buffer.byteLength(frame, "utf8") > MASSIMO_BYTE_FRAME_STANZA2) {
3392
3604
  this.adattatore.chiudi(connessione, 4009, "message_too_large");
3393
3605
  this.rimuoviConnessione(connessione);
3394
3606
  await this.nucleo.disconnetti(connessione);
@@ -3435,7 +3647,7 @@ var StanzaNode = class {
3435
3647
  }));
3436
3648
  }
3437
3649
  async riceviSpettatore(connessione, frame) {
3438
- if (Buffer.byteLength(frame, "utf8") > 16 * 1024) {
3650
+ if (Buffer.byteLength(frame, "utf8") > MASSIMO_BYTE_FRAME_STANZA2) {
3439
3651
  this.adattatore.chiudiSpettatore(connessione, 4009, "message_too_large");
3440
3652
  this.frameFrequenza.delete(connessione);
3441
3653
  return;
@@ -3724,140 +3936,158 @@ var NOMI_RISERVATI3 = [
3724
3936
  "shipz"
3725
3937
  ];
3726
3938
  var RISERVATI3 = new Set(NOMI_RISERVATI3);
3939
+ var REPLAY_MAX_BYTES3 = 10 * 1024 * 1024;
3940
+ var REPLAY_MAX_DURATION_MS3 = 30 * 60 * 1e3;
3941
+ var REPLAY_CHUNK_BYTES3 = 512 * 1024;
3942
+ var REPLAY_RETENTION_MS3 = 30 * 24 * 60 * 60 * 1e3;
3943
+ var MASSIMO_BYTE_FRAME_STANZA3 = 64 * 1024;
3727
3944
  var words = {
3728
- gameUpdated: ["This game was updated", "Questo gioco \xE8 stato aggiornato", "Este juego se ha actualizado", "Ce jeu a \xE9t\xE9 mis \xE0 jour", "Dieses Spiel wurde aktualisiert", "Este jogo foi atualizado"],
3729
- reloadGame: ["Reload game", "Ricarica il gioco", "Recargar el juego", "Recharger le jeu", "Spiel neu laden", "Recarregar o jogo"],
3730
- gameLanguages: ["Game languages", "Lingue del gioco", "Idiomas del juego", "Langues du jeu", "Spielsprachen", "Idiomas do jogo"],
3731
- loading: ["Loading game...", "Caricamento...", "Cargando...", "Chargement...", "Spiel wird geladen...", "Carregando..."],
3732
- loadingSlow: ["This game is taking longer than expected. You can wait a little longer or try again.", "Il gioco ci sta mettendo pi\xF9 del previsto. Puoi aspettare ancora un po\u2019 o riprovare.", "El juego est\xE1 tardando m\xE1s de lo esperado. Puedes esperar un poco m\xE1s o volver a intentarlo.", "Le jeu met plus de temps que pr\xE9vu. Vous pouvez patienter encore un peu ou r\xE9essayer.", "Das Spiel braucht l\xE4nger als erwartet. Du kannst noch etwas warten oder es erneut versuchen.", "O jogo est\xE1 demorando mais do que o esperado. Voc\xEA pode esperar mais um pouco ou tentar novamente."],
3733
- home: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar"],
3734
- homeMenu: ["Menu", "Menu", "Men\xFA", "Menu", "Men\xFC", "Menu"],
3735
- mode: ["Mode", "Modalit\xE0", "Modo", "Mode", "Modus", "Modo"],
3736
- play: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar"],
3737
- friendsPlay: ["Play with friends", "Gioca con amici", "Jugar con amigos", "Jouer entre amis", "Mit Freunden spielen", "Jogar com amigos"],
3738
- find: ["Find players", "Trova giocatori", "Buscar jugadores", "Trouver des joueurs", "Spieler finden", "Buscar jogadores"],
3739
- join: ["Join with code", "Entra con codice", "Entrar con c\xF3digo", "Rejoindre avec un code", "Mit Code beitreten", "Entrar com c\xF3digo"],
3740
- joinInvite: ["Join this room", "Entra in questa stanza", "Entrar en est\xE1 sala", "Rejoindre cette salle", "Diesem Raum beitreten", "Entrar nest\xE1 sala"],
3741
- watch: ["Watch a room", "Guarda una stanza", "Observar una sala", "Regarder une salle", "Raum ansehen", "Assistir a uma sala"],
3742
- resume: ["Resume", "Riprendi", "Continuar", "Reprendre", "Fortsetzen", "Continuar"],
3743
- room: ["Room", "Stanza", "Sala", "Salle", "Raum", "Sala"],
3744
- code: ["Room code", "Codice stanza", "C\xF3digo de sala", "Code de salle", "Raumcode", "C\xF3digo da sala"],
3745
- copy: ["Copy invite", "Copia invito", "Copiar invitaci\xF3n", "Copier le lien", "Einladung kopieren", "Copiar convite"],
3746
- copied: ["Invite copied", "Invito copiato", "Invitacion copiada", "Lien copi\xE9", "Einladung kopiert", "Convite copiado"],
3747
- copyFailed: ["Copy this link:", "Copia questo link:", "Copia este enlace:", "Copiez ce lien :", "Diesen Link kopieren:", "Copie este link:"],
3748
- joining: ["Joining room...", "Ingresso nella stanza...", "Entrando en la sala...", "Connexion \xE0 la salle...", "Raum wird betreten...", "Entrando na sala..."],
3749
- matching: ["Finding your people...", "Ricerca giocatori...", "Buscando jugadores...", "Recherche de joueurs...", "Spieler werden gesucht...", "Buscando jogadores..."],
3750
- queue: ["{n} / {max} players", "{n} / {max} giocatori", "{n} / {max} jugadores", "{n} / {max} joueurs", "{n} / {max} Spieler", "{n} / {max} jogadores"],
3751
- cancel: ["Cancel", "Annulla", "Cancelar", "Annuler", "Abbrechen", "Cancelar"],
3752
- close: ["Close", "Chiudi", "Cerrar", "Fermer", "Schlie\xDFen", "Fechar"],
3753
- back: ["Back", "Indietro", "Volver", "Retour", "Zur\xFCck", "Voltar"],
3754
- ready: ["Ready", "Pronto", "Listo", "Pr\xEAt", "Bereit", "Pronto"],
3755
- unready: ["Not ready", "Non pronto", "No listo", "Pas pr\xEAt", "Nicht bereit", "N\xE3o pronto"],
3756
- start: ["Start", "Inizia", "Empezar", "Commencer", "Starten", "Come\xE7ar"],
3757
- role: ["Role", "Ruolo", "Rol", "R\xF4le", "Rolle", "Fun\xE7\xE3o"],
3758
- team: ["Team", "Squadra", "Equipo", "\xC9quipe", "Team", "Equipe"],
3759
- host: ["Host", "Host", "Anfitrion", "H\xF4te", "Host", "Anfitri\xE3o"],
3760
- you: ["You", "Tu", "T\xFA", "Vous", "Du", "Voc\xEA"],
3761
- away: ["Away", "Assente", "Ausente", "Absent", "Abwesend", "Ausente"],
3762
- needPlayers: ["Waiting for more players", "In attesa di giocatori", "Esperando m\xE1s jugadores", "En attente de joueurs", "Weitere Spieler fehlen", "Esperando mais jogadores"],
3763
- needReady: ["Everyone needs to be ready", "Tutti devono essere pronti", "Todos deben estar listos", "Tout le monde doit \xEAtre pr\xEAt", "Alle m\xFCssen bereit sein", "Todos precisam estar prontos"],
3764
- needRoles: ["Fill the required roles", "Completa i ruoli richiesti", "Completa los roles", "Compl\xE9tez les r\xF4les", "Ben\xF6tigte Rollen besetzen", "Complete as fun\xE7\xF5es"],
3765
- needTeams: ["Choose the required teams", "Scegli le squadre richieste", "Elige los equipos", "Choisissez les \xE9quipes", "Teams auswahlen", "Escolha as equipes"],
3766
- waitHost: ["Waiting for the host", "In attesa dell'host", "Esperando al anfitrion", "En attente de l\u2019h\xF4te", "Warten auf den Host", "Esperando o anfitri\xE3o"],
3767
- starting: ["Starting in", "Si inizia tra", "Empieza en", "D\xE9but dans", "Start in", "Come\xE7a em"],
3768
- playing: ["Playing", "In partita", "Jugando", "En jeu", "Im Spiel", "Jogando"],
3769
- ended: ["Game finished", "Partita conclusa", "Partida terminada", "Partie termin\xE9e", "Spiel beendet", "Partida encerrada"],
3770
- rematchReady: ["{n}/{max} ready", "{n}/{max} pronti", "{n}/{max} listos", "{n}/{max} pr\xEAts", "{n}/{max} bereit", "{n}/{max} prontos"],
3771
- rematchStart: ["Start rematch", "Avvia rivincita", "Iniciar revancha", "Lancer la revanche", "Revanche starten", "Iniciar revanche"],
3772
- won: ["You won", "Hai vinto", "Has ganado", "Vous avez gagn\xE9", "Du hast gewonnen", "Voc\xEA venceu"],
3773
- lost: ["You lost", "Hai perso", "Has perdido", "Vous avez perdu", "Du hast verloren", "Voc\xEA perdeu"],
3774
- draw: ["Draw", "Pareggio", "Empate", "\xC9galit\xE9", "Unentschieden", "Empate"],
3775
- standings: ["Standings", "Piazzamenti", "Posiciones", "R\xE9sultats", "Platzierungen", "Coloca\xE7\xF5es"],
3776
- points: ["points", "punti", "puntos", "points", "Punkte", "pontos"],
3777
- time: ["time", "tempo", "tiempo", "temps", "Zeit", "tempo"],
3778
- distance: ["distance", "distanza", "distancia", "distance", "Distanz", "dist\xE2ncia"],
3779
- again: ["Play again", "Gioca ancora", "Jugar de nuevo", "Rejouer", "Erneut spielen", "Jogar novamente"],
3780
- newRoom: ["New room. Share the new invite.", "Nuova stanza. Condividi il nuovo invito.", "Nueva sala. Comparte la invitaci\xF3n.", "Nouvelle salle. Partagez le lien.", "Neuer Raum. Neue Einladung teilen.", "Nova sala. Compartilhe o convite."],
3781
- watching: ["Watching", "In osservazione", "Observando", "Spectateur", "Zuschauen", "Assistindo"],
3782
- delay: ["{n}s delay", "Ritardo {n}s", "Retraso de {n}s", "Retard de {n}s", "{n}s Verz\xF6gerung", "Atraso de {n}s"],
3783
- exit: ["Exit", "Esci", "Salir", "Quitter", "Verlassen", "Sair"],
3784
- leaveNow: ["Leave for now", "Esci per ora", "Salir por ahora", "Quitter pour le moment", "Vor\xFCbergehend verlassen", "Sair por enquanto"],
3785
- leaveRoom: ["Leave room", "Lascia la stanza", "Abandonar sala", "Abandonner la salle", "Raum verlassen", "Deixar a sala"],
3786
- leaveHint: ["Your room stays available for Resume.", "La stanza resta disponibile con Riprendi.", "Podr\xE1s volver a est\xE1 sala.", "Vous pourrez reprendre cette salle.", "Du kannst den Raum fortsetzen.", "Voc\xEA pode voltar a est\xE1 sala."],
3787
- temporaryHint: ["The game continues. Rejoining may only be possible briefly.", "La partita continua. Il rientro pu\xF2 essere disponibile solo per poco.", "La partida continua. Volver puede ser posible solo por poco tiempo.", "La partie continue. Le retour peut \xEAtre limit\xE9.", "Das Spiel l\xE4uft weiter. R\xFCckkehr nur kurz m\xF6glich.", "A partida continua. O retorno pode ser limitado."],
3788
- abandonHint: ["Leave room gives up your place.", "Lascia la stanza libera il tuo posto.", "Abandonar libera tu plaza.", "Abandonner lib\xE8re votre place.", "Raum verlassen gibt deinen Platz frei.", "Deixar a sala libera sua vaga."],
3789
- reconnecting: ["Reconnecting...", "Riconnessione...", "Reconectando...", "Reconnexion...", "Verbindung wird erneuert...", "Reconectando..."],
3790
- replaced: ["Opened in another tab", "Aperta in un\u2019altra scheda", "Abierta en otra pest\xE1na", "Ouverte dans un autre onglet", "In anderem Tab ge\xF6ffnet", "Aberta em outra aba"],
3791
- error: ["Something went wrong. Try again.", "Qualcosa non va. Riprova.", "Algo sali\xF3 mal. Reintenta.", "Une erreur est survenue. R\xE9essayez.", "Etwas ist schiefgelaufen. Erneut versuchen.", "Algo deu errado. Tente novamente."],
3792
- noRoom: ["This room is no longer available.", "Questa stanza non \xE8 pi\xF9 disponibile.", "Esta sala ya no est\xE1 disponible.", "Cette salle n'est plus disponible.", "Dieser Raum ist nicht mehr verf\xFCgbar.", "Esta sala n\xE3o est\xE1 mais disponivel."],
3793
- full: ["This room is full.", "La stanza \xE8 piena.", "La sala est\xE1 llena.", "Cette salle est pleine.", "Dieser Raum ist voll.", "Esta sala est\xE1 cheia."],
3794
- noMatch: ["No match this time. Try again.", "Nessun gruppo trovato. Riprova.", "No hay grupo. Reintenta.", "Aucun groupe trouv\xE9. R\xE9essayez.", "Keine Gruppe gefunden. Erneut versuchen.", "Nenhum grupo encontrado. Tente novamente."],
3795
- invalidCode: ["Enter a six-character room code.", "Inserisci un codice di sei caratteri.", "Escribe un c\xF3digo de seis caracteres.", "Entrez un code de six caracteres.", "Sechsstelligen Raumcode eingeben.", "Digite um c\xF3digo de seis caracteres."],
3796
- refused: ["The room did not accept that change.", "La stanza ha rifiutato la modifica.", "La sala rechaz\xF3 el cambio.", "La salle a refus\xE9 ce changement.", "Der Raum hat die \xC4nderung abgelehnt.", "A sala recusou a altera\xE7\xE3o."],
3797
- unavailable: ["Unavailable right now", "Non disponibile ora", "No disponible ahora", "Indisponible pour le moment", "Derzeit nicht verf\xFCgbar", "Indisponivel agora"],
3798
- offline: ["Connection unavailable. Try again.", "Connessione non disponibile. Riprova.", "Sin conexi\xF3n. Reintenta.", "Connexion indisponible. R\xE9essayez.", "Keine Verbindung. Erneut versuchen.", "Sem conex\xE3o. Tente novamente."],
3799
- saveFailed: ["Keep the room code. Resume could not be saved.", "Conserva il codice. Riprendi non \xE8 stato salvato.", "Guarda el c\xF3digo. No se pudo guardar el regreso.", "Gardez le code. La reprise ne peut pas \xEAtre enregistr\xE9e.", "Raumcode aufbewahren. Fortsetzen nicht gespeichert.", "Guarde o c\xF3digo. O retorno n\xE3o foi salvo."],
3800
- boards: ["Leaderboard", "Classifica", "Clasificaci\xF3n", "Classement", "Bestenliste", "Classifica\xE7\xE3o"],
3801
- board: ["Board", "Classifica", "Tabla", "Classement", "Bestenliste", "Tabela"],
3802
- daily: ["Daily", "Giornaliera", "Diaria", "Du jour", "T\xE4glich", "Di\xE1ria"],
3803
- allTime: ["All time", "Di sempre", "Hist\xF3rica", "Tous les temps", "Gesamt", "Geral"],
3804
- accounts: ["Accounts", "Account", "Cuentas", "Comptes", "Konten", "Contas"],
3805
- guests: ["Guests", "Ospiti", "Invitados", "Invit\xE9s", "G\xE4ste", "Visitantes"],
3806
- category: ["Category", "Categoria", "Categoria", "Cat\xE9gorie", "Kategorie", "Categoria"],
3807
- period: ["Period", "Periodo", "Per\xEDodo", "P\xE9riode", "Zeitraum", "Per\xEDodo"],
3808
- rank: ["Rank", "Posizione", "Puesto", "Rang", "Platz", "Posicao"],
3809
- score: ["Score", "Punteggio", "Puntos", "Score", "Punkte", "Pontos"],
3810
- verified: ["Verified", "Verificato", "Verificado", "V\xE9rifi\xE9", "Verifiziert", "Verificado"],
3811
- own: ["Your best", "Il tuo record", "Tu record", "Votre record", "Dein Rekord", "Seu recorde"],
3812
- empty: ["No scores yet", "Nessun punteggio", "A\xFAn no hay puntos", "Aucun score", "Noch keine Punkte", "Ainda sem pontos"],
3813
- saving: ["Saving score...", "Salvataggio punteggio...", "Guardando puntos...", "Enregistrement du score...", "Punkte werden gespeichert...", "Salvando pontos..."],
3814
- saved: ["Your best is on the board", "Il tuo record \xE8 in classifica", "Tu record est\xE1 en la tabla", "Votre record est au classement", "Dein Rekord ist eingetragen", "Seu recorde est\xE1 na tabela"],
3815
- bestAlready: ["Your best is already on the board", "Il tuo record era gi\xE0 in classifica", "Tu record ya estaba en la tabla", "Votre record est d\xE9j\xE0 au classement", "Dein Rekord ist bereits eingetragen", "Seu recorde j\xE1 est\xE1 na tabela"],
3816
- refresh: ["Refresh", "Aggiorna", "Actualizar", "Actualiser", "Aktualisieren", "Atualizar"],
3817
- refreshHint: ["Score not visible yet. Refresh to check.", "Punteggio non ancora visibile. Aggiorna per controllare.", "Puntos a\xFAn no visibles. Actualiza.", "Score pas encore visible. Actualisez.", "Punkte noch nicht sichtbar. Aktualisieren.", "Pontos ainda n\xE3o visiveis. Atualize."],
3818
- friends: ["Friends & party", "Amici e gruppo", "Amigos y grupo", "Amis et groupe", "Freunde & Gruppe", "Amigos e grupo"],
3819
- localCrew: ["Friends and party are unavailable in local preview.", "Amici e gruppo non disponibili in anteprima locale.", "Amigos y grupo no disponibles en la vista local.", "Amis et groupe indisponibles en aper\xE7u local.", "Freunde und Gruppe in lokaler Vorschau nicht verf\xFCgbar.", "Amigos e grupo indispon\xEDveis na pr\xE9via local."],
3820
- loginCrew: ["Sign in on Caisual to use friends and party.", "Accedi a Caisual per amici e gruppo.", "Inicia sesion para amigos y grupo.", "Connectez-vous pour utiliser amis et groupe.", "F\xFCr Freunde und Gruppe bei Caisual anmelden.", "Entre no Caisual para amigos e grupo."],
3821
- online: ["Online", "Online", "En linea", "En ligne", "Online", "Online"],
3822
- noFriends: ["No friends online", "Nessun amico online", "Sin amigos en linea", "Aucun ami en ligne", "Keine Freunde online", "Nenhum amigo online"],
3823
- createParty: ["Create party", "Crea gruppo", "Crear grupo", "Cr\xE9er un groupe", "Gruppe erstellen", "Criar grupo"],
3824
- inviteParty: ["Invite to party", "Invita nel gruppo", "Invitar al grupo", "Inviter au groupe", "In Gruppe einladen", "Convidar para o grupo"],
3825
- leaveParty: ["Leave party", "Lascia gruppo", "Salir del grupo", "Quitter le groupe", "Gruppe verlassen", "Sair do grupo"],
3826
- accept: ["Accept", "Accetta", "Aceptar", "Accepter", "Annehmen", "Aceitar"],
3827
- decline: ["Decline", "Rifiuta", "Rechazar", "Refuser", "Ablehnen", "Recusar"],
3828
- follow: ["Join them", "Raggiungi", "Unirse", "Rejoindre", "Beitreten", "Juntar-se"],
3829
- voice: ["Voice", "Voce", "Voz", "Voix", "Sprache", "Voz"],
3830
- voiceJoin: ["Join voice", "Entra in voce", "Unirse a voz", "Activer la voix", "Sprachchat beitreten", "Entrar na voz"],
3831
- voiceLeave: ["Leave voice", "Esci dalla voce", "Salir de voz", "Quitter la voix", "Sprachchat verlassen", "Sair da voz"],
3832
- voiceMute: ["Mute", "Disattiva microfono", "Silenciar", "Couper le micro", "Stummschalten", "Silenciar"],
3833
- voiceUnmute: ["Unmute", "Attiva microfono", "Activar micr\xF3fono", "Activer le micro", "Mikrofon aktivieren", "Ativar microfone"],
3834
- voiceOff: ["Voice off", "Voce disattivata", "Voz desactivada", "Voix d\xE9sactiv\xE9e", "Sprachchat aus", "Voz desativada"],
3835
- voiceJoining: ["Joining voice...", "Connessione voce...", "Conectando voz...", "Connexion vocale...", "Sprachchat verbindet...", "Conectando voz..."],
3836
- voiceOn: ["Voice connected", "Voce connessa", "Voz conectada", "Voix connect\xE9e", "Sprachchat verbunden", "Voz conectada"],
3837
- voiceMuted: ["Muted", "Microfono disattivato", "Silenciado", "Micro coup\xE9", "Stumm", "Silenciado"],
3838
- voiceMic: ["Mic on", "Microfono attivo", "Micr\xF3fono activo", "Micro actif", "Mikrofon an", "Microfone ativo"],
3839
- voiceListening: ["Listening only", "Solo ascolto", "Solo escucha", "\xC9coute seule", "Nur zuh\xF6ren", "Somente ouvindo"],
3840
- voiceSpeaking: ["Speaking", "Sta parlando", "Hablando", "Parle", "Spricht", "Falando"],
3841
- voicePeers: ["Voice participants", "Partecipanti in voce", "Participantes de voz", "Participants vocaux", "Sprachteilnehmer", "Participantes de voz"],
3842
- voiceEmpty: ["No one else in voice yet.", "Nessun altro in voce per ora.", "A\xFAn no hay nadie m\xE1s en voz.", "Personne d\u2019autre en voix pour le moment.", "Noch niemand im Sprachchat.", "Ningu\xE9m mais na voz ainda."],
3843
- voiceVolume: ["Volume for {name}", "Volume di {name}", "Volumen de {name}", "Volume de {name}", "Lautst\xE4rke f\xFCr {name}", "Volume de {name}"],
3844
- voiceUnavailable: ["Join a room with voice to use these controls.", "Entra in una stanza con voce per usare questi controlli.", "Entra en una sala con voz para usar estos controles.", "Rejoignez une salle vocale pour utiliser ces commandes.", "Diese Steuerung braucht einen Raum mit Sprachchat.", "Entre em uma sala com voz para usar estes controles."],
3845
- voiceWatch: ["Voice is unavailable while watching.", "La voce non e' disponibile in osservazione.", "La voz no est\xE1 disponible al observar.", "La voix est indisponible en observation.", "Beim Zuschauen ist kein Sprachchat verf\xFCgbar.", "A voz n\xE3o est\xE1 dispon\xEDvel ao assistir."],
3846
- voiceDenied: ["Microphone permission denied. Allow it in your browser, then try again.", "Permesso microfono negato. Consenti l'accesso nel browser e riprova.", "Permiso de micr\xF3fono denegado. Act\xEDvalo en el navegador e int\xE9ntalo de nuevo.", "Acc\xE8s au micro refus\xE9. Autorisez-le dans le navigateur, puis r\xE9essayez.", "Mikrofonzugriff verweigert. Im Browser erlauben und erneut versuchen.", "Permiss\xE3o do microfone negada. Permita no navegador e tente novamente."],
3847
- voiceUnsupported: ["Voice is not supported in this browser.", "Questo browser non supporta la voce.", "Este navegador no admite voz.", "Ce navigateur ne prend pas en charge la voix.", "Dieser Browser unterst\xFCtzt keinen Sprachchat.", "Este navegador n\xE3o oferece suporte a voz."],
3848
- voiceFailed: ["Voice could not connect. Try again.", "Connessione voce non riuscita. Riprova.", "No se pudo conectar la voz. Int\xE9ntalo de nuevo.", "Connexion vocale impossible. R\xE9essayez.", "Sprachverbindung fehlgeschlagen. Erneut versuchen.", "N\xE3o foi poss\xEDvel conectar a voz. Tente novamente."],
3849
- voicePeerGone: ["This participant has left voice.", "Questo partecipante e' uscito dalla voce.", "Este participante sali\xF3 de voz.", "Ce participant a quitt\xE9 la voix.", "Diese Person hat den Sprachchat verlassen.", "Este participante saiu da voz."],
3850
- shortcut: ["Shift+Tab shortcut", "Scorciatoia Shift+Tab", "Atajo Shift+Tab", "Raccourci Maj+Tab", "Umschalt+Tab-Kurzbefehl", "Atalho Shift+Tab"],
3851
- menu: ["Caisual menu", "Menu Caisual", "Menu Caisual", "Menu Caisual", "Caisual-Menu", "Menu Caisual"],
3852
- retry: ["Retry", "Riprova", "Reintentar", "R\xE9essayer", "Erneut versuchen", "Tentar novamente"]
3945
+ watchReplay: ["Watch replay", "Guarda replay", "Ver repetici\xF3n", "Voir le replay", "Wiederholung ansehen", "Assistir \xE0 repeti\xE7\xE3o", "\u30EA\u30D7\u30EC\u30A4\u3092\u898B\u308B"],
3946
+ copyReplay: ["Copy link", "Copia link", "Copiar enlace", "Copier le lien", "Link kopieren", "Copiar link", "\u30EA\u30F3\u30AF\u3092\u30B3\u30D4\u30FC"],
3947
+ replayCopied: ["Link copied", "Link copiato", "Enlace copiado", "Lien copi\xE9", "Link kopiert", "Link copiado", "\u30EA\u30F3\u30AF\u3092\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F"],
3948
+ replay: ["Replay", "Replay", "Repetici\xF3n", "Replay", "Wiederholung", "Repeti\xE7\xE3o", "\u30EA\u30D7\u30EC\u30A4"],
3949
+ replayPlay: ["Play", "Riproduci", "Reproducir", "Lire", "Abspielen", "Reproduzir", "\u518D\u751F"],
3950
+ replayPause: ["Pause", "Pausa", "Pausar", "Pause", "Pause", "Pausar", "\u4E00\u6642\u505C\u6B62"],
3951
+ replaySeek: ["Position", "Posizione", "Posici\xF3n", "Position", "Position", "Posi\xE7\xE3o", "\u518D\u751F\u4F4D\u7F6E"],
3952
+ replaySpeed: ["Speed", "Velocit\xE0", "Velocidad", "Vitesse", "Geschwindigkeit", "Velocidade", "\u518D\u751F\u901F\u5EA6"],
3953
+ replayTruncated: ["Partial recording", "Registrazione parziale", "Grabaci\xF3n parcial", "Enregistrement partiel", "Teilweise Aufzeichnung", "Grava\xE7\xE3o parcial", "\u4E00\u90E8\u306E\u307F\u306E\u9332\u753B"],
3954
+ gameUpdated: ["This game was updated", "Questo gioco \xE8 stato aggiornato", "Este juego se ha actualizado", "Ce jeu a \xE9t\xE9 mis \xE0 jour", "Dieses Spiel wurde aktualisiert", "Este jogo foi atualizado", "\u30B2\u30FC\u30E0\u304C\u66F4\u65B0\u3055\u308C\u307E\u3057\u305F"],
3955
+ reloadGame: ["Reload game", "Ricarica il gioco", "Recargar el juego", "Recharger le jeu", "Spiel neu laden", "Recarregar o jogo", "\u30B2\u30FC\u30E0\u3092\u518D\u8AAD\u307F\u8FBC\u307F"],
3956
+ gameLanguages: ["Game languages", "Lingue del gioco", "Idiomas del juego", "Langues du jeu", "Spielsprachen", "Idiomas do jogo", "\u30B2\u30FC\u30E0\u306E\u8A00\u8A9E"],
3957
+ loading: ["Loading game...", "Caricamento...", "Cargando...", "Chargement...", "Spiel wird geladen...", "Carregando...", "\u8AAD\u307F\u8FBC\u307F\u4E2D..."],
3958
+ loadingSlow: ["This game is taking longer than expected. You can wait a little longer or try again.", "Il gioco ci sta mettendo pi\xF9 del previsto. Puoi aspettare ancora un po\u2019 o riprovare.", "El juego est\xE1 tardando m\xE1s de lo esperado. Puedes esperar un poco m\xE1s o volver a intentarlo.", "Le jeu met plus de temps que pr\xE9vu. Vous pouvez patienter encore un peu ou r\xE9essayer.", "Das Spiel braucht l\xE4nger als erwartet. Du kannst noch etwas warten oder es erneut versuchen.", "O jogo est\xE1 demorando mais do que o esperado. Voc\xEA pode esperar mais um pouco ou tentar novamente.", "\u8AAD\u307F\u8FBC\u307F\u306B\u6642\u9593\u304C\u304B\u304B\u3063\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u5F85\u3064\u304B\u3001\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002"],
3959
+ home: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar", "\u30D7\u30EC\u30A4"],
3960
+ homeMenu: ["Menu", "Menu", "Men\xFA", "Menu", "Men\xFC", "Menu", "\u30E1\u30CB\u30E5\u30FC"],
3961
+ mode: ["Mode", "Modalit\xE0", "Modo", "Mode", "Modus", "Modo", "\u30E2\u30FC\u30C9"],
3962
+ singlePlayer: ["Single player", "Giocatore singolo", "Un jugador", "Un joueur", "Einzelspieler", "Um jogador", "\u30B7\u30F3\u30B0\u30EB\u30D7\u30EC\u30A4"],
3963
+ multiplayer: ["Multiplayer", "Multigiocatore", "Multijugador", "Multijoueur", "Mehrspieler", "Multijogador", "\u30DE\u30EB\u30C1\u30D7\u30EC\u30A4"],
3964
+ createRoom: ["Create a room", "Crea una stanza", "Crear una sala", "Cr\xE9er une salle", "Raum erstellen", "Criar uma sala", "\u30EB\u30FC\u30E0\u3092\u4F5C\u6210"],
3965
+ play: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar", "\u30D7\u30EC\u30A4"],
3966
+ friendsPlay: ["Play with friends", "Gioca con amici", "Jugar con amigos", "Jouer entre amis", "Mit Freunden spielen", "Jogar com amigos", "\u53CB\u9054\u3068\u30D7\u30EC\u30A4"],
3967
+ find: ["Find a match", "Trova una partita", "Buscar partida", "Trouver une partie", "Partie finden", "Encontrar partida", "\u5BFE\u6226\u3092\u63A2\u3059"],
3968
+ join: ["Join with code", "Entra con codice", "Entrar con c\xF3digo", "Rejoindre avec un code", "Mit Code beitreten", "Entrar com c\xF3digo", "\u30B3\u30FC\u30C9\u3067\u53C2\u52A0"],
3969
+ joinInvite: ["Join this room", "Entra in questa stanza", "Entrar en est\xE1 sala", "Rejoindre cette salle", "Diesem Raum beitreten", "Entrar nest\xE1 sala", "\u3053\u306E\u30EB\u30FC\u30E0\u306B\u53C2\u52A0"],
3970
+ watch: ["Watch a room", "Guarda una stanza", "Observar una sala", "Regarder une salle", "Raum ansehen", "Assistir a uma sala", "\u30EB\u30FC\u30E0\u3092\u89B3\u6226"],
3971
+ resume: ["Resume", "Riprendi", "Continuar", "Reprendre", "Fortsetzen", "Continuar", "\u518D\u958B"],
3972
+ room: ["Room", "Stanza", "Sala", "Salle", "Raum", "Sala", "\u30EB\u30FC\u30E0"],
3973
+ code: ["Room code", "Codice stanza", "C\xF3digo de sala", "Code de salle", "Raumcode", "C\xF3digo da sala", "\u30EB\u30FC\u30E0\u30B3\u30FC\u30C9"],
3974
+ copy: ["Copy invite", "Copia invito", "Copiar invitaci\xF3n", "Copier le lien", "Einladung kopieren", "Copiar convite", "\u62DB\u5F85\u3092\u30B3\u30D4\u30FC"],
3975
+ copied: ["Invite copied", "Invito copiato", "Invitacion copiada", "Lien copi\xE9", "Einladung kopiert", "Convite copiado", "\u62DB\u5F85\u3092\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F"],
3976
+ copyFailed: ["Copy this link:", "Copia questo link:", "Copia este enlace:", "Copiez ce lien :", "Diesen Link kopieren:", "Copie este link:", "\u3053\u306E\u30EA\u30F3\u30AF\u3092\u30B3\u30D4\u30FC\u3057\u3066\u304F\u3060\u3055\u3044\uFF1A"],
3977
+ joining: ["Joining room...", "Ingresso nella stanza...", "Entrando en la sala...", "Connexion \xE0 la salle...", "Raum wird betreten...", "Entrando na sala...", "\u30EB\u30FC\u30E0\u306B\u53C2\u52A0\u4E2D..."],
3978
+ matching: ["Finding your people...", "Ricerca giocatori...", "Buscando jugadores...", "Recherche de joueurs...", "Spieler werden gesucht...", "Buscando jogadores...", "\u30D7\u30EC\u30A4\u30E4\u30FC\u3092\u691C\u7D22\u4E2D..."],
3979
+ queue: ["{n} / {max} players", "{n} / {max} giocatori", "{n} / {max} jugadores", "{n} / {max} joueurs", "{n} / {max} Spieler", "{n} / {max} jogadores", "{n} / {max} \u4EBA"],
3980
+ cancel: ["Cancel", "Annulla", "Cancelar", "Annuler", "Abbrechen", "Cancelar", "\u30AD\u30E3\u30F3\u30BB\u30EB"],
3981
+ close: ["Close", "Chiudi", "Cerrar", "Fermer", "Schlie\xDFen", "Fechar", "\u9589\u3058\u308B"],
3982
+ back: ["Back", "Indietro", "Volver", "Retour", "Zur\xFCck", "Voltar", "\u623B\u308B"],
3983
+ ready: ["Ready", "Pronto", "Listo", "Pr\xEAt", "Bereit", "Pronto", "\u6E96\u5099\u5B8C\u4E86"],
3984
+ unready: ["Not ready", "Non pronto", "No listo", "Pas pr\xEAt", "Nicht bereit", "N\xE3o pronto", "\u6E96\u5099\u3092\u89E3\u9664"],
3985
+ start: ["Start", "Inizia", "Empezar", "Commencer", "Starten", "Come\xE7ar", "\u958B\u59CB"],
3986
+ role: ["Role", "Ruolo", "Rol", "R\xF4le", "Rolle", "Fun\xE7\xE3o", "\u5F79\u5272"],
3987
+ team: ["Team", "Squadra", "Equipo", "\xC9quipe", "Team", "Equipe", "\u30C1\u30FC\u30E0"],
3988
+ host: ["Host", "Host", "Anfitrion", "H\xF4te", "Host", "Anfitri\xE3o", "\u30DB\u30B9\u30C8"],
3989
+ you: ["You", "Tu", "T\xFA", "Vous", "Du", "Voc\xEA", "\u3042\u306A\u305F"],
3990
+ away: ["Away", "Assente", "Ausente", "Absent", "Abwesend", "Ausente", "\u96E2\u5E2D\u4E2D"],
3991
+ needPlayers: ["Waiting for more players", "In attesa di giocatori", "Esperando m\xE1s jugadores", "En attente de joueurs", "Weitere Spieler fehlen", "Esperando mais jogadores", "\u30D7\u30EC\u30A4\u30E4\u30FC\u3092\u5F85\u3063\u3066\u3044\u307E\u3059"],
3992
+ needReady: ["Everyone needs to be ready", "Tutti devono essere pronti", "Todos deben estar listos", "Tout le monde doit \xEAtre pr\xEAt", "Alle m\xFCssen bereit sein", "Todos precisam estar prontos", "\u5168\u54E1\u306E\u6E96\u5099\u5B8C\u4E86\u3092\u5F85\u3063\u3066\u3044\u307E\u3059"],
3993
+ needRoles: ["Fill the required roles", "Completa i ruoli richiesti", "Completa los roles", "Compl\xE9tez les r\xF4les", "Ben\xF6tigte Rollen besetzen", "Complete as fun\xE7\xF5es", "\u5FC5\u8981\u306A\u5F79\u5272\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044"],
3994
+ needTeams: ["Choose the required teams", "Scegli le squadre richieste", "Elige los equipos", "Choisissez les \xE9quipes", "Teams auswahlen", "Escolha as equipes", "\u5FC5\u8981\u306A\u30C1\u30FC\u30E0\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044"],
3995
+ waitHost: ["Waiting for the host", "In attesa dell'host", "Esperando al anfitrion", "En attente de l\u2019h\xF4te", "Warten auf den Host", "Esperando o anfitri\xE3o", "\u30DB\u30B9\u30C8\u3092\u5F85\u3063\u3066\u3044\u307E\u3059"],
3996
+ starting: ["Starting in", "Si inizia tra", "Empieza en", "D\xE9but dans", "Start in", "Come\xE7a em", "\u958B\u59CB\u307E\u3067"],
3997
+ playing: ["Playing", "In partita", "Jugando", "En jeu", "Im Spiel", "Jogando", "\u30D7\u30EC\u30A4\u4E2D"],
3998
+ ended: ["Game finished", "Partita conclusa", "Partida terminada", "Partie termin\xE9e", "Spiel beendet", "Partida encerrada", "\u8A66\u5408\u7D42\u4E86"],
3999
+ rematchReady: ["{n}/{max} ready", "{n}/{max} pronti", "{n}/{max} listos", "{n}/{max} pr\xEAts", "{n}/{max} bereit", "{n}/{max} prontos", "{n}/{max} \u4EBA\u304C\u6E96\u5099\u5B8C\u4E86"],
4000
+ rematchStart: ["Start rematch", "Avvia rivincita", "Iniciar revancha", "Lancer la revanche", "Revanche starten", "Iniciar revanche", "\u518D\u6226\u3092\u958B\u59CB"],
4001
+ won: ["You won", "Hai vinto", "Has ganado", "Vous avez gagn\xE9", "Du hast gewonnen", "Voc\xEA venceu", "\u52DD\u5229"],
4002
+ lost: ["You lost", "Hai perso", "Has perdido", "Vous avez perdu", "Du hast verloren", "Voc\xEA perdeu", "\u6557\u5317"],
4003
+ draw: ["Draw", "Pareggio", "Empate", "\xC9galit\xE9", "Unentschieden", "Empate", "\u5F15\u304D\u5206\u3051"],
4004
+ standings: ["Standings", "Piazzamenti", "Posiciones", "R\xE9sultats", "Platzierungen", "Coloca\xE7\xF5es", "\u9806\u4F4D"],
4005
+ points: ["points", "punti", "puntos", "points", "Punkte", "pontos", "\u30DD\u30A4\u30F3\u30C8"],
4006
+ time: ["time", "tempo", "tiempo", "temps", "Zeit", "tempo", "\u6642\u9593"],
4007
+ distance: ["distance", "distanza", "distancia", "distance", "Distanz", "dist\xE2ncia", "\u8DDD\u96E2"],
4008
+ again: ["Play again", "Gioca ancora", "Jugar de nuevo", "Rejouer", "Erneut spielen", "Jogar novamente", "\u3082\u3046\u4E00\u5EA6\u30D7\u30EC\u30A4"],
4009
+ newRoom: ["New room. Share the new invite.", "Nuova stanza. Condividi il nuovo invito.", "Nueva sala. Comparte la invitaci\xF3n.", "Nouvelle salle. Partagez le lien.", "Neuer Raum. Neue Einladung teilen.", "Nova sala. Compartilhe o convite.", "\u65B0\u3057\u3044\u30EB\u30FC\u30E0\u3067\u3059\u3002\u65B0\u3057\u3044\u62DB\u5F85\u3092\u5171\u6709\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
4010
+ watching: ["Watching", "In osservazione", "Observando", "Spectateur", "Zuschauen", "Assistindo", "\u89B3\u6226\u4E2D"],
4011
+ delay: ["{n}s delay", "Ritardo {n}s", "Retraso de {n}s", "Retard de {n}s", "{n}s Verz\xF6gerung", "Atraso de {n}s", "{n}\u79D2\u306E\u9045\u5EF6"],
4012
+ exit: ["Exit", "Esci", "Salir", "Quitter", "Verlassen", "Sair", "\u7D42\u4E86"],
4013
+ leaveNow: ["Leave for now", "Esci per ora", "Salir por ahora", "Quitter pour le moment", "Vor\xFCbergehend verlassen", "Sair por enquanto", "\u4E00\u6642\u9000\u51FA"],
4014
+ leaveRoom: ["Leave room", "Lascia la stanza", "Abandonar sala", "Abandonner la salle", "Raum verlassen", "Deixar a sala", "\u30EB\u30FC\u30E0\u3092\u9000\u51FA"],
4015
+ leaveHint: ["Your room stays available for Resume.", "La stanza resta disponibile con Riprendi.", "Podr\xE1s volver a est\xE1 sala.", "Vous pourrez reprendre cette salle.", "Du kannst den Raum fortsetzen.", "Voc\xEA pode voltar a est\xE1 sala.", "\u30EB\u30FC\u30E0\u306F\u5F8C\u304B\u3089\u518D\u958B\u3067\u304D\u307E\u3059\u3002"],
4016
+ temporaryHint: ["The game continues. Rejoining may only be possible briefly.", "La partita continua. Il rientro pu\xF2 essere disponibile solo per poco.", "La partida continua. Volver puede ser posible solo por poco tiempo.", "La partie continue. Le retour peut \xEAtre limit\xE9.", "Das Spiel l\xE4uft weiter. R\xFCckkehr nur kurz m\xF6glich.", "A partida continua. O retorno pode ser limitado.", "\u30B2\u30FC\u30E0\u306F\u7D9A\u304D\u307E\u3059\u3002\u518D\u53C2\u52A0\u3067\u304D\u308B\u6642\u9593\u306F\u9650\u3089\u308C\u308B\u5834\u5408\u304C\u3042\u308A\u307E\u3059\u3002"],
4017
+ abandonHint: ["Leave room gives up your place.", "Lascia la stanza libera il tuo posto.", "Abandonar libera tu plaza.", "Abandonner lib\xE8re votre place.", "Raum verlassen gibt deinen Platz frei.", "Deixar a sala libera sua vaga.", "\u30EB\u30FC\u30E0\u3092\u9000\u51FA\u3059\u308B\u3068\u53C2\u52A0\u67A0\u3092\u624B\u653E\u3057\u307E\u3059\u3002"],
4018
+ reconnecting: ["Reconnecting...", "Riconnessione...", "Reconectando...", "Reconnexion...", "Verbindung wird erneuert...", "Reconectando...", "\u518D\u63A5\u7D9A\u4E2D..."],
4019
+ replaced: ["Opened in another tab", "Aperta in un\u2019altra scheda", "Abierta en otra pest\xE1na", "Ouverte dans un autre onglet", "In anderem Tab ge\xF6ffnet", "Aberta em outra aba", "\u5225\u306E\u30BF\u30D6\u3067\u958B\u304B\u308C\u307E\u3057\u305F"],
4020
+ error: ["Something went wrong. Try again.", "Qualcosa non va. Riprova.", "Algo sali\xF3 mal. Reintenta.", "Une erreur est survenue. R\xE9essayez.", "Etwas ist schiefgelaufen. Erneut versuchen.", "Algo deu errado. Tente novamente.", "\u554F\u984C\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002"],
4021
+ noRoom: ["This room is no longer available.", "Questa stanza non \xE8 pi\xF9 disponibile.", "Esta sala ya no est\xE1 disponible.", "Cette salle n'est plus disponible.", "Dieser Raum ist nicht mehr verf\xFCgbar.", "Esta sala n\xE3o est\xE1 mais disponivel.", "\u3053\u306E\u30EB\u30FC\u30E0\u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002"],
4022
+ full: ["This room is full.", "La stanza \xE8 piena.", "La sala est\xE1 llena.", "Cette salle est pleine.", "Dieser Raum ist voll.", "Esta sala est\xE1 cheia.", "\u30EB\u30FC\u30E0\u306F\u6E80\u54E1\u3067\u3059\u3002"],
4023
+ noMatch: ["No match this time. Try again.", "Nessun gruppo trovato. Riprova.", "No hay grupo. Reintenta.", "Aucun groupe trouv\xE9. R\xE9essayez.", "Keine Gruppe gefunden. Erneut versuchen.", "Nenhum grupo encontrado. Tente novamente.", "\u76F8\u624B\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002"],
4024
+ invalidCode: ["Enter a six-character room code.", "Inserisci un codice di sei caratteri.", "Escribe un c\xF3digo de seis caracteres.", "Entrez un code de six caracteres.", "Sechsstelligen Raumcode eingeben.", "Digite um c\xF3digo de seis caracteres.", "6\u6587\u5B57\u306E\u30EB\u30FC\u30E0\u30B3\u30FC\u30C9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
4025
+ refused: ["The room did not accept that change.", "La stanza ha rifiutato la modifica.", "La sala rechaz\xF3 el cambio.", "La salle a refus\xE9 ce changement.", "Der Raum hat die \xC4nderung abgelehnt.", "A sala recusou a altera\xE7\xE3o.", "\u30EB\u30FC\u30E0\u306F\u5909\u66F4\u3092\u53D7\u3051\u4ED8\u3051\u307E\u305B\u3093\u3067\u3057\u305F\u3002"],
4026
+ unavailable: ["Unavailable right now", "Non disponibile ora", "No disponible ahora", "Indisponible pour le moment", "Derzeit nicht verf\xFCgbar", "Indisponivel agora", "\u73FE\u5728\u5229\u7528\u3067\u304D\u307E\u305B\u3093"],
4027
+ offline: ["Connection unavailable. Try again.", "Connessione non disponibile. Riprova.", "Sin conexi\xF3n. Reintenta.", "Connexion indisponible. R\xE9essayez.", "Keine Verbindung. Erneut versuchen.", "Sem conex\xE3o. Tente novamente.", "\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002"],
4028
+ saveFailed: ["Keep the room code. Resume could not be saved.", "Conserva il codice. Riprendi non \xE8 stato salvato.", "Guarda el c\xF3digo. No se pudo guardar el regreso.", "Gardez le code. La reprise ne peut pas \xEAtre enregistr\xE9e.", "Raumcode aufbewahren. Fortsetzen nicht gespeichert.", "Guarde o c\xF3digo. O retorno n\xE3o foi salvo.", "\u30EB\u30FC\u30E0\u30B3\u30FC\u30C9\u3092\u63A7\u3048\u3066\u304F\u3060\u3055\u3044\u3002\u518D\u958B\u60C5\u5831\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002"],
4029
+ boards: ["Leaderboard", "Classifica", "Clasificaci\xF3n", "Classement", "Bestenliste", "Classifica\xE7\xE3o", "\u30E9\u30F3\u30AD\u30F3\u30B0"],
4030
+ board: ["Board", "Classifica", "Tabla", "Classement", "Bestenliste", "Tabela", "\u30E9\u30F3\u30AD\u30F3\u30B0"],
4031
+ daily: ["Daily", "Giornaliera", "Diaria", "Du jour", "T\xE4glich", "Di\xE1ria", "\u65E5\u5225"],
4032
+ allTime: ["All time", "Di sempre", "Hist\xF3rica", "Tous les temps", "Gesamt", "Geral", "\u5168\u671F\u9593"],
4033
+ accounts: ["Accounts", "Account", "Cuentas", "Comptes", "Konten", "Contas", "\u30A2\u30AB\u30A6\u30F3\u30C8"],
4034
+ guests: ["Guests", "Ospiti", "Invitados", "Invit\xE9s", "G\xE4ste", "Visitantes", "\u30B2\u30B9\u30C8"],
4035
+ category: ["Category", "Categoria", "Categoria", "Cat\xE9gorie", "Kategorie", "Categoria", "\u533A\u5206"],
4036
+ period: ["Period", "Periodo", "Per\xEDodo", "P\xE9riode", "Zeitraum", "Per\xEDodo", "\u671F\u9593"],
4037
+ rank: ["Rank", "Posizione", "Puesto", "Rang", "Platz", "Posicao", "\u9806\u4F4D"],
4038
+ score: ["Score", "Punteggio", "Puntos", "Score", "Punkte", "Pontos", "\u30B9\u30B3\u30A2"],
4039
+ verified: ["Verified", "Verificato", "Verificado", "V\xE9rifi\xE9", "Verifiziert", "Verificado", "\u78BA\u8A8D\u6E08\u307F"],
4040
+ own: ["Your best", "Il tuo record", "Tu record", "Votre record", "Dein Rekord", "Seu recorde", "\u81EA\u5DF1\u30D9\u30B9\u30C8"],
4041
+ empty: ["No scores yet", "Nessun punteggio", "A\xFAn no hay puntos", "Aucun score", "Noch keine Punkte", "Ainda sem pontos", "\u307E\u3060\u30B9\u30B3\u30A2\u304C\u3042\u308A\u307E\u305B\u3093"],
4042
+ saving: ["Saving score...", "Salvataggio punteggio...", "Guardando puntos...", "Enregistrement du score...", "Punkte werden gespeichert...", "Salvando pontos...", "\u30B9\u30B3\u30A2\u3092\u4FDD\u5B58\u4E2D..."],
4043
+ saved: ["Your best is on the board", "Il tuo record \xE8 in classifica", "Tu record est\xE1 en la tabla", "Votre record est au classement", "Dein Rekord ist eingetragen", "Seu recorde est\xE1 na tabela", "\u81EA\u5DF1\u30D9\u30B9\u30C8\u304C\u30E9\u30F3\u30AD\u30F3\u30B0\u306B\u53CD\u6620\u3055\u308C\u307E\u3057\u305F"],
4044
+ bestAlready: ["Your best is already on the board", "Il tuo record era gi\xE0 in classifica", "Tu record ya estaba en la tabla", "Votre record est d\xE9j\xE0 au classement", "Dein Rekord ist bereits eingetragen", "Seu recorde j\xE1 est\xE1 na tabela", "\u81EA\u5DF1\u30D9\u30B9\u30C8\u306F\u53CD\u6620\u6E08\u307F\u3067\u3059"],
4045
+ refresh: ["Refresh", "Aggiorna", "Actualizar", "Actualiser", "Aktualisieren", "Atualizar", "\u66F4\u65B0"],
4046
+ refreshHint: ["Score not visible yet. Refresh to check.", "Punteggio non ancora visibile. Aggiorna per controllare.", "Puntos a\xFAn no visibles. Actualiza.", "Score pas encore visible. Actualisez.", "Punkte noch nicht sichtbar. Aktualisieren.", "Pontos ainda n\xE3o visiveis. Atualize.", "\u30B9\u30B3\u30A2\u304C\u307E\u3060\u8868\u793A\u3055\u308C\u307E\u305B\u3093\u3002\u66F4\u65B0\u3057\u3066\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
4047
+ friends: ["Friends & party", "Amici e gruppo", "Amigos y grupo", "Amis et groupe", "Freunde & Gruppe", "Amigos e grupo", "\u53CB\u9054\u3068\u30D1\u30FC\u30C6\u30A3\u30FC"],
4048
+ localCrew: ["Friends and party are unavailable in local preview.", "Amici e gruppo non disponibili in anteprima locale.", "Amigos y grupo no disponibles en la vista local.", "Amis et groupe indisponibles en aper\xE7u local.", "Freunde und Gruppe in lokaler Vorschau nicht verf\xFCgbar.", "Amigos e grupo indispon\xEDveis na pr\xE9via local.", "\u30ED\u30FC\u30AB\u30EB\u30D7\u30EC\u30D3\u30E5\u30FC\u3067\u306F\u53CB\u9054\u3068\u30D1\u30FC\u30C6\u30A3\u30FC\u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002"],
4049
+ loginCrew: ["Sign in on Caisual to use friends and party.", "Accedi a Caisual per amici e gruppo.", "Inicia sesion para amigos y grupo.", "Connectez-vous pour utiliser amis et groupe.", "F\xFCr Freunde und Gruppe bei Caisual anmelden.", "Entre no Caisual para amigos e grupo.", "\u53CB\u9054\u3068\u30D1\u30FC\u30C6\u30A3\u30FC\u3092\u5229\u7528\u3059\u308B\u306B\u306FCaisual\u306B\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
4050
+ online: ["Online", "Online", "En linea", "En ligne", "Online", "Online", "\u30AA\u30F3\u30E9\u30A4\u30F3"],
4051
+ noFriends: ["No friends online", "Nessun amico online", "Sin amigos en linea", "Aucun ami en ligne", "Keine Freunde online", "Nenhum amigo online", "\u30AA\u30F3\u30E9\u30A4\u30F3\u306E\u53CB\u9054\u306F\u3044\u307E\u305B\u3093"],
4052
+ createParty: ["Create party", "Crea gruppo", "Crear grupo", "Cr\xE9er un groupe", "Gruppe erstellen", "Criar grupo", "\u30D1\u30FC\u30C6\u30A3\u30FC\u3092\u4F5C\u6210"],
4053
+ inviteParty: ["Invite to party", "Invita nel gruppo", "Invitar al grupo", "Inviter au groupe", "In Gruppe einladen", "Convidar para o grupo", "\u30D1\u30FC\u30C6\u30A3\u30FC\u306B\u62DB\u5F85"],
4054
+ leaveParty: ["Leave party", "Lascia gruppo", "Salir del grupo", "Quitter le groupe", "Gruppe verlassen", "Sair do grupo", "\u30D1\u30FC\u30C6\u30A3\u30FC\u3092\u9000\u51FA"],
4055
+ accept: ["Accept", "Accetta", "Aceptar", "Accepter", "Annehmen", "Aceitar", "\u627F\u8AFE"],
4056
+ decline: ["Decline", "Rifiuta", "Rechazar", "Refuser", "Ablehnen", "Recusar", "\u8F9E\u9000"],
4057
+ follow: ["Join them", "Raggiungi", "Unirse", "Rejoindre", "Beitreten", "Juntar-se", "\u4E00\u7DD2\u306B\u53C2\u52A0"],
4058
+ voice: ["Voice", "Voce", "Voz", "Voix", "Sprache", "Voz", "\u30DC\u30A4\u30B9"],
4059
+ voiceJoin: ["Join voice", "Entra in voce", "Unirse a voz", "Activer la voix", "Sprachchat beitreten", "Entrar na voz", "\u30DC\u30A4\u30B9\u306B\u53C2\u52A0"],
4060
+ voiceLeave: ["Leave voice", "Esci dalla voce", "Salir de voz", "Quitter la voix", "Sprachchat verlassen", "Sair da voz", "\u30DC\u30A4\u30B9\u3092\u9000\u51FA"],
4061
+ voiceMute: ["Mute", "Disattiva microfono", "Silenciar", "Couper le micro", "Stummschalten", "Silenciar", "\u30DF\u30E5\u30FC\u30C8"],
4062
+ voiceUnmute: ["Unmute", "Attiva microfono", "Activar micr\xF3fono", "Activer le micro", "Mikrofon aktivieren", "Ativar microfone", "\u30DF\u30E5\u30FC\u30C8\u89E3\u9664"],
4063
+ voiceOff: ["Voice off", "Voce disattivata", "Voz desactivada", "Voix d\xE9sactiv\xE9e", "Sprachchat aus", "Voz desativada", "\u30DC\u30A4\u30B9\u30AA\u30D5"],
4064
+ voiceJoining: ["Joining voice...", "Connessione voce...", "Conectando voz...", "Connexion vocale...", "Sprachchat verbindet...", "Conectando voz...", "\u30DC\u30A4\u30B9\u306B\u63A5\u7D9A\u4E2D..."],
4065
+ voiceOn: ["Voice connected", "Voce connessa", "Voz conectada", "Voix connect\xE9e", "Sprachchat verbunden", "Voz conectada", "\u30DC\u30A4\u30B9\u63A5\u7D9A\u6E08\u307F"],
4066
+ voiceMuted: ["Muted", "Microfono disattivato", "Silenciado", "Micro coup\xE9", "Stumm", "Silenciado", "\u30DF\u30E5\u30FC\u30C8\u4E2D"],
4067
+ voiceMic: ["Mic on", "Microfono attivo", "Micr\xF3fono activo", "Micro actif", "Mikrofon an", "Microfone ativo", "\u30DE\u30A4\u30AF\u30AA\u30F3"],
4068
+ voiceListening: ["Listening only", "Solo ascolto", "Solo escucha", "\xC9coute seule", "Nur zuh\xF6ren", "Somente ouvindo", "\u805E\u304F\u3060\u3051"],
4069
+ voiceSpeaking: ["Speaking", "Sta parlando", "Hablando", "Parle", "Spricht", "Falando", "\u767A\u8A71\u4E2D"],
4070
+ voicePeers: ["Voice participants", "Partecipanti in voce", "Participantes de voz", "Participants vocaux", "Sprachteilnehmer", "Participantes de voz", "\u30DC\u30A4\u30B9\u53C2\u52A0\u8005"],
4071
+ voiceEmpty: ["No one else in voice yet.", "Nessun altro in voce per ora.", "A\xFAn no hay nadie m\xE1s en voz.", "Personne d\u2019autre en voix pour le moment.", "Noch niemand im Sprachchat.", "Ningu\xE9m mais na voz ainda.", "\u4ED6\u306E\u53C2\u52A0\u8005\u306F\u307E\u3060\u3044\u307E\u305B\u3093\u3002"],
4072
+ voiceVolume: ["Volume for {name}", "Volume di {name}", "Volumen de {name}", "Volume de {name}", "Lautst\xE4rke f\xFCr {name}", "Volume de {name}", "{name}\u306E\u97F3\u91CF"],
4073
+ voiceUnavailable: ["Join a room with voice to use these controls.", "Entra in una stanza con voce per usare questi controlli.", "Entra en una sala con voz para usar estos controles.", "Rejoignez une salle vocale pour utiliser ces commandes.", "Diese Steuerung braucht einen Raum mit Sprachchat.", "Entre em uma sala com voz para usar estes controles.", "\u30DC\u30A4\u30B9\u5BFE\u5FDC\u306E\u30EB\u30FC\u30E0\u306B\u53C2\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
4074
+ voiceWatch: ["Voice is unavailable while watching.", "La voce non e' disponibile in osservazione.", "La voz no est\xE1 disponible al observar.", "La voix est indisponible en observation.", "Beim Zuschauen ist kein Sprachchat verf\xFCgbar.", "A voz n\xE3o est\xE1 dispon\xEDvel ao assistir.", "\u89B3\u6226\u4E2D\u306F\u30DC\u30A4\u30B9\u3092\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002"],
4075
+ voiceDenied: ["Microphone permission denied. Allow it in your browser, then try again.", "Permesso microfono negato. Consenti l'accesso nel browser e riprova.", "Permiso de micr\xF3fono denegado. Act\xEDvalo en el navegador e int\xE9ntalo de nuevo.", "Acc\xE8s au micro refus\xE9. Autorisez-le dans le navigateur, puis r\xE9essayez.", "Mikrofonzugriff verweigert. Im Browser erlauben und erneut versuchen.", "Permiss\xE3o do microfone negada. Permita no navegador e tente novamente.", "\u30DE\u30A4\u30AF\u304C\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u30D6\u30E9\u30A6\u30B6\u3067\u8A31\u53EF\u3057\u3066\u304B\u3089\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002"],
4076
+ voiceUnsupported: ["Voice is not supported in this browser.", "Questo browser non supporta la voce.", "Este navegador no admite voz.", "Ce navigateur ne prend pas en charge la voix.", "Dieser Browser unterst\xFCtzt keinen Sprachchat.", "Este navegador n\xE3o oferece suporte a voz.", "\u3053\u306E\u30D6\u30E9\u30A6\u30B6\u306F\u30DC\u30A4\u30B9\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\u3002"],
4077
+ voiceFailed: ["Voice could not connect. Try again.", "Connessione voce non riuscita. Riprova.", "No se pudo conectar la voz. Int\xE9ntalo de nuevo.", "Connexion vocale impossible. R\xE9essayez.", "Sprachverbindung fehlgeschlagen. Erneut versuchen.", "N\xE3o foi poss\xEDvel conectar a voz. Tente novamente.", "\u30DC\u30A4\u30B9\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002"],
4078
+ voicePeerGone: ["This participant has left voice.", "Questo partecipante e' uscito dalla voce.", "Este participante sali\xF3 de voz.", "Ce participant a quitt\xE9 la voix.", "Diese Person hat den Sprachchat verlassen.", "Este participante saiu da voz.", "\u3053\u306E\u53C2\u52A0\u8005\u306F\u30DC\u30A4\u30B9\u3092\u9000\u51FA\u3057\u307E\u3057\u305F\u3002"],
4079
+ shortcut: ["Shift+Tab shortcut", "Scorciatoia Shift+Tab", "Atajo Shift+Tab", "Raccourci Maj+Tab", "Umschalt+Tab-Kurzbefehl", "Atalho Shift+Tab", "Shift+Tab\u30B7\u30E7\u30FC\u30C8\u30AB\u30C3\u30C8"],
4080
+ menu: ["Caisual menu", "Menu Caisual", "Menu Caisual", "Menu Caisual", "Caisual-Menu", "Menu Caisual", "Caisual\u30E1\u30CB\u30E5\u30FC"],
4081
+ retry: ["Retry", "Riprova", "Reintentar", "R\xE9essayer", "Erneut versuchen", "Tentar novamente", "\u518D\u8A66\u884C"]
3853
4082
  };
3854
4083
  var column = (index) => Object.fromEntries(Object.entries(words).map(([key, row]) => [key, row[index]]));
3855
- var dictionaries = { en: column(0), it: column(1), es: column(2), fr: column(3), de: column(4), pt: column(5) };
4084
+ var dictionaries = { en: column(0), it: column(1), es: column(2), fr: column(3), de: column(4), pt: column(5), ja: column(6) };
3856
4085
  var styles = `
3857
- .game-icon{width:28px;height:28px;aspect-ratio:1;object-fit:contain;border-radius:7px;flex:none;vertical-align:middle}.game-icon-title{width:56px;height:56px;margin-bottom:12px}
4086
+ .game-icon{width:28px;height:28px;aspect-ratio:1;object-fit:contain;border-radius:7px;flex:none;vertical-align:middle}.game-icon-title{width:36px;height:36px;border-radius:10px}
3858
4087
  .safe-area-probe{position:fixed;visibility:hidden;pointer-events:none;padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px)}
3859
4088
  :host{all:initial;position:fixed;inset:0;z-index:10000;pointer-events:none;font:15px/1.45 system-ui,sans-serif;color:#f4f4f1;color-scheme:dark;--accent:#a8efc5}
3860
4089
  [data-layout],[data-surface],.sr{pointer-events:none}*{box-sizing:border-box}button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}button,select,input{border:1px solid #ffffff30;background:#25292b;color:inherit;border-radius:12px;min-height:44px;padding:10px 14px}button{cursor:pointer}button:disabled{opacity:.45;cursor:default}button:hover:not(:disabled){background:#343b3a}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--accent);outline-offset:3px}a{color:var(--accent)}.primary{background:var(--accent);color:var(--accent-ink,#11221b);border-color:transparent;font-weight:700}.primary:hover:not(:disabled){filter:brightness(1.1);background:var(--accent)}.quiet{background:transparent}label{display:grid;gap:6px;text-align:left}select,input{width:100%;min-width:0}h1,h2,p{margin:0}h1{font-size:clamp(26px,5vw,42px);line-height:1.1;letter-spacing:-.035em}h2{font-size:20px}small,.muted{color:#bdc5c1}.stack{display:grid;gap:16px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row>*{flex:0 1 auto}.row .grow,.grow{flex:1}.split{display:grid;grid-template-columns:1fr 1fr;gap:10px}.pill{position:absolute;top:max(10px,env(safe-area-inset-top));right:max(10px,env(safe-area-inset-right));display:flex;height:44px;border:1px solid #ffffff35;border-radius:24px;background:#171e20eF;box-shadow:0 4px 20px #0004;pointer-events:auto;overflow:hidden}.pill button{border:0;border-radius:0;padding:8px 13px;background:transparent}.pill button:focus-visible{outline-offset:-4px}.pill small{margin-left:8px}.backdrop{position:absolute;inset:0;background:#0b151ce8;backdrop-filter:blur(10px);pointer-events:auto;display:grid;place-items:center;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));overflow:auto}.backdrop.home{background-color:#142127;background-size:contain;background-repeat:no-repeat;background-position:center}.dialog{position:relative;width:min(100%,540px);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#141b1df5;border:1px solid #ffffff25;border-radius:22px;padding:24px;box-shadow:0 20px 80px #0005}.dialog.wide{width:min(100%,700px)}.top{display:flex;align-items:center;gap:12px;margin-bottom:18px}.top h2{flex:1}.tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid #ffffff25;padding-bottom:12px}.tabs button{min-height:36px;padding:6px 10px}.tabs [aria-current=true]{border-color:var(--accent)}.roster{list-style:none;padding:0;margin:0;display:grid;gap:8px;max-height:32dvh;overflow:auto}.roster li{display:flex;align-items:center;gap:8px;padding:10px;background:#ffffff08;border-radius:10px}.roster .name{flex:1;overflow-wrap:anywhere}.badge{border:1px solid #ffffff30;border-radius:6px;padding:2px 6px;font-size:12px}.code{font-size:24px;letter-spacing:.13em;font-variant-numeric:tabular-nums}.notice,.error{border-radius:10px;padding:10px;background:#a8efc514;overflow-wrap:anywhere}.error{background:#ff8b7720;color:#ffd2c9}.countdown{font-size:88px;line-height:1;text-align:center;font-variant-numeric:tabular-nums}.ended{position:absolute;bottom:max(12px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);max-width:calc(100% - 24px);width:max-content;background:#171e20f5;pointer-events:auto;border:1px solid #ffffff30;border-radius:16px;padding:10px;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap}.ended [data-rematch-players]{max-width:100%;max-height:3.2em;overflow:auto;overflow-wrap:anywhere}.standings{list-style:none;margin:0;padding:0;flex-basis:100%;max-height:20dvh;overflow:auto;font-size:13px}.standings li{display:flex;justify-content:space-between;gap:16px;overflow-wrap:anywhere}.ended{max-height:calc(100dvh - 80px);overflow:auto}.ended strong{padding:0 8px}.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.table-wrap{overflow:auto;max-height:38dvh}table{width:100%;border-collapse:collapse;text-align:left}th,td{padding:9px 6px;border-bottom:1px solid #ffffff20}td:nth-child(3){text-align:right}td small{display:block}.self{background:#ffffff0a}.checkbox{display:flex;align-items:center;gap:8px;font-size:13px}.checkbox input{width:18px;min-height:18px}.full{width:100%}
4090
+ .game-heading{display:flex;align-items:center;gap:12px;min-width:0;flex:1}.game-heading h1{font-size:28px;overflow-wrap:anywhere}.home .top{margin-bottom:12px}.game-description{font-size:13px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.experience-tabs{display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:4px;border:1px solid #ffffff18;border-radius:14px;background:#0003}.experience-tabs button{background:transparent;border-color:transparent;font-size:14px;font-weight:600;padding:10px 8px;border-radius:10px;color:#bdc5c1}.experience-tabs [aria-selected=true]{background:#ffffff16;color:#f4f4f1;box-shadow:0 1px 4px #0003}.experience-tabs button:focus-visible{outline-offset:-3px}.home-content{gap:14px;min-width:0}.mode-details{display:grid;gap:6px;min-width:0}.mode-details h2{font-size:16px;font-weight:600}.mode-details label{font-size:13px}.mode-instructions{font-size:12px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.play-actions{gap:8px}.play-actions .primary{min-height:50px;font-size:16px}.home-links{gap:4px}.home-links button{border-color:transparent;font-size:13px}.home-links button:hover:not(:disabled){background:#ffffff0a}.resume-action{gap:4px}.resume-action small{text-align:center}.panel-footer{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:18px;padding-top:10px;border-top:1px solid #ffffff18;color:#bdc5c1}.panel-footer .checkbox{font-size:11px;white-space:nowrap;min-height:32px;gap:6px}.panel-footer input{margin:0;accent-color:var(--accent);width:14px;min-height:14px}.panel-footer small{min-width:0;text-align:right;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase}
3861
4091
  [hidden]{display:none!important}.voice-peers{list-style:none;margin:0;padding:0;display:grid;gap:10px}.voice-peers li{border:1px solid #ffffff25;border-radius:12px;padding:12px;display:grid;gap:8px}.voice-peers [data-speaking=true]{border-color:var(--accent)}.voice-peers input{width:100%;accent-color:var(--accent);padding:0}.voice-peers label{font-size:13px}.pill .voice-toggle{width:44px;padding:8px}.voice-toggle[data-voice-state=on][data-muted=false]{color:var(--accent)}
3862
4092
  .boot{position:absolute;inset:0;z-index:2;isolation:isolate;display:grid;place-items:center;overflow:auto;overscroll-behavior:contain;padding:max(100px,env(safe-area-inset-top)) max(24px,env(safe-area-inset-right)) max(48px,env(safe-area-inset-bottom)) max(24px,env(safe-area-inset-left));background:#0b151c;opacity:1;transition:opacity .4s ease;pointer-events:auto;outline:none}
3863
4093
  .boot::before,.boot::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:-1}.boot::before{background:radial-gradient(ellipse at 50% 38%,color-mix(in srgb,var(--accent),transparent 80%),transparent 65%)}.boot::after{background:radial-gradient(ellipse at 50% 38%,#0b151c20,#0b151cd9 85%),linear-gradient(#0b151c66,#0b151cbf)}
@@ -3867,6 +4097,7 @@ var styles = `
3867
4097
  .boot-progress{width:112px;height:3px;border-radius:12px;background:#ffffff20;overflow:hidden;margin-top:12px}.boot-progress span{display:block;width:44%;height:100%;border-radius:inherit;background:var(--boot-accent);animation:boot-progress 1.8s ease-in-out infinite}.boot-status{max-width:42ch;min-height:3em;font-size:14px;line-height:1.5;color:#d3dad6;text-wrap:balance}.boot-recovery{min-height:44px}.boot-recovery .row{justify-content:center}.boot-leaving{opacity:0;pointer-events:none}
3868
4098
  @keyframes boot-progress{0%{transform:translateX(-110%)}100%{transform:translateX(340%)}}
3869
4099
  @media(max-width:480px){.dialog{padding:18px;border-radius:18px}.split{grid-template-columns:1fr 1fr;gap:8px}.tabs{gap:4px}.tabs button{font-size:13px;padding:6px 8px}.pill button:focus-visible{outline-offset:-4px}.pill small{display:none}.ended{gap:6px}.standings{list-style:none;margin:0;padding:0;flex-basis:100%;max-height:20dvh;overflow:auto;font-size:13px}.standings li{display:flex;justify-content:space-between;gap:16px;overflow-wrap:anywhere}.ended{max-height:calc(100dvh - 80px);overflow:auto}.ended strong{font-size:13px}.ended button{padding:8px 10px;font-size:13px}.roster{max-height:28dvh}}
4100
+ .replay-controls{position:absolute;bottom:max(12px,env(safe-area-inset-bottom));left:12px;right:12px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px;background:#141b1df5;border:1px solid #ffffff30;border-radius:16px;pointer-events:auto}.replay-position{flex:1;min-width:100px}.replay-controls label{font-size:12px}.replay-controls span{font-variant-numeric:tabular-nums;font-size:13px}.replay-link{display:inline-flex;align-items:center;min-height:44px;padding:10px 14px;border-radius:12px;text-decoration:none}
3870
4101
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}.boot{transition:none}.boot-progress span{animation:none;transform:translateX(65%)}}
3871
4102
  `;
3872
4103
 
@@ -3945,7 +4176,7 @@ function add(watch) {
3945
4176
  section.dataset.player = String(slot); title.textContent = watch ? 'Spectator ' + slot : 'Player ' + slot;
3946
4177
  const drop = document.createElement('button'), spectate = document.createElement('button');
3947
4178
  drop.textContent = 'Drop'; spectate.textContent = 'Spectate';
3948
- frame.title = title.textContent; frame.allow = 'autoplay; fullscreen; microphone; gamepad; pointer-lock; cross-origin-isolated';
4179
+ frame.title = title.textContent; frame.allow = 'autoplay; fullscreen; microphone; gamepad; pointer-lock';
3949
4180
  const query = new URLSearchParams({ devPlayer: id, lang: params.get('lang') || 'en' });
3950
4181
  if (watch) query.set('devWatch', watch);
3951
4182
  frame.src = '/?' + query;
@@ -3985,8 +4216,8 @@ var MASSIMO_FRAME_MATCH = 4096;
3985
4216
  var DURATA_STANZA_APERTA = 24 * 60 * 60 * 1e3;
3986
4217
  var VALORE_CHIAVE_MATCH = /^[A-Za-z0-9_.:-]+$/;
3987
4218
  var PREFISSO_DEPOSITO = /^[a-z0-9_-]{0,32}$/;
3988
- var LIMITE_DEPOSITO2 = 64 * 1024;
3989
- var MASSIMO_CHIAVI_DEPOSITO = 1024;
4219
+ var LIMITE_DEPOSITO2 = 256 * 1024;
4220
+ var MASSIMO_CHIAVI_DEPOSITO = 4096;
3990
4221
  var VERSIONE_STATO_DEV = 1;
3991
4222
  function erroreDeposito(code, message) {
3992
4223
  return Object.assign(new Error(message), { code });
@@ -4211,7 +4442,7 @@ function utcDay(now = Date.now()) {
4211
4442
  return new Date(now).toISOString().slice(0, 10);
4212
4443
  }
4213
4444
  function dailySeed(game, day) {
4214
- return createHash2("sha256").update(`caisual:${game}:${day}`).digest().readUInt32BE(0);
4445
+ return createHash3("sha256").update(`caisual:${game}:${day}`).digest().readUInt32BE(0);
4215
4446
  }
4216
4447
  function randomUniform(alphabet, length) {
4217
4448
  const limit = Math.floor(256 / alphabet.length) * alphabet.length;
@@ -4452,6 +4683,8 @@ async function readGame(root) {
4452
4683
  throw new Error(`caisual.json is not valid:
4453
4684
  ${result.errori.map((error) => `- ${error}`).join("\n")}`);
4454
4685
  }
4686
+ for (const warning of avvisiManifest(parsed)) process.stderr.write(`Warning: ${warning}
4687
+ `);
4455
4688
  const clientRoot = await fs3.realpath(join3(root, "client")).catch(() => null);
4456
4689
  if (clientRoot === null) throw new Error("client/: folder not found.");
4457
4690
  const stat = await fs3.stat(clientRoot);
@@ -4471,12 +4704,19 @@ async function loadDefinition(root) {
4471
4704
  throw new Error("server.js: file not readable.");
4472
4705
  }
4473
4706
  if (!stat.isFile()) throw new Error("server.js: file not readable.");
4474
- const { source } = await bundleServer(root);
4707
+ const { source, wasm } = await bundleServer(root);
4475
4708
  const kitUrl = `data:text/javascript;base64,${Buffer.from('// src/server/index.ts\nvar GAME_DEFINITION = /* @__PURE__ */ Symbol.for("@caisual/kit/game-definition");\nvar CALLBACKS = [\n "onCreate",\n "onStart",\n "onRestart",\n "onJoin",\n "onConnection",\n "onLeave",\n "onMessage",\n "onRoleRequest",\n "onTick",\n "onEnd"\n];\nfunction isRecord(value) {\n return typeof value === "object" && value !== null && !Array.isArray(value);\n}\nfunction defineGame(definition) {\n if (!isRecord(definition)) {\n throw new TypeError("Game definition must be an object.");\n }\n if (typeof definition.tickRate !== "number" || !Number.isInteger(definition.tickRate) || definition.tickRate < 0 || definition.tickRate > 60) {\n throw new TypeError("Game definition tickRate must be an integer from 0 to 60.");\n }\n for (const callback of CALLBACKS) {\n const value = definition[callback];\n if (value !== void 0 && typeof value !== "function") {\n throw new TypeError(`Game definition ${callback} must be a function.`);\n }\n }\n Object.defineProperty(definition, GAME_DEFINITION, {\n configurable: false,\n enumerable: false,\n value: true,\n writable: false\n });\n return definition;\n}\nexport {\n defineGame\n};\n').toString("base64")}`;
4476
- const rewritten = source.replace(
4709
+ let rewritten = source.replace(
4477
4710
  /(\bfrom\s*)(['"])@caisual\/kit\/server\2/g,
4478
4711
  (_match, prefix) => `${prefix}${JSON.stringify(kitUrl)}`
4479
4712
  );
4713
+ const wasmUrls = new Map(wasm.map(({ path: path2, content }) => {
4714
+ const wrapper = `export default await WebAssembly.compile(Uint8Array.from(atob('${Buffer.from(content).toString("base64")}'), c => c.charCodeAt(0)));`;
4715
+ return [path2, `data:text/javascript;base64,${Buffer.from(wrapper).toString("base64")}`];
4716
+ }));
4717
+ for (const { inizio, fine, nome, path: path2 } of importWasmServer(rewritten).reverse()) {
4718
+ rewritten = rewritten.slice(0, inizio) + `import ${nome} from ${JSON.stringify(wasmUrls.get(path2))}` + rewritten.slice(fine);
4719
+ }
4480
4720
  const sourceUrl = `data:text/javascript;base64,${Buffer.from(rewritten).toString("base64")}`;
4481
4721
  const loaded = await import(sourceUrl);
4482
4722
  if (loaded.default === void 0) throw new Error("server.js must have an export default.");
@@ -5028,7 +5268,7 @@ var DevService = class {
5028
5268
  response.setHeader("Content-Type", "text/javascript; charset=utf-8");
5029
5269
  response.setHeader("Cache-Control", "no-store");
5030
5270
  response.setHeader("X-Content-Type-Options", "nosniff");
5031
- response.end(request.method === "HEAD" ? void 0 : '// Caisual game kit v0.15.0\nvar ft=["www","api","app","play","live","multi","cdn","assets","static","mail","mx","ns1","ns2","autodiscover","_dmarc","admin","login","account","auth","pay","secure","support","help","blog","status","dev","staging","test","caisual","shipz"],ht=new Set(ft),gt=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,vt=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;function de(n){return n.length>=3&&n.length<=32&&gt.test(n)||vt.test(n)}function Oe(n){return ht.has(n)}function q(n){if(typeof n!="string"||n.length>128)return null;try{return Intl.getCanonicalLocales(n)[0]??null}catch{return null}}function we(n){return n.languages?.length?[...n.languages]:[n.language??"en"]}function yt(n,e="en"){let t=[],i=q(n);for(;i;){t.push(i);let r=i.split("-");r.pop(),r.at(-1)?.length===1&&r.pop(),i=r.join("-")}return t.push(q(e)??e),[...new Set(t)]}function Ee(n,e=[]){let t=e.map(q).filter(r=>r!==null),i=n.map(q).filter(r=>r!==null);if(!t.length)return i[0]??"en";for(let r of i)for(let o of yt(r,r))if(t.includes(o))return o;return t[0]}function Ve(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)&&Object.values(n).every(e=>typeof e=="string")}function je(n,e){let t=e===null?void 0:n.modes.find(i=>i.id===e);if(e!==null&&t===void 0)throw new Error("The selected game mode does not exist.");return{players:{...t?.players??n.players},lobby:t?.lobby??n.lobby}}function pe(n,e){return e!==null&&n.modes.some(t=>t.id===e&&t.execution==="local")}var T=24;var bt=3e3,_e=32,wt=new Set(["overlay","manifest","id","name","description","cover","card","icon","screenshots","tags","languages","language","platform","orientation","input","visibility","network","isolated","requires","players","lobby","persistent","spectators","boards","roles","teams","voice","modes"]),St=new Set(["keyboard","mouse","touch","gamepad"]),Rt=new Set(["desktop","mobile","both"]),xt=new Set(["landscape","portrait"]),kt=new Set(["public","unlisted"]),Ct=new Set(["none","room","team","proximity"]),Pt=new Set(["light","medium","heavy"]),At=/^[a-z0-9-]+$/,De=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,Tt=/^[a-z0-9][a-z0-9-]{0,31}$/,Mt=/^[a-z0-9][a-z0-9_-]{0,31}$/;function J(n){return typeof n!="object"||n===null||Array.isArray(n)?null:n}function Le(n){if(n===""||n.startsWith("/")||n.includes("\\\\")||n.includes("\\0")||n.includes("?")||n.includes("#"))return!1;let e=n.split("/");if(e.some(t=>t===""||t==="."||t===".."))return!1;try{return!e.map(i=>decodeURIComponent(i)).some(i=>i===""||i==="."||i===".."||i.includes("/"))}catch{return!1}}function It(n){return n.length===0||n.length>253||n.includes("://")||/[/:?#@]/.test(n)?!1:n.split(".").every(t=>t.length>=1&&t.length<=63&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(t))}function _(n,e,t){return typeof n=="number"&&Number.isInteger(n)&&n>=e&&n<=t}function Se(n,e,t,i){let r=n[e];return r===void 0?t:typeof r!="string"?(i.push(`${e}: must be a string.`),t):r}function me(n,e,t,i,r){if(n[e]===void 0)return;let o=(g,d)=>{if(typeof g!="string"||g.trim().length===0||g.trim().length>t||/[\\r\\n\\u0000-\\u001f]/.test(g)){r.push(`${d}: must contain 1-${t} characters on one line.`);return}return g.trim()},s=n[e],a=i?`${i}.${e}`:e;if(typeof s=="string")return o(s,a);let u=J(s);if(!u||Object.keys(u).length===0){r.push(`${a}: must be a string or a non-empty language-to-text object.`);return}let m={};for(let[g,d]of Object.entries(u)){let c=q(g);if(!c){r.push(`${a}.${g}: must be a BCP 47 language tag.`);continue}Object.hasOwn(m,c)&&r.push(`${a}.${g}: duplicate language.`);let y=o(d,`${a}.${g}`);y!==void 0&&(m[c]=y)}return m}function Re(n){let e=[],t=J(n);if(t===null)return{ok:!1,errori:["manifest: must be a JSON object."]};for(let h of Object.keys(t))wt.has(h)||e.push(`${h}: unknown field.`);t.manifest===void 0?e.push("manifest: is required and must be 1."):t.manifest!==1&&e.push("manifest: must be exactly 1.");let i=Se(t,"id","",e);t.id===void 0?e.push("id: is required."):typeof t.id=="string"&&(de(i)?Oe(i)&&e.push("id: this slug is reserved."):e.push("id: must be 3-32 lowercase ASCII letters or digits with internal hyphens; historical UUID v4 slugs may be 36 characters."));let r=Se(t,"name","",e);t.name===void 0?e.push("name: is required."):typeof t.name=="string"&&(r.trim()===""||r.length>60)&&e.push("name: must contain 1-60 characters.");let o=t.description===""?"":me(t,"description",500,"",e)??"",s={cover:"",card:"",icon:""},a=new Set;for(let h of["cover","card","icon"]){let p=t[h];if(p==null)e.push(`${h}: is required.`);else if(typeof p!="string"||!Le(p))e.push(`${h}: must be a relative file path inside client/ without query, fragment, or parent segments.`);else{/\\.(png|jpe?g|webp)$/i.test(p)||e.push(`${h}: must be a PNG, JPEG or WebP file.`);let z=decodeURIComponent(p);a.has(z)&&e.push(`${h}: each image must use a different file; cover, card and icon cannot share a path.`),a.add(z),s[h]=p}}let{cover:u,card:m,icon:g}=s,d=[];if(t.screenshots!==void 0)if(!Array.isArray(t.screenshots))e.push("screenshots: must be an array of relative file paths.");else{t.screenshots.length>8&&e.push("screenshots: must contain at most 8 paths.");for(let[h,p]of t.screenshots.entries())typeof p!="string"||!Le(p)?e.push(`screenshots[${h}]: must be a relative file path without query, fragment, or parent segments.`):d.push(p)}let c=[];if(t.tags!==void 0)if(!Array.isArray(t.tags))e.push("tags: must be an array.");else{t.tags.length>10&&e.push("tags: must contain at most 10 tags.");for(let[h,p]of t.tags.entries())typeof p!="string"||p.length>24||!At.test(p)?e.push(`tags[${h}]: must be 1-24 lowercase letters, digits, or hyphens.`):c.push(p)}let y=Se(t,"language","en",e);/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(y)||e.push("language: must be a BCP 47 language tag such as en, it, or pt-BR.");let k=[];if(!Array.isArray(t.languages)||t.languages.length===0)e.push("languages: must be a non-empty array of BCP 47 language tags.");else for(let[h,p]of t.languages.entries()){let z=q(p);z?k.includes(z)?e.push(`languages[${h}]: duplicate language ${z}.`):k.push(z):e.push(`languages[${h}]: must be a BCP 47 language tag.`)}k.includes("en")||e.push("languages: English is always required alongside the game\'s own languages.");let C=k[0]??y;if(typeof o=="object")for(let h of Object.keys(o))k.includes(h)||e.push(`description.${h}: language must be declared in languages.`);t.language!==void 0&&t.languages!==void 0&&y.toLowerCase()!==C.toLowerCase()&&e.push("language: must match the first entry in languages when both are present.");let D="both";t.platform===void 0?e.push("platform: is required."):typeof t.platform!="string"||!Rt.has(t.platform)?e.push("platform: must be desktop, mobile, or both."):D=t.platform;let E="landscape";t.orientation!==void 0&&(typeof t.orientation!="string"||!xt.has(t.orientation)?e.push("orientation: must be landscape or portrait."):E=t.orientation);let $=[];if(t.input!==void 0)if(!Array.isArray(t.input))e.push("input: must be an array.");else for(let[h,p]of t.input.entries())typeof p!="string"||!St.has(p)?e.push(`input[${h}]: must be keyboard, mouse, touch, or gamepad.`):$.includes(p)?e.push(`input[${h}]: duplicate value ${p}.`):$.push(p);let V="public";t.visibility!==void 0&&(typeof t.visibility!="string"||!kt.has(t.visibility)?e.push("visibility: must be public or unlisted."):V=t.visibility);let B=[];if(t.network!==void 0)if(!Array.isArray(t.network))e.push("network: must be an array of host names.");else for(let[h,p]of t.network.entries())typeof p!="string"||!It(p)?e.push(`network[${h}]: must be a host name without scheme, port, path, query, or fragment.`):B.includes(p)?e.push(`network[${h}]: duplicate host ${p}.`):B.push(p);let U=!1;t.isolated!==void 0&&(typeof t.isolated!="boolean"?e.push("isolated: must be a boolean."):U=t.isolated);let G={webgl2:!1,webgpu:!1,wasm:!1,threads:!1,memoryMb:null,performance:"light"};if(t.requires!==void 0){let h=J(t.requires);if(h===null)e.push("requires: must be an object.");else{for(let p of Object.keys(h))["webgl2","webgpu","wasm","threads","memoryMb","performance"].includes(p)||e.push(`requires.${p}: unknown field.`);for(let p of["webgl2","webgpu","wasm","threads"])h[p]!==void 0&&(typeof h[p]!="boolean"?e.push(`requires.${p}: must be a boolean.`):G[p]=h[p]);h.memoryMb!==void 0&&(h.memoryMb!==null&&(!_(h.memoryMb,512,32768)||h.memoryMb%256!==0)?e.push("requires.memoryMb: must be an integer from 512 to 32768 in steps of 256, or null."):G.memoryMb=h.memoryMb),h.performance!==void 0&&(typeof h.performance!="string"||!Pt.has(h.performance)?e.push("requires.performance: must be light, medium, or heavy."):G.performance=h.performance),G.threads&&!U&&e.push("requires.threads: needs isolated: true.")}}let I={min:1,max:1};if(t.players!==void 0){let h=J(t.players);if(h===null)e.push("players: must be an object with min and max.");else{for(let p of Object.keys(h))p!=="min"&&p!=="max"&&e.push(`players.${p}: unknown field.`);_(h.min,1,T)||e.push(`players.min: must be an integer from 1 to ${T}.`),_(h.max,1,T)||e.push(`players.max: must be an integer from 1 to ${T} in manifest version 1.`),_(h.min,1,T)&&_(h.max,1,T)&&(h.min>h.max?e.push("players.max: must be greater than or equal to players.min."):I={min:h.min,max:h.max})}}let F=!1;t.lobby!==void 0&&(typeof t.lobby!="boolean"?e.push("lobby: must be a boolean."):F=t.lobby);let P=!1;t.persistent!==void 0&&(typeof t.persistent!="boolean"?e.push("persistent: must be a boolean."):P=t.persistent);let te={delayMs:bt};if(t.spectators===!1||t.spectators===null)te=null;else if(t.spectators!==void 0&&t.spectators!==!0){let h=J(t.spectators);if(h===null)e.push("spectators: must be a boolean or an object with delayMs.");else{for(let p of Object.keys(h))p!=="delayMs"&&e.push(`spectators.${p}: unknown field.`);_(h.delayMs,0,3e4)?te={delayMs:h.delayMs}:e.push("spectators.delayMs: must be an integer from 0 to 30000.")}}let M=null;if(t.overlay!==void 0&&t.overlay!==null){let h=J(t.overlay);if(h===null)e.push("overlay: must be an object or null.");else{for(let p of Object.keys(h))["version","accent"].includes(p)||e.push(`overlay.${p}: unknown field.`);h.version!==1&&e.push("overlay.version: must be exactly 1."),h.accent!==void 0&&(typeof h.accent!="string"||!/^#[0-9a-fA-F]{6}$/.test(h.accent))&&e.push("overlay.accent: must be a six-digit hexadecimal color, such as #336699."),M={version:1,...typeof h.accent=="string"?{accent:h.accent}:{}}}}let X={};if(t.boards!==void 0){let h=J(t.boards);if(h===null)e.push("boards: must be an object of board ids.");else{Object.keys(h).length>_e&&e.push(`boards: at most ${_e} boards.`);for(let[p,z]of Object.entries(h)){let b=!0;Mt.test(p)||(e.push(`boards.${p}: invalid board id.`),b=!1);let S=J(z);if(S===null){e.push(`boards.${p}.source: must be "client" or "server".`);continue}for(let O of Object.keys(S))["source","label","periods","day"].includes(O)||e.push(`boards.${p}.${O}: unknown field.`);S.source!=="client"&&S.source!=="server"&&(e.push(`boards.${p}.source: must be "client" or "server".`),b=!1),S.day!==void 0&&S.day!=="submit"&&S.day!=="start"&&e.push(`boards.${p}.day: must be "submit" or "start".`),S.day==="start"&&S.source!=="server"&&e.push(`boards.${p}.day: start requires source "server".`);let A=me(S,"label",48,`boards.${p}`,e),L=["all-time"];S.periods!==void 0&&(!Array.isArray(S.periods)||S.periods.length<1||S.periods.length>2||S.periods.some(O=>O!=="daily"&&O!=="all-time")||new Set(S.periods).size!==S.periods.length?e.push(`boards.${p}.periods: must contain daily, all-time, or both without duplicates.`):L=[...S.periods]),b&&Object.defineProperty(X,p,{value:{source:S.source,periods:L,...S.day===void 0?{}:{day:S.day},...A===void 0?{}:{label:A}},enumerable:!0,configurable:!0,writable:!0})}}}let H=[];if(t.roles!==void 0)if(!Array.isArray(t.roles))e.push("roles: must be an array.");else{let h=new Set;for(let[p,z]of t.roles.entries()){let b=J(z);if(b===null){e.push(`roles[${p}]: must be an object.`);continue}for(let v of Object.keys(b))["id","min","max","label"].includes(v)||e.push(`roles[${p}].${v}: unknown field.`);let S=b.id,A=b.min,L=b.max,O=!0;typeof S!="string"||S.length>32||!De.test(S)?(e.push(`roles[${p}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`),O=!1):h.has(S)?(e.push(`roles[${p}].id: duplicate role ${S}.`),O=!1):h.add(S),_(A,0,T)||(e.push(`roles[${p}].min: must be an integer from 0 to ${T}.`),O=!1),L!==void 0&&!_(L,0,T)&&(e.push(`roles[${p}].max: must be an integer from 0 to ${T} when present.`),O=!1),typeof A=="number"&&typeof L=="number"&&A>L&&(e.push(`roles[${p}].max: must be greater than or equal to min.`),O=!1);let l=me(b,"label",32,`roles[${p}]`,e);O&&H.push({id:S,min:A,...L===void 0?{}:{max:L},...l===void 0?{}:{label:l}})}}let ie=null;if(t.teams!==void 0&&t.teams!==null){let h=J(t.teams);if(h===null)e.push("teams: must be null or an object with min and max.");else{for(let p of Object.keys(h))p!=="min"&&p!=="max"&&e.push(`teams.${p}: unknown field.`);_(h.min,2,T)||e.push(`teams.min: must be an integer from 2 to ${T}.`),_(h.max,2,T)||e.push(`teams.max: must be an integer from 2 to ${T}.`),_(h.min,2,T)&&_(h.max,2,T)&&(h.min>h.max?e.push("teams.max: must be greater than or equal to teams.min."):ie={min:h.min,max:h.max})}}let K="none";t.voice!==void 0&&(typeof t.voice!="string"||!Ct.has(t.voice)?e.push("voice: must be none, room, team, or proximity."):K=t.voice);let W=[];if(t.modes!==void 0)if(!Array.isArray(t.modes))e.push("modes: must be an array.");else{let h=new Set;for(let[p,z]of t.modes.entries()){let b=J(z);if(b===null){e.push(`modes[${p}]: must be an object.`);continue}for(let v of Object.keys(b))["id","players","lobby","matchmaking","execution","label","instructions"].includes(v)||e.push(`modes[${p}].${v}: unknown field.`);if(typeof b.id!="string"||b.id.length>32||!De.test(b.id)){e.push(`modes[${p}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`);continue}if(h.has(b.id)){e.push(`modes[${p}].id: duplicate mode ${b.id}.`);continue}h.add(b.id);let S={id:b.id};for(let[v,w]of[["label",48],["instructions",160]]){let R=me(b,v,w,`modes[${p}]`,e);R!==void 0&&(S[v]=R)}if(b.execution!==void 0&&(b.execution!=="local"&&b.execution!=="room"?e.push(`modes[${p}].execution: must be local or room.`):S.execution=b.execution),M!==null&&S.execution===void 0&&e.push(`modes[${p}].execution: is required with the standard overlay.`),b.players!==void 0){let v=`modes[${p}].players`,w=J(b.players);if(w===null)e.push(`${v}: must be an object with min and max.`);else{for(let R of Object.keys(w))R!=="min"&&R!=="max"&&e.push(`${v}.${R}: unknown field.`);_(w.min,1,T)||e.push(`${v}.min: must be an integer from 1 to ${T}.`),_(w.max,1,T)||e.push(`${v}.max: must be an integer from 1 to ${T}.`),_(w.min,1,T)&&_(w.max,1,T)&&(w.min>w.max?e.push(`${v}.max: must be greater than or equal to min.`):S.players={min:w.min,max:w.max})}}if(b.lobby!==void 0&&(typeof b.lobby!="boolean"?e.push(`modes[${p}].lobby: must be a boolean.`):S.lobby=b.lobby),S.execution==="local"){let v=S.players??I;(v.min!==1||v.max!==1)&&e.push(`modes[${p}].players: local execution requires min and max to be 1.`),(S.lobby??F)&&e.push(`modes[${p}].lobby: local execution requires false.`),b.matchmaking!==void 0&&e.push(`modes[${p}].matchmaking: local execution cannot use matchmaking.`)}if(b.matchmaking===void 0){W.push(S);continue}let A=J(b.matchmaking);if(A===null){e.push(`modes[${p}].matchmaking: must be an object.`);continue}for(let v of Object.keys(A))["key","timeoutMs","defaults"].includes(v)||e.push(`modes[${p}].matchmaking.${v}: unknown field.`);let L=!0,O=[];if(!Array.isArray(A.key)||A.key.length<1||A.key.length>8)e.push(`modes[${p}].matchmaking.key: must contain from 1 to 8 fields.`),L=!1;else for(let[v,w]of A.key.entries())typeof w!="string"||!Tt.test(w)?(e.push(`modes[${p}].matchmaking.key[${v}]: must be 1-32 lowercase letters, digits, or hyphens and start with a letter or digit.`),L=!1):O.includes(w)?(e.push(`modes[${p}].matchmaking.key[${v}]: duplicate field ${w}.`),L=!1):O.push(w);_(A.timeoutMs,1e3,3e5)||(e.push(`modes[${p}].matchmaking.timeoutMs: must be an integer from 1000 to 300000.`),L=!1);let l;if(A.defaults!==void 0){let v=J(A.defaults);if(v===null||Object.keys(v).length!==O.length||O.some(w=>!Object.hasOwn(v,w)))e.push(`modes[${p}].matchmaking.defaults: must contain exactly the declared key fields.`);else{l={};for(let[w,R]of Object.entries(v))!(typeof R=="string"&&R.length>=1&&R.length<=64&&/^[A-Za-z0-9_.:-]+$/.test(R))&&!Number.isSafeInteger(R)?e.push(`modes[${p}].matchmaking.defaults.${w}: must be a string of 1-64 characters or a safe integer.`):Object.defineProperty(l,w,{value:R,enumerable:!0})}}L&&W.push({...S,matchmaking:{...l===void 0?{}:{defaults:l},key:O,timeoutMs:A.timeoutMs}})}}return M!==null&&W.length===0&&e.push("modes: at least one explicit mode is required with the standard overlay."),e.length>0?{ok:!1,errori:e}:{ok:!0,manifest:{manifest:1,overlay:M,id:i,name:r,description:o,cover:u,card:m,icon:g,screenshots:d,tags:c,languages:k,language:C,platform:D,orientation:E,input:$,visibility:V,network:B,isolated:U,requires:G,players:I,lobby:F,persistent:P,spectators:te,boards:X,roles:H,teams:ie,voice:K,modes:W}}}function zt(n){return n.gpu!=="hardware"||n.memoryMb!==null&&n.memoryMb<=2048?"low":n.mobile||n.memoryMb!==null&&n.memoryMb<=4096||n.cores!==null&&n.cores<=4?"mid":"high"}function Ne(n){try{n?.getExtension("WEBGL_lose_context")?.loseContext()}catch{}}function Ot(n){let e;try{e=n.navigator}catch{e=void 0}let t=null;try{let s=e?.deviceMemory,a=typeof s=="number"?s*1024:NaN;Number.isFinite(a)&&(t=a)}catch{t=null}let i=null;try{let s=e?.hardwareConcurrency;typeof s=="number"&&Number.isFinite(s)&&(i=s)}catch{i=null}let r=!1;try{r=typeof e?.userAgentData?.mobile=="boolean"?e.userAgentData.mobile:/Android|iPhone|iPad|iPod|Mobile/i.test(e?.userAgent??"")}catch{r=!1}let o=!1;try{o=n.crossOriginIsolated===!0}catch{o=!1}return{webgl2:!1,webgpu:!1,wasm:!1,threads:!1,isolated:o,gpu:"none",memoryMb:t,cores:i,mobile:r}}async function $e(n,e=1500){let t=n??globalThis,i=Ot(t),r=Promise.resolve().then(()=>{try{let m=t.document?.createElement("canvas");if(m===void 0)return;let g=m.getContext("webgl2",{failIfMajorPerformanceCaveat:!0});if(g!==null){i.webgl2=!0,i.gpu="hardware",Ne(g);return}let d=m.getContext("webgl2");d!==null&&(i.webgl2=!0,i.gpu="software",Ne(d))}catch{i.webgl2=!1,i.gpu="none"}}),o=Promise.resolve().then(async()=>{let m;try{let g=t.navigator?.gpu;if(g===void 0)return;let d=await g.requestAdapter();if(d===null)return;m=await d.requestDevice(),i.webgpu=!0}catch{i.webgpu=!1}finally{try{m?.destroy?.()}catch{}}}),s=Promise.resolve().then(()=>{try{i.wasm=t.WebAssembly?.validate(new Uint8Array([0,97,115,109,1,0,0,0]))===!0}catch{i.wasm=!1}}),a=Promise.resolve().then(()=>{try{if(t.WebAssembly===void 0)return;new t.WebAssembly.Memory({initial:1,maximum:1,shared:!0}),i.threads=!0}catch{i.threads=!1}}),u;return await Promise.race([Promise.all([r,o,s,a]),new Promise(m=>{u=setTimeout(m,Math.max(0,e))})]),u!==void 0&&clearTimeout(u),{...i,tier:zt(i)}}function xe(n){return typeof n=="number"&&Number.isSafeInteger(n)&&n>0}function _t(n,e=null,t=null,i=null){let r=Re(n);if(!r.ok)throw new Error("The overlay manifest is invalid.");return{manifest:r.manifest,coverUrl:e,iconUrl:i,invite:t}}function N(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)?n:null}function Dt(n){let e=N(n),t=N(e?.configuration);return e?.v===1&&typeof e.epoch=="string"&&e.epoch.length>0&&e.epoch.length<=128&&t!==null&&(t.coverUrl===null||typeof t.coverUrl=="string")&&(t.iconUrl===null||typeof t.iconUrl=="string")&&(t.invite===null||typeof t.invite=="string"&&/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(t.invite))&&Re(t.manifest).ok}function Ge(n){return Dt(n)?{v:1,epoch:n.epoch,configuration:_t(n.configuration.manifest,n.configuration.coverUrl,n.configuration.invite,n.configuration.iconUrl)}:null}function Lt(n){let e=N(n);return e!==null&&Object.keys(e).length===4&&["top","right","bottom","left"].every(t=>typeof e[t]=="number"&&Number.isFinite(e[t])&&Number(e[t])>=0&&Number(e[t])<=1e5)}function ke(n){let e=N(n);return e!==null&&Object.keys(e).every(t=>["inputBlocked","reservedRects","safeArea","shortcutEnabled"].includes(t))&&(e.safeArea===void 0||Lt(e.safeArea))&&(e.shortcutEnabled===void 0||typeof e.shortcutEnabled=="boolean")&&typeof e.inputBlocked=="boolean"&&Array.isArray(e.reservedRects)&&e.reservedRects.length<=8&&e.reservedRects.every(t=>{let i=N(t);return i!==null&&Object.keys(i).length===4&&["x","y","width","height"].every(r=>typeof i[r]=="number"&&Number.isFinite(i[r])&&i[r]>=0&&i[r]<=1e5)})}function qe(n){let e=N(n),t=N(e?.args);if(e?.type!=="caisual:overlay"||e.v!==1||typeof e.epoch!="string"||e.epoch.length<1||e.epoch.length>128||typeof e.requestId!="string"||!(/^[1-9][0-9]{0,15}$/.test(e.requestId)&&Number.isSafeInteger(Number(e.requestId)))||t===null||Object.keys(e).some(o=>!["type","v","epoch","requestId","sessionId","op","args"].includes(o))||!(e.sessionId===void 0||e.sessionId===null||typeof e.sessionId=="string"&&/^[1-9][0-9]{0,15}$/.test(e.sessionId)))return!1;let i=(...o)=>Object.keys(t).every(s=>o.includes(s)),r=o=>typeof t[o]=="string"&&t[o].length>=1&&t[o].length<=64;switch(e.op){case"local.start":return i("mode")&&r("mode");case"room.create":return i("mode")&&(t.mode===null||r("mode"));case"room.join":return i("code")&&(t.code===void 0||r("code"));case"room.watch":return i("code")&&r("code");case"room.match":{let o=N(t.key);return i("mode","key")&&r("mode")&&(t.key===void 0||o!==null&&Object.keys(o).length<=8&&Object.values(o).every(s=>typeof s=="string"&&s.length>=1&&s.length<=64||typeof s=="number"&&Number.isSafeInteger(s)))}case"room.ready":return i("ready")&&typeof t.ready=="boolean";case"room.role":case"room.requestRole":return i("role")&&r("role");case"room.team":return i("team")&&Number.isInteger(t.team)&&t.team>=1&&t.team<=24;case"room.restart":case"room.start":case"session.cancel":case"session.leave":case"session.disconnect":case"session.resume":return i();case"voice.join":case"voice.leave":return i()&&typeof e.sessionId=="string";case"voice.mute":return i("muted")&&typeof t.muted=="boolean"&&typeof e.sessionId=="string";case"voice.setVolume":return i("playerId","volume")&&typeof e.sessionId=="string"&&typeof t.playerId=="string"&&t.playerId.length>0&&t.playerId.length<=128&&typeof t.volume=="number"&&Number.isFinite(t.volume)&&t.volume>=0&&t.volume<=1;case"overlay.view":return ke(t);default:return!1}}function le(n){if(typeof n!="string"||!/^\\d{4}-\\d{2}-\\d{2}$/.test(n))return!1;let e=Date.parse(`${n}T00:00:00Z`);return Number.isFinite(e)&&new Date(e).toISOString().slice(0,10)===n}function Je(n,e){let t=a=>a!==null&&typeof a=="object"&&!Array.isArray(a)?a:null,i=t(n);if(!i||!Array.isArray(i.standings))return null;let r=new Set(e),o=new Set,s=[];for(let a of i.standings){let u=t(a);!u||typeof u.playerId!="string"||!r.has(u.playerId)||o.has(u.playerId)||(o.add(u.playerId),s.push({playerId:u.playerId,...typeof u.score=="number"&&Number.isFinite(u.score)?{score:u.score}:{},...typeof u.rank=="number"&&Number.isSafeInteger(u.rank)&&u.rank>0?{rank:u.rank}:{}}))}return s.length?{standings:s,...Array.isArray(i.winners)?{winners:[...new Set(i.winners.filter(a=>typeof a=="string"&&o.has(a)))]}:{},...typeof i.draw=="boolean"?{draw:i.draw}:{},...typeof i.unit=="string"?{unit:i.unit}:{}}:null}var jt=["en","it","es","fr","de","pt"],Nt={gameUpdated:["This game was updated","Questo gioco \\xE8 stato aggiornato","Este juego se ha actualizado","Ce jeu a \\xE9t\\xE9 mis \\xE0 jour","Dieses Spiel wurde aktualisiert","Este jogo foi atualizado"],reloadGame:["Reload game","Ricarica il gioco","Recargar el juego","Recharger le jeu","Spiel neu laden","Recarregar o jogo"],gameLanguages:["Game languages","Lingue del gioco","Idiomas del juego","Langues du jeu","Spielsprachen","Idiomas do jogo"],loading:["Loading game...","Caricamento...","Cargando...","Chargement...","Spiel wird geladen...","Carregando..."],loadingSlow:["This game is taking longer than expected. You can wait a little longer or try again.","Il gioco ci sta mettendo pi\\xF9 del previsto. Puoi aspettare ancora un po\\u2019 o riprovare.","El juego est\\xE1 tardando m\\xE1s de lo esperado. Puedes esperar un poco m\\xE1s o volver a intentarlo.","Le jeu met plus de temps que pr\\xE9vu. Vous pouvez patienter encore un peu ou r\\xE9essayer.","Das Spiel braucht l\\xE4nger als erwartet. Du kannst noch etwas warten oder es erneut versuchen.","O jogo est\\xE1 demorando mais do que o esperado. Voc\\xEA pode esperar mais um pouco ou tentar novamente."],home:["Play","Gioca","Jugar","Jouer","Spielen","Jogar"],homeMenu:["Menu","Menu","Men\\xFA","Menu","Men\\xFC","Menu"],mode:["Mode","Modalit\\xE0","Modo","Mode","Modus","Modo"],play:["Play","Gioca","Jugar","Jouer","Spielen","Jogar"],friendsPlay:["Play with friends","Gioca con amici","Jugar con amigos","Jouer entre amis","Mit Freunden spielen","Jogar com amigos"],find:["Find players","Trova giocatori","Buscar jugadores","Trouver des joueurs","Spieler finden","Buscar jogadores"],join:["Join with code","Entra con codice","Entrar con c\\xF3digo","Rejoindre avec un code","Mit Code beitreten","Entrar com c\\xF3digo"],joinInvite:["Join this room","Entra in questa stanza","Entrar en est\\xE1 sala","Rejoindre cette salle","Diesem Raum beitreten","Entrar nest\\xE1 sala"],watch:["Watch a room","Guarda una stanza","Observar una sala","Regarder une salle","Raum ansehen","Assistir a uma sala"],resume:["Resume","Riprendi","Continuar","Reprendre","Fortsetzen","Continuar"],room:["Room","Stanza","Sala","Salle","Raum","Sala"],code:["Room code","Codice stanza","C\\xF3digo de sala","Code de salle","Raumcode","C\\xF3digo da sala"],copy:["Copy invite","Copia invito","Copiar invitaci\\xF3n","Copier le lien","Einladung kopieren","Copiar convite"],copied:["Invite copied","Invito copiato","Invitacion copiada","Lien copi\\xE9","Einladung kopiert","Convite copiado"],copyFailed:["Copy this link:","Copia questo link:","Copia este enlace:","Copiez ce lien :","Diesen Link kopieren:","Copie este link:"],joining:["Joining room...","Ingresso nella stanza...","Entrando en la sala...","Connexion \\xE0 la salle...","Raum wird betreten...","Entrando na sala..."],matching:["Finding your people...","Ricerca giocatori...","Buscando jugadores...","Recherche de joueurs...","Spieler werden gesucht...","Buscando jogadores..."],queue:["{n} / {max} players","{n} / {max} giocatori","{n} / {max} jugadores","{n} / {max} joueurs","{n} / {max} Spieler","{n} / {max} jogadores"],cancel:["Cancel","Annulla","Cancelar","Annuler","Abbrechen","Cancelar"],close:["Close","Chiudi","Cerrar","Fermer","Schlie\\xDFen","Fechar"],back:["Back","Indietro","Volver","Retour","Zur\\xFCck","Voltar"],ready:["Ready","Pronto","Listo","Pr\\xEAt","Bereit","Pronto"],unready:["Not ready","Non pronto","No listo","Pas pr\\xEAt","Nicht bereit","N\\xE3o pronto"],start:["Start","Inizia","Empezar","Commencer","Starten","Come\\xE7ar"],role:["Role","Ruolo","Rol","R\\xF4le","Rolle","Fun\\xE7\\xE3o"],team:["Team","Squadra","Equipo","\\xC9quipe","Team","Equipe"],host:["Host","Host","Anfitrion","H\\xF4te","Host","Anfitri\\xE3o"],you:["You","Tu","T\\xFA","Vous","Du","Voc\\xEA"],away:["Away","Assente","Ausente","Absent","Abwesend","Ausente"],needPlayers:["Waiting for more players","In attesa di giocatori","Esperando m\\xE1s jugadores","En attente de joueurs","Weitere Spieler fehlen","Esperando mais jogadores"],needReady:["Everyone needs to be ready","Tutti devono essere pronti","Todos deben estar listos","Tout le monde doit \\xEAtre pr\\xEAt","Alle m\\xFCssen bereit sein","Todos precisam estar prontos"],needRoles:["Fill the required roles","Completa i ruoli richiesti","Completa los roles","Compl\\xE9tez les r\\xF4les","Ben\\xF6tigte Rollen besetzen","Complete as fun\\xE7\\xF5es"],needTeams:["Choose the required teams","Scegli le squadre richieste","Elige los equipos","Choisissez les \\xE9quipes","Teams auswahlen","Escolha as equipes"],waitHost:["Waiting for the host","In attesa dell\'host","Esperando al anfitrion","En attente de l\\u2019h\\xF4te","Warten auf den Host","Esperando o anfitri\\xE3o"],starting:["Starting in","Si inizia tra","Empieza en","D\\xE9but dans","Start in","Come\\xE7a em"],playing:["Playing","In partita","Jugando","En jeu","Im Spiel","Jogando"],ended:["Game finished","Partita conclusa","Partida terminada","Partie termin\\xE9e","Spiel beendet","Partida encerrada"],rematchReady:["{n}/{max} ready","{n}/{max} pronti","{n}/{max} listos","{n}/{max} pr\\xEAts","{n}/{max} bereit","{n}/{max} prontos"],rematchStart:["Start rematch","Avvia rivincita","Iniciar revancha","Lancer la revanche","Revanche starten","Iniciar revanche"],won:["You won","Hai vinto","Has ganado","Vous avez gagn\\xE9","Du hast gewonnen","Voc\\xEA venceu"],lost:["You lost","Hai perso","Has perdido","Vous avez perdu","Du hast verloren","Voc\\xEA perdeu"],draw:["Draw","Pareggio","Empate","\\xC9galit\\xE9","Unentschieden","Empate"],standings:["Standings","Piazzamenti","Posiciones","R\\xE9sultats","Platzierungen","Coloca\\xE7\\xF5es"],points:["points","punti","puntos","points","Punkte","pontos"],time:["time","tempo","tiempo","temps","Zeit","tempo"],distance:["distance","distanza","distancia","distance","Distanz","dist\\xE2ncia"],again:["Play again","Gioca ancora","Jugar de nuevo","Rejouer","Erneut spielen","Jogar novamente"],newRoom:["New room. Share the new invite.","Nuova stanza. Condividi il nuovo invito.","Nueva sala. Comparte la invitaci\\xF3n.","Nouvelle salle. Partagez le lien.","Neuer Raum. Neue Einladung teilen.","Nova sala. Compartilhe o convite."],watching:["Watching","In osservazione","Observando","Spectateur","Zuschauen","Assistindo"],delay:["{n}s delay","Ritardo {n}s","Retraso de {n}s","Retard de {n}s","{n}s Verz\\xF6gerung","Atraso de {n}s"],exit:["Exit","Esci","Salir","Quitter","Verlassen","Sair"],leaveNow:["Leave for now","Esci per ora","Salir por ahora","Quitter pour le moment","Vor\\xFCbergehend verlassen","Sair por enquanto"],leaveRoom:["Leave room","Lascia la stanza","Abandonar sala","Abandonner la salle","Raum verlassen","Deixar a sala"],leaveHint:["Your room stays available for Resume.","La stanza resta disponibile con Riprendi.","Podr\\xE1s volver a est\\xE1 sala.","Vous pourrez reprendre cette salle.","Du kannst den Raum fortsetzen.","Voc\\xEA pode voltar a est\\xE1 sala."],temporaryHint:["The game continues. Rejoining may only be possible briefly.","La partita continua. Il rientro pu\\xF2 essere disponibile solo per poco.","La partida continua. Volver puede ser posible solo por poco tiempo.","La partie continue. Le retour peut \\xEAtre limit\\xE9.","Das Spiel l\\xE4uft weiter. R\\xFCckkehr nur kurz m\\xF6glich.","A partida continua. O retorno pode ser limitado."],abandonHint:["Leave room gives up your place.","Lascia la stanza libera il tuo posto.","Abandonar libera tu plaza.","Abandonner lib\\xE8re votre place.","Raum verlassen gibt deinen Platz frei.","Deixar a sala libera sua vaga."],reconnecting:["Reconnecting...","Riconnessione...","Reconectando...","Reconnexion...","Verbindung wird erneuert...","Reconectando..."],replaced:["Opened in another tab","Aperta in un\\u2019altra scheda","Abierta en otra pest\\xE1na","Ouverte dans un autre onglet","In anderem Tab ge\\xF6ffnet","Aberta em outra aba"],error:["Something went wrong. Try again.","Qualcosa non va. Riprova.","Algo sali\\xF3 mal. Reintenta.","Une erreur est survenue. R\\xE9essayez.","Etwas ist schiefgelaufen. Erneut versuchen.","Algo deu errado. Tente novamente."],noRoom:["This room is no longer available.","Questa stanza non \\xE8 pi\\xF9 disponibile.","Esta sala ya no est\\xE1 disponible.","Cette salle n\'est plus disponible.","Dieser Raum ist nicht mehr verf\\xFCgbar.","Esta sala n\\xE3o est\\xE1 mais disponivel."],full:["This room is full.","La stanza \\xE8 piena.","La sala est\\xE1 llena.","Cette salle est pleine.","Dieser Raum ist voll.","Esta sala est\\xE1 cheia."],noMatch:["No match this time. Try again.","Nessun gruppo trovato. Riprova.","No hay grupo. Reintenta.","Aucun groupe trouv\\xE9. R\\xE9essayez.","Keine Gruppe gefunden. Erneut versuchen.","Nenhum grupo encontrado. Tente novamente."],invalidCode:["Enter a six-character room code.","Inserisci un codice di sei caratteri.","Escribe un c\\xF3digo de seis caracteres.","Entrez un code de six caracteres.","Sechsstelligen Raumcode eingeben.","Digite um c\\xF3digo de seis caracteres."],refused:["The room did not accept that change.","La stanza ha rifiutato la modifica.","La sala rechaz\\xF3 el cambio.","La salle a refus\\xE9 ce changement.","Der Raum hat die \\xC4nderung abgelehnt.","A sala recusou a altera\\xE7\\xE3o."],unavailable:["Unavailable right now","Non disponibile ora","No disponible ahora","Indisponible pour le moment","Derzeit nicht verf\\xFCgbar","Indisponivel agora"],offline:["Connection unavailable. Try again.","Connessione non disponibile. Riprova.","Sin conexi\\xF3n. Reintenta.","Connexion indisponible. R\\xE9essayez.","Keine Verbindung. Erneut versuchen.","Sem conex\\xE3o. Tente novamente."],saveFailed:["Keep the room code. Resume could not be saved.","Conserva il codice. Riprendi non \\xE8 stato salvato.","Guarda el c\\xF3digo. No se pudo guardar el regreso.","Gardez le code. La reprise ne peut pas \\xEAtre enregistr\\xE9e.","Raumcode aufbewahren. Fortsetzen nicht gespeichert.","Guarde o c\\xF3digo. O retorno n\\xE3o foi salvo."],boards:["Leaderboard","Classifica","Clasificaci\\xF3n","Classement","Bestenliste","Classifica\\xE7\\xE3o"],board:["Board","Classifica","Tabla","Classement","Bestenliste","Tabela"],daily:["Daily","Giornaliera","Diaria","Du jour","T\\xE4glich","Di\\xE1ria"],allTime:["All time","Di sempre","Hist\\xF3rica","Tous les temps","Gesamt","Geral"],accounts:["Accounts","Account","Cuentas","Comptes","Konten","Contas"],guests:["Guests","Ospiti","Invitados","Invit\\xE9s","G\\xE4ste","Visitantes"],category:["Category","Categoria","Categoria","Cat\\xE9gorie","Kategorie","Categoria"],period:["Period","Periodo","Per\\xEDodo","P\\xE9riode","Zeitraum","Per\\xEDodo"],rank:["Rank","Posizione","Puesto","Rang","Platz","Posicao"],score:["Score","Punteggio","Puntos","Score","Punkte","Pontos"],verified:["Verified","Verificato","Verificado","V\\xE9rifi\\xE9","Verifiziert","Verificado"],own:["Your best","Il tuo record","Tu record","Votre record","Dein Rekord","Seu recorde"],empty:["No scores yet","Nessun punteggio","A\\xFAn no hay puntos","Aucun score","Noch keine Punkte","Ainda sem pontos"],saving:["Saving score...","Salvataggio punteggio...","Guardando puntos...","Enregistrement du score...","Punkte werden gespeichert...","Salvando pontos..."],saved:["Your best is on the board","Il tuo record \\xE8 in classifica","Tu record est\\xE1 en la tabla","Votre record est au classement","Dein Rekord ist eingetragen","Seu recorde est\\xE1 na tabela"],bestAlready:["Your best is already on the board","Il tuo record era gi\\xE0 in classifica","Tu record ya estaba en la tabla","Votre record est d\\xE9j\\xE0 au classement","Dein Rekord ist bereits eingetragen","Seu recorde j\\xE1 est\\xE1 na tabela"],refresh:["Refresh","Aggiorna","Actualizar","Actualiser","Aktualisieren","Atualizar"],refreshHint:["Score not visible yet. Refresh to check.","Punteggio non ancora visibile. Aggiorna per controllare.","Puntos a\\xFAn no visibles. Actualiza.","Score pas encore visible. Actualisez.","Punkte noch nicht sichtbar. Aktualisieren.","Pontos ainda n\\xE3o visiveis. Atualize."],friends:["Friends & party","Amici e gruppo","Amigos y grupo","Amis et groupe","Freunde & Gruppe","Amigos e grupo"],localCrew:["Friends and party are unavailable in local preview.","Amici e gruppo non disponibili in anteprima locale.","Amigos y grupo no disponibles en la vista local.","Amis et groupe indisponibles en aper\\xE7u local.","Freunde und Gruppe in lokaler Vorschau nicht verf\\xFCgbar.","Amigos e grupo indispon\\xEDveis na pr\\xE9via local."],loginCrew:["Sign in on Caisual to use friends and party.","Accedi a Caisual per amici e gruppo.","Inicia sesion para amigos y grupo.","Connectez-vous pour utiliser amis et groupe.","F\\xFCr Freunde und Gruppe bei Caisual anmelden.","Entre no Caisual para amigos e grupo."],online:["Online","Online","En linea","En ligne","Online","Online"],noFriends:["No friends online","Nessun amico online","Sin amigos en linea","Aucun ami en ligne","Keine Freunde online","Nenhum amigo online"],createParty:["Create party","Crea gruppo","Crear grupo","Cr\\xE9er un groupe","Gruppe erstellen","Criar grupo"],inviteParty:["Invite to party","Invita nel gruppo","Invitar al grupo","Inviter au groupe","In Gruppe einladen","Convidar para o grupo"],leaveParty:["Leave party","Lascia gruppo","Salir del grupo","Quitter le groupe","Gruppe verlassen","Sair do grupo"],accept:["Accept","Accetta","Aceptar","Accepter","Annehmen","Aceitar"],decline:["Decline","Rifiuta","Rechazar","Refuser","Ablehnen","Recusar"],follow:["Join them","Raggiungi","Unirse","Rejoindre","Beitreten","Juntar-se"],voice:["Voice","Voce","Voz","Voix","Sprache","Voz"],voiceJoin:["Join voice","Entra in voce","Unirse a voz","Activer la voix","Sprachchat beitreten","Entrar na voz"],voiceLeave:["Leave voice","Esci dalla voce","Salir de voz","Quitter la voix","Sprachchat verlassen","Sair da voz"],voiceMute:["Mute","Disattiva microfono","Silenciar","Couper le micro","Stummschalten","Silenciar"],voiceUnmute:["Unmute","Attiva microfono","Activar micr\\xF3fono","Activer le micro","Mikrofon aktivieren","Ativar microfone"],voiceOff:["Voice off","Voce disattivata","Voz desactivada","Voix d\\xE9sactiv\\xE9e","Sprachchat aus","Voz desativada"],voiceJoining:["Joining voice...","Connessione voce...","Conectando voz...","Connexion vocale...","Sprachchat verbindet...","Conectando voz..."],voiceOn:["Voice connected","Voce connessa","Voz conectada","Voix connect\\xE9e","Sprachchat verbunden","Voz conectada"],voiceMuted:["Muted","Microfono disattivato","Silenciado","Micro coup\\xE9","Stumm","Silenciado"],voiceMic:["Mic on","Microfono attivo","Micr\\xF3fono activo","Micro actif","Mikrofon an","Microfone ativo"],voiceListening:["Listening only","Solo ascolto","Solo escucha","\\xC9coute seule","Nur zuh\\xF6ren","Somente ouvindo"],voiceSpeaking:["Speaking","Sta parlando","Hablando","Parle","Spricht","Falando"],voicePeers:["Voice participants","Partecipanti in voce","Participantes de voz","Participants vocaux","Sprachteilnehmer","Participantes de voz"],voiceEmpty:["No one else in voice yet.","Nessun altro in voce per ora.","A\\xFAn no hay nadie m\\xE1s en voz.","Personne d\\u2019autre en voix pour le moment.","Noch niemand im Sprachchat.","Ningu\\xE9m mais na voz ainda."],voiceVolume:["Volume for {name}","Volume di {name}","Volumen de {name}","Volume de {name}","Lautst\\xE4rke f\\xFCr {name}","Volume de {name}"],voiceUnavailable:["Join a room with voice to use these controls.","Entra in una stanza con voce per usare questi controlli.","Entra en una sala con voz para usar estos controles.","Rejoignez une salle vocale pour utiliser ces commandes.","Diese Steuerung braucht einen Raum mit Sprachchat.","Entre em uma sala com voz para usar estes controles."],voiceWatch:["Voice is unavailable while watching.","La voce non e\' disponibile in osservazione.","La voz no est\\xE1 disponible al observar.","La voix est indisponible en observation.","Beim Zuschauen ist kein Sprachchat verf\\xFCgbar.","A voz n\\xE3o est\\xE1 dispon\\xEDvel ao assistir."],voiceDenied:["Microphone permission denied. Allow it in your browser, then try again.","Permesso microfono negato. Consenti l\'accesso nel browser e riprova.","Permiso de micr\\xF3fono denegado. Act\\xEDvalo en el navegador e int\\xE9ntalo de nuevo.","Acc\\xE8s au micro refus\\xE9. Autorisez-le dans le navigateur, puis r\\xE9essayez.","Mikrofonzugriff verweigert. Im Browser erlauben und erneut versuchen.","Permiss\\xE3o do microfone negada. Permita no navegador e tente novamente."],voiceUnsupported:["Voice is not supported in this browser.","Questo browser non supporta la voce.","Este navegador no admite voz.","Ce navigateur ne prend pas en charge la voix.","Dieser Browser unterst\\xFCtzt keinen Sprachchat.","Este navegador n\\xE3o oferece suporte a voz."],voiceFailed:["Voice could not connect. Try again.","Connessione voce non riuscita. Riprova.","No se pudo conectar la voz. Int\\xE9ntalo de nuevo.","Connexion vocale impossible. R\\xE9essayez.","Sprachverbindung fehlgeschlagen. Erneut versuchen.","N\\xE3o foi poss\\xEDvel conectar a voz. Tente novamente."],voicePeerGone:["This participant has left voice.","Questo partecipante e\' uscito dalla voce.","Este participante sali\\xF3 de voz.","Ce participant a quitt\\xE9 la voix.","Diese Person hat den Sprachchat verlassen.","Este participante saiu da voz."],shortcut:["Shift+Tab shortcut","Scorciatoia Shift+Tab","Atajo Shift+Tab","Raccourci Maj+Tab","Umschalt+Tab-Kurzbefehl","Atalho Shift+Tab"],menu:["Caisual menu","Menu Caisual","Menu Caisual","Menu Caisual","Caisual-Menu","Menu Caisual"],retry:["Retry","Riprova","Reintentar","R\\xE9essayer","Erneut versuchen","Tentar novamente"]},se=n=>Object.fromEntries(Object.entries(Nt).map(([e,t])=>[e,t[n]])),sn={en:se(0),it:se(1),es:se(2),fr:se(3),de:se(4),pt:se(5)};function Be(n){let e=q(n);return e&&jt.includes(e.split("-")[0])?e:"en"}function Ue(n,e,t="/"){let i,r=t.match(/^\\/rt\\/[^/]+\\/[1-9][0-9]*\\//)?.[0]??"/";return()=>i??(i=(async()=>{let o={};try{let s=await n(`${r}__caisual/text/${encodeURIComponent(e)}.json`);if(s.ok){let a=await s.json();Ve(a)&&(o=a)}}catch{}return(s,a={})=>Object.hasOwn(o,s)?o[s].replace(/\\{([^{}]+)\\}/g,(m,g)=>Object.hasOwn(a,g)?String(a[g]):m):s})())}function f(n,e,t={}){return Object.assign(new Error(e),{name:"CaisualError",code:n,...t})}function j(){return f("offline","Caisual services are unavailable.")}function ce(n){return typeof n=="object"&&n!==null&&"code"in n?n.code:null}var Fe="caisual-session-v1";function He(n){let e=N(n);return!e||typeof e.code!="string"||!/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(e.code)||!(e.mode===void 0||e.mode===null||typeof e.mode=="string")?null:{version:1,code:e.code,mode:typeof e.mode=="string"?e.mode:null,updatedAt:typeof e.updatedAt=="number"&&Number.isFinite(e.updatedAt)?e.updatedAt:0}}function Ke(n,e){let t=null,i=!1,r=Promise.resolve(),o=async()=>{let a={version:1,imported:!0,resume:t};return r=r.catch(()=>{}).then(async()=>{try{await n.set(Fe,a),i=!1}catch(u){throw i=!0,u}finally{e()}}),r},s=(async()=>{try{let a=await n.get(Fe),u=N(a);if(u?.version===1&&u.imported===!0)t=He(u.resume);else{let m=await n.get("resume");t=He(m),(a!==null||m!==null)&&await o()}}catch{i=!0}e()})();return{loaded:s,get value(){return t===null?null:{...t}},get error(){return i},async set(a){await s,t=a,e(),await o()}}}function Z(n,e){for(let t of n)try{t(e)}catch{}}function We(n,e=null,t=n.connected){let i=e?.manifest.overlay?.version===1,r=e?.manifest,o={kind:"idle"},s=!1,a=0,u=0,m=null,g=null,d=null,c=null,y=[],k=!1,C="",D={inputBlocked:i,reservedRects:[],safeArea:{top:0,right:0,bottom:0,left:0}},E=new Set,$=new Set,V=new Set,B=new Set,U=new Set,G=new Set,I=null,F=()=>({local:!0,rooms:t,overlay:i,requestRole:o.kind==="room"&&o.room.metadata.configuration?.requestRole===!0});function P(){if(o.kind!=="room"||!r||r.voice==="none")return null;let l=o.room,v=l.voice;return!v||v.mode==="none"||l.players.find(w=>w.id===l.you)?.role==="spectator"?null:{mode:v.mode,state:v.state,mic:v.mic,muted:v.muted,speaking:v.speaking,peers:v.peers.map(({id:w,mic:R,muted:x,speaking:re,volume:ne})=>({id:w,mic:R,muted:x,speaking:re,volume:ne}))}}function te(){let l=o.kind==="room"||o.kind==="watch"?o.room:null,v=l?.metadata.configuration,w=l?Je(l.result,l.players.map(x=>x.id)):null,R=r&&l&&(l.mode===null||r.modes.some(x=>x.id===l.mode))?je(r,l.mode):{players:{min:1,max:1},lobby:!1};return{kind:m??(o.kind==="idle"?s?"home":"boot":o.kind),id:o.kind==="idle"?null:o.id,mode:m?g:o.kind==="local"?o.mode:l?.mode??null,localStatus:o.kind==="local"?o.status:null,ready:s,capabilities:F(),room:l?{...l.metadata.rematch?.keepSetup||l.metadata.rematch?.autoStart?{rematch:l.metadata.rematch}:{},code:l.code,mode:l.mode,status:l.status,host:l.host,you:o.kind==="room"?o.room.you:null,players:l.players.map(x=>({id:x.id,name:x.name,guest:x.guest,role:x.role,team:x.team,ready:x.ready,connected:x.connected})),...w?{result:w}:{},countdownAt:l.countdownAt,connection:l.connection,closedCode:l.metadata.closedCode,limits:{...v?.players??R.players},lobby:v?.lobby??R.lobby,persistent:v?.persistent??r?.persistent??!1,delayMs:o.kind==="watch"?o.room.delayMs:null,requestRole:v?.requestRole??!1}:null,voice:m?null:P(),waiting:d?{...d}:null,resume:I?.value??null,resumeError:I?.error??!1}}function M(){if(k)return;let l=te(),v=JSON.stringify(l);v!==C&&(C=v,Z(V,l))}function X(){Z(E,{...o}),M()}function H(){if(o.kind!=="room")throw f("no_room","There is no active player room.");return o.room}function ie(){let l=H();if(l.players.find(v=>v.id===l.you)?.role==="spectator")throw f("spectator","Spectators cannot use voice controls.");if(!r||r.voice==="none"||l.voice.mode==="none")throw f("voice_disabled","Voice is disabled for this room.");return l.voice}function K(){a++,c?.abort(),c=null,m=null,d=null,M()}function W(l){y.splice(0).forEach(v=>v()),(o.kind==="room"||o.kind==="watch")&&(l?o.room.disconnect():o.room.leave()),o={kind:"idle"},X()}async function h(l){I?.value?.code===l&&await I.set(null).catch(()=>{})}async function p(l,v,w){if(w!==a||k)throw l.leave(),f("cancelled","The operation was cancelled.");W(!1),o=v?{kind:"watch",room:l,id:String(++u)}:{kind:"room",room:l,id:String(++u)};let R=l;if(y=[R.onPlayers(M),R.onMetadata(()=>{R.connection==="disconnected"&&(o.kind==="room"||o.kind==="watch")&&o.room===R?(y.splice(0).forEach(x=>x()),!v&&R.metadata.closedCode===1e3&&h(R.code),o={kind:"idle"},X()):M()}),R.onStatus(()=>{M(),!v&&R.connection==="ended"&&h(R.code)})],!v){let x=l,re=o.id;x.voice&&y.push(x.voice.onState(M),x.voice.onPeers(M)),y.push(x.onError(ne=>Z(U,{sessionId:re,error:{...ne}}))),y.push(x.onScoreQueued(ne=>Z(G,{...ne})));for(let ne of x.queuedScores)Z(G,{...ne})}return m=null,d=null,X(),!v&&I&&R.connection!=="ended"&&await I.set({version:1,code:R.code,mode:R.mode,updatedAt:n.time.now()}).catch(()=>{}),l}async function z(l,v,w,R=!1){K();let x=a;c=new AbortController,m=l,g=v,M();try{let re=await w(c.signal,x);if(await p(re,R,x),x!==a||k)throw f("cancelled","The operation was cancelled.");return re}finally{x===a&&(m=null,d=null,c=null,M())}}let b=n.room,S=b.onError(l=>{!i||k||(l.code==="version_mismatch"?b.reload():l.code==="version_outdated"&&Z(U,{sessionId:o.kind==="idle"?null:o.id,error:l}))}),A=i?{invited:b.invited,reload:()=>b.reload(),onError:l=>b.onError(l),create(l){return r&&pe(r,l.mode)?Promise.reject(f("invalid_request","Local modes cannot create rooms.")):z("attaching",l.mode,()=>b.create(l))},join(l){return z("attaching",null,()=>b.join(l))},watch(l){return z("attaching",null,()=>b.watch(l),!0)},match(l){return r&&pe(r,l.mode)?Promise.reject(f("invalid_request","Local modes cannot use matchmaking.")):z("matching",l.mode,(v,w)=>{let R=()=>{a===w&&K()};return l.signal?.addEventListener("abort",R,{once:!0}),l.signal?.aborted&&R(),b.match({...l,signal:v,onWaiting(x){w===a&&(d={...x},M(),l.onWaiting?.(x))}}).finally(()=>l.signal?.removeEventListener("abort",R))})}}:b;return i&&(I=Ke(n.save,M)),{session:{get current(){return{...o}},get capabilities(){return F()},onChange(l){return E.add(l),Z(new Set([l]),{...o}),()=>{E.delete(l)}},ready(){k||s||(s=!0,M())},finish(){if(o.kind==="room"||o.kind==="watch")throw f("not_local","Only a local session can be finished by the client.");o.kind==="local"&&(o={...o,status:"ended"},X())}},overlay:{open(l){if(!["home","room","invite","friends","voice","boards"].includes(l))throw f("invalid_request","Unknown overlay panel.");i&&Z(B,l)},onChange(l){return $.add(l),Z(new Set([l]),structuredClone(D)),()=>{$.delete(l)}}},rooms:A,snapshot:te,serverTime:()=>o.kind==="room"||o.kind==="watch"?o.room.serverTime():n.time.now(),onState(l){return V.add(l),l(te()),()=>{V.delete(l)}},onOpen(l){return B.add(l),()=>{B.delete(l)}},onError(l){return U.add(l),()=>{U.delete(l)}},onScore(l){return G.add(l),()=>{G.delete(l)}},async execute(l){if(!i)throw f("overlay_disabled","This game uses its own room flow.");if(l.op==="overlay.view"){if(!ke(l.args))throw f("invalid_request","The overlay geometry is invalid.");if(D={...structuredClone(l.args),safeArea:{top:0,right:0,bottom:0,left:0,...l.args.safeArea}},typeof document<"u")for(let[v,w]of Object.entries(D.safeArea))document.documentElement.style.setProperty(`--caisual-safe-${v}`,`${w}px`);Z($,structuredClone(D));return}if(l.sessionId!==void 0&&l.sessionId!==(o.kind==="idle"?null:o.id))throw f("session_replaced","The active session changed.");if(l.op.startsWith("voice.")&&l.sessionId!==(o.kind==="idle"?null:o.id))throw f("session_replaced","The active session changed.");if(!s)throw f("game_not_ready","The game is still loading.");switch(l.op){case"local.start":{if(!r||!pe(r,l.args.mode))throw f("invalid_mode","This is not a local mode.");K();let v=a;if(o.kind==="room"&&await h(o.room.code),v!==a||k)throw f("cancelled","The operation was cancelled.");W(!1),o={kind:"local",id:String(++u),mode:l.args.mode,status:"playing"},X();return}case"room.create":await A.create(l.args);return;case"room.join":await A.join(l.args.code);return;case"room.watch":await A.watch(l.args.code);return;case"room.match":{let v=r?.modes.find(R=>R.id===l.args.mode),w=l.args.key??v?.matchmaking?.defaults;if(!w)throw f("invalid_request","Matchmaking needs a complete key.");await A.match({mode:l.args.mode,key:w});return}case"voice.join":{let v=H();if(await ie().join(),o.kind!=="room"||o.room!==v)throw f("session_replaced","The active session changed.");M();return}case"voice.mute":ie().mute(l.args.muted),M();return;case"voice.leave":ie().leave(),M();return;case"voice.setVolume":{let v=ie();if(!v.peers.some(w=>w.id===l.args.playerId))throw f("voice_peer_missing","This voice participant is no longer available.");v.setVolume(l.args.playerId,l.args.volume),M();return}case"room.ready":H().ready(l.args.ready);return;case"room.role":H().setRole(l.args.role);return;case"room.requestRole":await H().requestRole(l.args.role);return;case"room.team":H().setTeam(l.args.team);return;case"room.start":H().start();return;case"room.restart":H().restart();return;case"session.cancel":K();return;case"session.resume":{await z("attaching",null,async v=>{if(await I?.loaded,v.aborted)throw f("cancelled","The operation was cancelled.");if(!I?.value)throw f("no_resume","There is no saved room.");return b.join(I.value.code)});return}case"session.disconnect":{K();let v=a;if(o.kind==="room"&&o.room.connection!=="ended"&&I&&await I.set({version:1,code:o.room.code,mode:o.room.mode,updatedAt:n.time.now()}),v!==a||k)throw f("cancelled","The operation was cancelled.");W(!0);return}case"session.leave":{K();let v=a;if(o.kind==="room"&&await h(o.room.code),v!==a||k)throw f("cancelled","The operation was cancelled.");W(!1);return}}},dispose(){S(),K(),W(!0),k=!0,E.clear(),$.clear(),V.clear(),B.clear(),G.clear(),U.clear()}}}function Ze(n,e,t){let i=!0,r=!1,o=e.onChange(a=>{i=a.shortcutEnabled!==!1,r=a.inputBlocked}),s=a=>{let u=a.target;!i||r||a.repeat||a.key!=="Tab"||!a.shiftKey||a.ctrlKey||a.altKey||a.metaKey||u?.closest?.(\'input,textarea,select,[contenteditable="true"]\')||(a.preventDefault(),a.stopImmediatePropagation(),t())};return n.addEventListener("keydown",s,!0),()=>{o(),n.removeEventListener("keydown",s,!0)}}function Qe(n,e,t){let i=!1,r=0,o=0,s=0,a=new Map,u=d=>{if(!i)try{n.postMessage(d)}catch{}},m=[...e.configuration.manifest.overlay&&typeof window<"u"?[Ze(window,t.overlay,()=>u({type:"caisual:overlay-shortcut",v:1,epoch:e.epoch}))]:[],t.onState(d=>u({type:"caisual:overlay-state",v:1,epoch:e.epoch,seq:++r,serverTime:t.serverTime(),state:d})),t.onOpen(d=>u({type:"caisual:overlay-open",v:1,epoch:e.epoch,panel:d})),t.onError(({sessionId:d,error:c})=>u({type:"caisual:overlay-error",v:1,epoch:e.epoch,sessionId:d,error:c})),t.onScore(d=>u({type:"caisual:overlay-score",v:1,epoch:e.epoch,score:d}))],g=d=>{let c=N(d.data);if(c?.type!=="caisual:overlay"||c.epoch!==e.epoch||i)return;let y={type:"caisual:overlay-response",v:1,epoch:e.epoch,requestId:typeof c.requestId=="string"?c.requestId:""};if(!qe(c)){u({...y,ok:!1,error:{code:"invalid_request",message:"The overlay request is invalid."}});return}let k=JSON.stringify([c.op,c.args,c.sessionId]),C=a.get(c.requestId);if(C){C.fingerprint!==k?u({...y,ok:!1,error:{code:"duplicate_request",message:"The request id was already used."}}):C.response.then(u);return}if(Number(c.requestId)<=o||s>=32){u({...y,ok:!1,error:{code:"stale_request",message:"The request is stale or too many requests are pending."}});return}o=Number(c.requestId),s++;let D=Promise.resolve().then(()=>t.execute(c)).then(()=>({...y,ok:!0}),E=>({...y,ok:!1,error:{code:typeof N(E)?.code=="string"?N(E).code:"internal_error",message:E instanceof Error?E.message:"The operation could not be completed."}}));a.set(c.requestId,{fingerprint:k,response:D}),D.then(E=>{if(s--,u(E),a.size>64)for(let $ of a.keys())Number($)<o-64&&a.delete($)})};return n.addEventListener("message",g),n.start(),()=>{i=!0,n.removeEventListener("message",g),m.forEach(d=>d()),t.dispose(),a.clear()}}async function $t(n){let e={};try{e=await n.json()}catch{}return f(typeof e.error?.code=="string"?e.error.code:n.status===401?"invalid_ticket":"internal_error",typeof e.error?.message=="string"?e.error.message:`The request failed with status ${n.status}.`,{currentVersion:e.error?.currentVersion,roomVersion:e.error?.roomVersion})}function fe(n,e,t,i){async function r(o,s,a,u){let m=new Headers({Authorization:`Bearer ${a}`}),g;if(u!==void 0){m.set("Content-Type","application/json");try{g=JSON.stringify(u)}catch{throw f("invalid_request","The value must be valid JSON.")}}try{return await t(new URL(e+o,n),{method:s,headers:m,body:g,credentials:"omit"})}catch{throw j()}}return async function(s,a,u,m=!1){let g;try{g=m?await i.rinnova():await i.ottieni()}catch{throw j()}let d=await r(s,a,g,u);if(d.status===401){try{g=await i.rinnova()}catch{throw j()}d=await r(s,a,g,u)}if(!d.ok)throw await $t(d);try{return await d.json()}catch{throw f("internal_error","The service returned an invalid response.")}}}function Ye(n,e,t){let i=fe(n,"/api/kit",e,t);return{me:()=>i("/me","GET"),saveSet:(r,o)=>i(`/saves/${encodeURIComponent(r)}`,"PUT",{value:o}),async saveGet(r){try{return(await i(`/saves/${encodeURIComponent(r)}`,"GET")).value}catch(o){if(ce(o)==="not_found")return null;throw o}},async saveRemove(r){await i(`/saves/${encodeURIComponent(r)}`,"DELETE")},async saveList(){return(await i("/saves","GET")).saves},async boardSubmit(r,o,s){let a=await i("/scores","POST",{board:r,score:o,daily:s});return{accepted:!0,best:a.best,rank:a.rank,day:a.day,verified:a.verified}},async boardTop(r,o){if(o.day!==void 0&&(!le(o.day)||o.daily===!1))throw f("invalid_request","day must be a real UTC date and cannot be combined with daily: false.");let s=new URLSearchParams;o.day!==void 0&&s.set("day",o.day),o.daily&&s.set("daily","1"),o.limit!==void 0&&s.set("limit",String(o.limit)),o.guests&&s.set("guests","1");let a=s.size===0?"":`?${s.toString()}`,{day:u,entries:m,me:g}=await i(`/scores/${encodeURIComponent(r)}${a}`,"GET");return{day:u,entries:m,me:g}}}}function ae(n){return(Math.floor(n/864e5)+1)*864e5}function he(n,e,t){let i=new Set,r={...n},o,s=!1;function a(){!i.size||o!==void 0||s||(o=setTimeout(u,Math.max(0,Math.min(2147483647,r.expiresAt-e()))),o.unref?.())}async function u(){o=void 0,s=!0;try{let m=await t(),g=m.day!==r.day;if(r={...m},g)for(let d of[...i])try{d({...m})}catch{}}catch{}finally{s=!1,r.expiresAt<=e()&&(r.expiresAt=e()+3e4),a()}}return{...n,random:Xe(n.seed),rng:()=>Xe(n.seed),onChange(m){return i.add(m),a(),()=>{i.delete(m),!i.size&&o!==void 0&&(clearTimeout(o),o=void 0)}}}}function Ce(n){return new Date(n).toISOString().slice(0,10)}async function Pe(n,e,t){let i=new TextEncoder().encode(`caisual:${n}:${e}`),r=new Uint8Array(await t.digest("SHA-256",i));return(r[0]??0)*16777216+((r[1]??0)<<16)+((r[2]??0)<<8)+(r[3]??0)>>>0}function Xe(n){let e=n>>>0;return()=>{e=e+1831565813>>>0;let t=e;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}}function ge(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)?n:null}function et(n,e){return ge(n)?.type===e}function Gt(n){if(typeof n!="string")return null;try{let e=new URL(n);return e.origin===n&&(e.protocol==="https:"||e.protocol==="http:")?n:null}catch{return null}}function tt(n,e,t=3e3){return new Promise(i=>{let r=!1,o=globalThis.crypto.randomUUID(),s=g=>{r||(r=!0,n.removeEventListener("message",u),n.clearTimeout(m),i(g))},a=()=>{n.parent.postMessage({type:"caisual:ready",instance:o,overlayVersion:1},e)},u=g=>{if(g.origin!==e||g.source!==n.parent)return;if(et(g.data,"caisual:ready?")){a();return}if(!et(g.data,"caisual:hello"))return;let d=ge(g.data),c=g.ports[0];if(typeof d?.ticket!="string"||!xe(d.n)||c===void 0)return;c.start();let y=Ge(d.overlay),k=C=>Array.isArray(C)?C.map(q).filter(D=>D!==null):void 0;s({...y?{overlay:y}:{},...q(d.language)?{language:q(d.language)}:{},uiLanguage:q(d.uiLanguage)??void 0,languagePreferences:k(d.languagePreferences),gameLanguages:k(d.gameLanguages),ticket:d.ticket,n:d.n,live:Gt(d.live),invite:typeof d.invite=="string"?d.invite:null,porta:c})};n.addEventListener("message",u);let m=n.setTimeout(()=>s(null),t);a()})}function qt(n){let e=n.split(".")[1];if(e===void 0)return null;let t=e.replace(/-/g,"+").replace(/_/g,"/").padEnd(Math.ceil(e.length/4)*4,"=");try{let i=ge(JSON.parse(globalThis.atob(t)));return typeof i?.exp=="number"&&Number.isFinite(i.exp)?i.exp*1e3:null}catch{return null}}function Jt(n,e,t,i){return new Promise((r,o)=>{let s=!1,a=g=>{s||(s=!0,n.removeEventListener("message",u),e.clearTimeout(m),g===null?o(new Error("Ticket refresh timed out.")):r(g))},u=g=>{let d=ge(g.data),c=d?.aud===void 0?"portal":d.aud;d?.type==="caisual:ticket"&&c===i&&typeof d.ticket=="string"&&a(d.ticket)};n.addEventListener("message",u);let m=e.setTimeout(()=>a(null),t);try{n.postMessage(i==="live"?{type:"caisual:ticket",aud:"live"}:{type:"caisual:ticket"})}catch{a(null)}})}function Ae(n,e,t,i,r=3e3,o="portal"){let s=n,a=null,u=()=>{if(a!==null)return a;let g=Jt(e,t,r,o).then(d=>(s=d,d)).finally(()=>{a===g&&(a=null)});return a=g,g};return{async ottieni(){if(s===null)return u();let m=qt(s);return m!==null&&m-i()<3e4?u():s},rinnova:u}}var Te=.02,it=300,Bt=200,nt=3e3,Ut=1e4,Ft=[1e3,2e3,4e3];function ot(n){return Number.isNaN(n)?1:Math.min(1,Math.max(0,n))}function Ht(n){let e=globalThis,t=e.AudioContext??e.webkitAudioContext;return typeof RTCPeerConnection>"u"||typeof MediaStream>"u"||t===void 0||typeof navigator>"u"||navigator.mediaDevices?.getUserMedia===void 0||typeof document>"u"?null:{...n,creaPeerConnection:i=>new RTCPeerConnection(i),getUserMedia:i=>navigator.mediaDevices.getUserMedia(i),creaAudioContext:()=>new t,creaAudioElement:()=>document.createElement("audio"),creaMediaStream:i=>new MediaStream(i)}}var ve=class{constructor(e,t,i){this.contesto=e;this.modeCorrente="none";this.stateCorrente="off";this.mutedCorrente=!1;this.speakingCorrente=!1;this.roster=[];this.gains=new Map;this.volumi=new Map;this.speakingPeers=new Map;this.ultimoAudio=new Map;this.zeroDa=new Map;this.timerZero=new Map;this.ascoltatoriPeers=new Set;this.ascoltatoriState=new Set;this.richieste=new Map;this.riproduzioni=new Map;this.sfuAttive=new Map;this.midGiocatori=new Map;this.negati=new Set;this.mesh=new Map;this.stream=null;this.tracciaMic=null;this.audioContext=null;this.analyser=null;this.peerSfu=null;this.sessioneSfu=null;this.connessioneSfuAttesa=!1;this.trasporto=null;this.intervalloAudio=null;this.timerConnessione=null;this.cancellaAttesaConnessione=null;this.timerRiconnessione=null;this.ultimoAudioMic=Number.NEGATIVE_INFINITY;this.sequenzaRichieste=0;this.generazione=0;this.tentativoRiconnessione=0;this.desiderata=!1;this.micDesiderato=!0;this.promessaIngresso=null;this.negoziazione=Promise.resolve();this.dipendenze=i??Ht(t)}get mode(){return this.modeCorrente}get state(){return this.stateCorrente}get mic(){return this.stateCorrente==="on"&&this.tracciaMic!==null}get muted(){return this.mutedCorrente}get speaking(){return this.speakingCorrente}get peers(){return this.copiaPeers()}async join(e={}){if(this.stateCorrente==="on")return;if(this.stateCorrente==="joining"){this.promessaIngresso!==null&&await this.promessaIngresso;return}if(this.stateCorrente==="reconnecting"&&this.desiderata)return;let t=this.scegliMic(e);this.verificaIngresso(t),this.micDesiderato=t,this.desiderata=!0,this.tentativoRiconnessione=0,this.aggiornaState("joining");let i=++this.generazione,r=this.completaIngresso(i);this.promessaIngresso=r;try{await r}finally{this.promessaIngresso===r&&(this.promessaIngresso=null)}}async completaIngresso(e){try{await this.entra(e)}catch(t){if(e!==this.generazione)return;throw this.desiderata=!1,this.chiudiRisorse(),this.aggiornaState("off"),this.mappaErrore(t)}}leave(){let e=this.desiderata||this.stateCorrente!=="off";this.desiderata=!1,this.generazione++,this.fermaRiconnessione(),e&&this.contesto.connessa()&&this.richiedi({t:"voice",op:"stop"}).catch(()=>{}),this.rifiutaRichieste(f("offline","Voice has stopped.")),this.chiudiRisorse(),this.aggiornaState("off")}mute(e=!0){if(this.stateCorrente!=="on"||this.tracciaMic===null)throw f("not_publishing","Join voice before changing mute.");this.mutedCorrente=e,this.tracciaMic.enabled=!e,e&&(this.speakingCorrente=!1),this.notificaPeers(),this.richiedi({t:"voice",op:"mute",muted:e}).catch(()=>{})}setVolume(e,t){let i=ot(t);this.volumi.set(e,i),this.aggiornaGuadagno(e),this.notificaPeers()}onPeers(e){return this.ascoltatoriPeers.add(e),()=>{this.ascoltatoriPeers.delete(e)}}onState(e){return this.ascoltatoriState.add(e),()=>{this.ascoltatoriState.delete(e)}}ricevi(e){if("r"in e){let t=this.richieste.get(e.r);t!==void 0&&(this.richieste.delete(e.r),"error"in e?t.reject(f(e.error.code,e.error.message)):t.resolve(e));return}if(e.op==="roster"){this.negati.clear(),this.modeCorrente=e.mode;let t=new Set(e.peers.map(i=>i.id));this.roster=[...e.peers.map(i=>({...i,mic:!0})),...e.listeners.flatMap(i=>t.has(i)?[]:[{id:i,mic:!1,muted:!0}])];for(let i of this.roster)i.muted&&this.speakingPeers.set(i.id,!1);this.pulisciPeerAssenti(),this.contesto.rosterPronto(),this.notificaPeers(),this.accodaRiconciliazione();return}if(e.op==="gain"){this.negati.clear();for(let[t,i]of Object.entries(e.gains))this.gains.set(t,ot(i)),this.aggiornaZero(t),this.aggiornaGuadagno(t);this.notificaPeers(),this.accodaRiconciliazione();return}if(e.op==="closed"){for(let t of e.mids){let i=this.midGiocatori.get(t);if(i===void 0)continue;let r=this.sfuAttive.get(i);r?.mid===t&&!this.riproduzioni.has(i)&&r.receiver?.track.stop(),r?.mid===t&&this.sfuAttive.delete(i),this.midGiocatori.delete(t),this.scollegaTraccia(i),this.negati.add(i)}this.notificaPeers();return}e.op==="signal"&&this.riceviSegnale(e.from,e.data)}giocatoriCambiati(){this.negati.clear();let e=new Set(this.contesto.giocatori().map(t=>t.id));for(let t of this.gains.keys()){if(e.has(t))continue;this.gains.delete(t),this.zeroDa.delete(t);let i=this.timerZero.get(t);i!==void 0&&this.dipendenze?.clearTimeout(i),this.timerZero.delete(t),this.aggiornaGuadagno(t)}this.notificaPeers(),this.accodaRiconciliazione()}socketDisconnesso(){this.sequenzaRichieste=0,this.rifiutaRichieste(f("offline","The room is reconnecting.")),this.desiderata&&(this.generazione++,this.chiudiRisorse(),this.tentativoRiconnessione=0,this.aggiornaState("reconnecting"))}socketRiconnesso(){this.sequenzaRichieste=0,this.desiderata&&this.stateCorrente==="reconnecting"&&this.programmaRiconnessione()}termina(){this.desiderata=!1,this.generazione++,this.fermaRiconnessione(),this.rifiutaRichieste(f("offline","The room connection ended.")),this.chiudiRisorse(),this.aggiornaState("off")}scegliMic(e){return e.mic!==void 0?e.mic:this.contesto.giocatori().find(i=>i.id===this.contesto.you())?.role!=="spectator"}verificaIngresso(e=this.micDesiderato){if(!this.contesto.connessa())throw f("offline","The room is not connected.");if(this.modeCorrente==="none")throw f("voice_disabled","Voice is disabled for this room.");if(this.contesto.giocatori().find(i=>i.id===this.contesto.you())?.role==="spectator"&&e)throw f("spectator","Spectators cannot publish voice.");if(this.dipendenze===null)throw f("unsupported","Voice is not supported in this browser.")}async entra(e){this.verificaIngresso();let t=this.richiediDipendenze(),i=t.creaAudioContext();if(this.audioContext=i,this.micDesiderato){let o;try{o=await t.getUserMedia({audio:!0})}catch(a){throw this.permessoNegato(a)?f("permission_denied","Microphone permission was denied."):f("voice_error","The microphone could not be opened.")}try{this.controllaGenerazione(e)}catch(a){for(let u of o.getTracks())u.stop();throw a}let s=o.getAudioTracks()[0];if(s===void 0)throw f("voice_error","The microphone has no audio track.");this.stream=o,this.tracciaMic=s,s.enabled=!this.mutedCorrente,this.preparaAnalizzatore(o)}try{await i.resume()}catch{}this.controllaGenerazione(e);let r=await this.richiedi({t:"voice",op:"ice"});if(this.controllaGenerazione(e),r.op!=="ice")throw f("voice_error","The voice service returned an invalid response.");if(this.modeCorrente=r.mode,r.mode==="none")throw f("voice_disabled","Voice is disabled for this room.");this.trasporto=r.transport,r.transport==="sfu"?await this.entraSfu(r.iceServers,e):await this.richiedi({t:"voice",op:"publish",mic:this.micDesiderato}),this.micDesiderato&&this.mutedCorrente&&await this.richiedi({t:"voice",op:"mute",muted:!0}),this.controllaGenerazione(e),this.tentativoRiconnessione=0,this.aggiornaState("on"),this.avviaMisuraAudio();for(let o of this.gains.keys())this.aggiornaZero(o);this.accodaRiconciliazione()}async entraSfu(e,t){let i=this.richiediDipendenze().creaPeerConnection({iceServers:e,bundlePolicy:"max-bundle"});this.peerSfu=i,i.ontrack=o=>{let s=o.transceiver.mid,a=s===null?void 0:this.midGiocatori.get(s);a!==void 0&&this.collegaTraccia(a,o.track,o.receiver)},this.osservaCaduta(i);let r;if(this.micDesiderato){let o=i.addTransceiver(this.richiediMic(),{direction:"sendonly"}),s=await i.createOffer();await i.setLocalDescription(s),this.controllaGenerazione(t);let a=o.mid,u=i.localDescription?.sdp;if(a===null||u===void 0)throw f("voice_error","The voice connection could not create an offer.");r=await this.richiedi({t:"voice",op:"session",sdp:u,mid:a})}else r=await this.richiedi({t:"voice",op:"session"});if(r.op!=="session")throw f("voice_error","The voice service returned an invalid response.");if(this.sessioneSfu=r.session,this.micDesiderato){if(r.sdp===null)throw f("voice_error","The voice service returned an invalid response.");await i.setRemoteDescription({type:"answer",sdp:r.sdp}),await this.attendiConnessione(i,t),this.connessioneSfuAttesa=!0;return}if(r.sdp!==null)throw f("voice_error","The voice service returned an invalid response.");this.publisherDesiderati().length>0&&await this.riconciliaSfu()}attendiConnessione(e,t){if(e.connectionState==="connected")return Promise.resolve();let i=this.richiediDipendenze();return new Promise((r,o)=>{let s=()=>{e.removeEventListener("connectionstatechange",a),this.timerConnessione!==null&&i.clearTimeout(this.timerConnessione),this.timerConnessione=null,this.cancellaAttesaConnessione=null},a=()=>{t!==this.generazione?(s(),o(f("offline","Voice was stopped."))):e.connectionState==="connected"?(s(),r()):(e.connectionState==="failed"||e.connectionState==="closed")&&(s(),o(f("voice_error","The voice connection failed.")))};e.addEventListener("connectionstatechange",a),this.cancellaAttesaConnessione=()=>{s(),o(f("offline","Voice was stopped."))},this.timerConnessione=i.setTimeout(()=>{s(),o(f("voice_error","The voice connection timed out."))},Ut)})}accodaRiconciliazione(){this.stateCorrente==="on"&&(this.negoziazione=this.negoziazione.then(async()=>{this.stateCorrente==="on"&&(this.trasporto==="sfu"?await this.riconciliaSfu():this.trasporto==="mesh"&&this.riconciliaMesh())}).catch(()=>this.avviaRiconnessione()))}async riconciliaSfu(){let e=this.sessioneSfu,t=this.peerSfu;if(e===null||t===null)return;let i=new Map(this.publisherDesiderati().map(m=>[m.id,m])),r=[];for(let[m,g]of this.sfuAttive){let d=i.get(m);d!==void 0&&d.session===g.session&&d.track===g.track||(r.push(g),this.riproduzioni.has(m)||g.receiver?.track.stop(),this.sfuAttive.delete(m),this.midGiocatori.delete(g.mid),this.scollegaTraccia(m))}r.length>0&&await this.richiedi({t:"voice",op:"close",session:e,mids:r.map(m=>m.mid)});let o=[...i.values()].filter(m=>!this.sfuAttive.has(m.id));if(o.length===0)return;let s;try{s=await this.richiedi({t:"voice",op:"subscribe",session:e,tracks:o.map(m=>({session:m.session,track:m.track}))})}catch(m){if(ce(m)!=="not_allowed")throw m;for(let g of o)this.negati.add(g.id);return}if(s.op!=="subscribe")throw f("voice_error","The voice service returned an invalid response.");for(let m of s.tracks){let g=o.find(d=>d.session===m.session&&d.track===m.track);m.error==="not_allowed"&&g!==void 0&&this.negati.add(g.id),!(m?.mid===null||m?.mid===void 0||m.error!==null||g===void 0)&&(this.midGiocatori.set(m.mid,g.id),this.sfuAttive.set(g.id,{session:g.session,track:g.track,mid:m.mid,receiver:null}))}await t.setRemoteDescription({type:"offer",sdp:s.sdp});let a=await t.createAnswer();await t.setLocalDescription(a);let u=t.localDescription?.sdp;if(u===void 0)throw f("voice_error","The voice answer is missing.");await this.richiedi({t:"voice",op:"answer",session:e,sdp:u}),this.connessioneSfuAttesa||(await this.attendiConnessione(t,this.generazione),this.connessioneSfuAttesa=!0)}riconciliaMesh(){let e=new Map(this.peerDesiderati().map(t=>[t.id,t]));for(let[t,i]of this.mesh)e.has(t)||(i.pc.close(),this.mesh.delete(t),this.scollegaTraccia(t));for(let t of e.values())this.mesh.has(t.id)||this.creaMesh(t)}creaMesh(e){let t=e.id,i=this.richiediDipendenze().creaPeerConnection(),r={pc:i,makingOffer:!1,ignoreOffer:!1,settingRemoteAnswer:!1,polite:this.contesto.you()>t,receiver:null};this.mesh.set(t,r),i.onicecandidate=o=>{o.candidate!==null&&this.inviaSegnale(t,{kind:"candidate",candidate:o.candidate.toJSON()})},r.polite||(i.onnegotiationneeded=()=>{this.offriMesh(t,r)}),i.ontrack=o=>{r.receiver=o.receiver,this.collegaTraccia(t,o.track,o.receiver)},this.osservaCaduta(i),this.micDesiderato?i.addTransceiver(this.richiediMic(),{direction:e.mic?"sendrecv":"sendonly"}):i.addTransceiver("audio",{direction:"recvonly"})}async offriMesh(e,t){try{t.makingOffer=!0;let i=await t.pc.createOffer();await t.pc.setLocalDescription(i);let r=t.pc.localDescription?.sdp;r!==void 0&&await this.inviaSegnale(e,{kind:"offer",sdp:r})}finally{t.makingOffer=!1}}async riceviSegnale(e,t){if(this.trasporto!=="mesh"||this.stateCorrente!=="on")return;let i=this.peerDesiderati().find(s=>s.id===e);if(i===void 0)return;this.mesh.has(e)||this.creaMesh(i);let r=this.mesh.get(e);if(r===void 0||typeof t!="object"||t===null||Array.isArray(t))return;let o=t;try{if(o.kind==="candidate"){r.ignoreOffer||await r.pc.addIceCandidate(o.candidate);return}if(o.kind!=="offer"&&o.kind!=="answer"||typeof o.sdp!="string")return;let s=!r.makingOffer&&(r.pc.signalingState==="stable"||r.settingRemoteAnswer),a=o.kind==="offer"&&!s;if(r.ignoreOffer=!r.polite&&a,r.ignoreOffer)return;if(r.settingRemoteAnswer=o.kind==="answer",await r.pc.setRemoteDescription({type:o.kind,sdp:o.sdp}),r.settingRemoteAnswer=!1,o.kind==="offer"){let u=await r.pc.createAnswer();await r.pc.setLocalDescription(u);let m=r.pc.localDescription?.sdp;m!==void 0&&await this.inviaSegnale(e,{kind:"answer",sdp:m})}}catch{this.avviaRiconnessione()}}async inviaSegnale(e,t){try{await this.richiedi({t:"voice",op:"signal",to:e,data:t})}catch(i){if(ce(i)!=="not_allowed")throw i;this.mesh.get(e)?.pc.close(),this.mesh.delete(e),this.scollegaTraccia(e),this.negati.add(e)}}peerDesiderati(){let e=this.contesto.you(),t=this.contesto.giocatori(),i=t.find(r=>r.id===e);return this.roster.filter(r=>{if(r.id===e||this.negati.has(r.id)||!this.micDesiderato&&!r.mic)return!1;if(this.modeCorrente==="team"){let o=t.find(s=>s.id===r.id);if(i?.role!=="spectator"&&o?.team!==i?.team)return!1}return!0})}publisherDesiderati(){return this.peerDesiderati().filter(e=>{if(!e.mic)return!1;let t=this.zeroDa.get(e.id);return t===void 0||this.richiediDipendenze().ora()-t<nt})}aggiornaZero(e){let t=this.dipendenze;if(t===null)return;let i=this.timerZero.get(e);if(i!==void 0&&t.clearTimeout(i),this.timerZero.delete(e),(this.gains.get(e)??1)>0){this.zeroDa.delete(e);return}this.zeroDa.has(e)||this.zeroDa.set(e,t.ora());let r=t.ora()-(this.zeroDa.get(e)??t.ora()),o=t.setTimeout(()=>{this.timerZero.delete(e),this.accodaRiconciliazione()},Math.max(0,nt-r));this.timerZero.set(e,o)}collegaTraccia(e,t,i){this.scollegaTraccia(e);let r=this.richiediDipendenze(),o=r.creaMediaStream([t]),s=this.richiediAudioContext().createMediaStreamSource(o),a=this.richiediAudioContext().createGain();s.connect(a),a.connect(this.richiediAudioContext().destination);let u=null;try{u=this.richiediAudioContext().createAnalyser(),u.fftSize=256,s.connect(u)}catch{u=null}let m=r.creaAudioElement();m.srcObject=o,m.muted=!0,m.playsInline=!0,m.play().catch(()=>{}),this.riproduzioni.set(e,{source:s,gain:a,analyser:u,audio:m,track:t,receiver:i});let g=this.sfuAttive.get(e);g!==void 0&&(g.receiver=i),this.aggiornaGuadagno(e)}scollegaTraccia(e){let t=this.riproduzioni.get(e);t!==void 0&&(t.source.disconnect(),t.gain.disconnect(),t.analyser?.disconnect(),t.track.stop(),t.audio.pause(),t.audio.srcObject=null,this.riproduzioni.delete(e),this.speakingPeers.delete(e),this.ultimoAudio.delete(e))}aggiornaGuadagno(e){let t=this.riproduzioni.get(e);t!==void 0&&(t.gain.gain.value=(this.volumi.get(e)??1)*(this.gains.get(e)??1))}preparaAnalizzatore(e){let t=this.richiediAudioContext(),i=t.createAnalyser();i.fftSize=256,t.createMediaStreamSource(e).connect(i),this.analyser=i}avviaMisuraAudio(){let e=this.richiediDipendenze();this.intervalloAudio!==null&&e.clearInterval(this.intervalloAudio),this.intervalloAudio=e.setInterval(()=>this.misuraAudio(),Bt)}misuraAudio(){let e=this.dipendenze;if(e===null)return;let t=!1;this.analyser!==null&&(t=this.livelloAnalizzatore(this.analyser)>Te),t&&(this.ultimoAudioMic=e.ora());let i=!this.mutedCorrente&&e.ora()-this.ultimoAudioMic<=it;i!==this.speakingCorrente&&(this.speakingCorrente=i,this.notificaPeers());let r=!1;for(let o of this.copiaPeers()){let s=this.riproduzioni.get(o.id);this.livelloAnalizzatore(s?.analyser??null)>Te?this.ultimoAudio.set(o.id,e.ora()):(s?.analyser===null||s?.analyser===void 0)&&(s?.receiver?.getSynchronizationSources?.()??[]).some(m=>(m.audioLevel??0)>Te)&&this.ultimoAudio.set(o.id,e.ora());let a=!o.muted&&e.ora()-(this.ultimoAudio.get(o.id)??0)<=it;(this.speakingPeers.get(o.id)??!1)!==a&&(this.speakingPeers.set(o.id,a),r=!0)}r&&this.notificaPeers()}livelloAnalizzatore(e){let t=e;if(t?.getFloatTimeDomainData===void 0)return 0;let i=new Float32Array(t.fftSize);return t.getFloatTimeDomainData(i),Math.sqrt(i.reduce((r,o)=>r+o*o,0)/Math.max(1,i.length))}copiaPeers(){let e=this.contesto.you(),t=this.contesto.giocatori(),i=t.find(r=>r.id===e);return this.roster.flatMap(r=>{if(r.id===e)return[];if(this.modeCorrente==="team"){let o=t.find(s=>s.id===r.id);if(i?.role!=="spectator"&&o?.team!==i?.team)return[]}return[{id:r.id,mic:r.mic,muted:r.muted,speaking:r.mic&&!r.muted&&(this.speakingPeers.get(r.id)??!1),volume:this.volumi.get(r.id)??1,gain:this.gains.get(r.id)??1}]})}pulisciPeerAssenti(){let e=new Set(this.roster.map(t=>t.id));for(let t of this.speakingPeers.keys())e.has(t)||this.speakingPeers.delete(t);for(let t of this.zeroDa.keys()){if(e.has(t))continue;this.zeroDa.delete(t);let i=this.timerZero.get(t);i!==void 0&&this.dipendenze?.clearTimeout(i),this.timerZero.delete(t)}}osservaCaduta(e){e.addEventListener("connectionstatechange",()=>{this.stateCorrente==="on"&&(e.connectionState==="failed"||e.connectionState==="disconnected")&&this.avviaRiconnessione()})}avviaRiconnessione(){!this.desiderata||this.stateCorrente==="reconnecting"||(this.generazione++,this.rifiutaRichieste(f("voice_error","The voice connection was restarted.")),this.chiudiRisorse(),this.tentativoRiconnessione=0,this.aggiornaState("reconnecting"),this.programmaRiconnessione())}programmaRiconnessione(){if(!this.desiderata||!this.contesto.connessa()||this.timerRiconnessione!==null||this.stateCorrente!=="reconnecting")return;let e=Ft[this.tentativoRiconnessione];if(e===void 0){this.desiderata=!1,this.aggiornaState("off");return}this.tentativoRiconnessione++,this.timerRiconnessione=this.richiediDipendenze().setTimeout(()=>{this.timerRiconnessione=null;let t=++this.generazione;this.entra(t).catch(()=>{t!==this.generazione||!this.desiderata||(this.chiudiRisorse(),this.aggiornaState("reconnecting"),this.programmaRiconnessione())})},e)}fermaRiconnessione(){this.timerRiconnessione===null||this.dipendenze===null||(this.dipendenze.clearTimeout(this.timerRiconnessione),this.timerRiconnessione=null)}chiudiRisorse(){let e=this.dipendenze;if(this.cancellaAttesaConnessione?.(),this.cancellaAttesaConnessione=null,e!==null){this.intervalloAudio!==null&&e.clearInterval(this.intervalloAudio),this.timerConnessione!==null&&e.clearTimeout(this.timerConnessione);for(let t of this.timerZero.values())e.clearTimeout(t)}this.intervalloAudio=null,this.timerConnessione=null,this.timerZero.clear();for(let t of[...this.riproduzioni.keys()])this.scollegaTraccia(t);this.peerSfu?.close(),this.peerSfu=null;for(let t of this.mesh.values())t.pc.close();this.mesh.clear(),this.sfuAttive.clear(),this.midGiocatori.clear(),this.negati.clear();for(let t of this.stream?.getTracks()??[])t.stop();this.stream=null,this.tracciaMic=null,this.analyser=null,this.audioContext?.close().catch(()=>{}),this.audioContext=null,this.sessioneSfu=null,this.connessioneSfuAttesa=!1,this.trasporto=null,this.speakingCorrente=!1,this.ultimoAudioMic=Number.NEGATIVE_INFINITY,this.speakingPeers.clear(),this.ultimoAudio.clear(),this.negoziazione=Promise.resolve()}richiedi(e){if(!this.contesto.connessa())return Promise.reject(f("offline","The room is reconnecting."));let t=++this.sequenzaRichieste;return new Promise((i,r)=>{this.richieste.set(t,{resolve:i,reject:r});try{this.contesto.invia({...e,r:t})}catch(o){this.richieste.delete(t),r(o)}})}rifiutaRichieste(e){for(let t of this.richieste.values())t.reject(e);this.richieste.clear()}aggiornaState(e){if(e!==this.stateCorrente){this.stateCorrente=e;for(let t of this.ascoltatoriState)try{t(e)}catch{}}}notificaPeers(){let e=this.copiaPeers();for(let t of this.ascoltatoriPeers)try{t(e)}catch{}}controllaGenerazione(e){if(e!==this.generazione||!this.desiderata)throw f("offline","Voice was stopped.")}richiediDipendenze(){if(this.dipendenze===null)throw f("unsupported","Voice is not supported.");return this.dipendenze}richiediMic(){if(this.tracciaMic===null)throw f("voice_error","The microphone is not ready.");return this.tracciaMic}richiediAudioContext(){if(this.audioContext===null)throw f("voice_error","Audio is not ready.");return this.audioContext}permessoNegato(e){return typeof e=="object"&&e!==null&&"name"in e&&(e.name==="NotAllowedError"||e.name==="SecurityError")}mappaErrore(e){if(typeof e=="object"&&e!==null&&"code"in e){let t=e.code;return t==="voice_disabled"||t==="permission_denied"||t==="unsupported"||t==="spectator"||t==="offline"||t==="voice_error"?e:f("voice_error","Voice could not be started.")}return f("voice_error","Voice could not be started.")}};var ee=1,rt=[1e3,2e3,4e3,8e3],Kt=6e4,Wt=5e3,Zt=2e4,Qt=500,Yt=2e3,Xt=new Set([4003,4004,4005,4006,4008,4009]);function Y(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)?n:null}function st(n){let e=Y(n);return e!==null&&typeof e.roomId=="string"&&typeof e.code=="string"&&typeof e.join=="string"&&typeof e.url=="string"}function ei(n){let e=Y(n);return e!==null&&typeof e.roomId=="string"&&typeof e.code=="string"&&typeof e.watch=="string"&&typeof e.url=="string"}function ti(n){let e=Y(n),t=Y(e?.players);return e!==null&&typeof e.url=="string"&&Number.isInteger(e.timeoutMs)&&e.timeoutMs>=1e3&&e.timeoutMs<=3e5&&t!==null&&Number.isInteger(t.min)&&Number.isInteger(t.max)&&t.min>=1&&t.max>=t.min}function Q(n){return JSON.parse(JSON.stringify(n))}function ii(n,e){let t=Q(n);for(let i of e){if(i.path.length===0){if(i.op!=="set")return{ok:!1};t=Q(i.value);continue}let r=t,o=i.path;for(let a=0;a<o.length-1;a++){let u=o[a];if(Array.isArray(r)){if(typeof u!="number"||u>=r.length)return{ok:!1};r=r[u]}else{let m=Y(r);if(m===null||typeof u!="string"||!Object.hasOwn(m,u))return{ok:!1};r=m[u]}}let s=o.at(-1);if(Array.isArray(r)){if(i.op!=="set"||typeof s!="number"||s>=r.length)return{ok:!1};r[s]=Q(i.value)}else{let a=Y(r);if(a===null||typeof s!="string")return{ok:!1};if(i.op==="del"){if(!Object.hasOwn(a,s))return{ok:!1};delete a[s]}else Object.defineProperty(a,s,{configurable:!0,enumerable:!0,value:Q(i.value),writable:!0})}}return{ok:!0,state:t}}function ni(n){let e=fe(n.liveOrigin,"",n.fetcher,n.biglietto),t=async(s,a,u,m)=>{try{return await e(s,a,{...Y(u),n:n.n},m)}catch(g){if(g instanceof Error&&"code"in g&&["version_outdated","version_mismatch"].includes(String(g.code))){let d=Y(u),c=typeof d?.code=="string"?d.code.toUpperCase().replace(/[\\s-]/g,""):void 0,y=typeof d?.roomId=="string"?d.roomId:void 0;n.onVersionError?.(g,g.code==="version_mismatch"?{code:c,roomId:y,watch:s==="/rooms/watch"}:void 0)}throw g}};async function i(s,a,u=!1){let m=await t(s,"POST",a,u);if(!st(m))throw f("internal_error","The room service returned an invalid response.");return m}async function r(s){let a=await t("/match","POST",{mode:s.mode,key:s.key});if(!ti(a))throw f("internal_error","The matchmaking service returned an invalid response.");return a}async function o(s,a=!1){let u=await t("/rooms/watch","POST",s,a);if(!ei(u))throw f("internal_error","The room service returned an invalid response.");return u}return{create:s=>i("/rooms",{mode:s}),joinCode:s=>i("/rooms/join",{code:s}),joinRoom:s=>i("/rooms/join",{roomId:s},!0),watchCode:s=>o({code:s}),watchRoom:s=>o({roomId:s},!0),match:r,flush:s=>t(`/rooms/${encodeURIComponent(s)}/flush`,"POST")}}var ye=class{constructor(e,t,i,r,o,s,a=!1){this.roomId=e;this.codice=t;this.dipendenze=r;this.api=o;this.segnalaStanza=s;this.spettatore=a;this.meta={host:null,mode:null,countdownAt:null,configuration:null,connection:"connecting",closedCode:null};this.metaListeners=new Set;this.connectionListeners=new Set;this.scoreListeners=new Set;this.scores=[];this.errorListeners=new Set;this.roleId=0;this.roleRequests=new Map;this.statoPubblico=null;this.statoSincronizzato=null;this.tickCorrente=0;this.tickRateCorrente=0;this.latenzaCorrente=null;this.ultimoInput=null;this.inputInviato=null;this.timerInput=null;this.ultimoInvioGioco=-1/0;this.inviiGioco=[];this.seedCorrente=0;this.statusCorrente="lobby";this.giocatoriCorrenti=[];this.youCorrente="";this.hostCorrente=null;this.resultCorrente=null;this.delaySpettatore=0;this.socket=null;this.seq=0;this.scartoOrario=0;this.timerPing=null;this.intervalloPing=null;this.timerRiconnessione=null;this.timerFlush=null;this.flushInCorso=!1;this.flushRichiesto=!1;this.ritardoIndice=0;this.tempoRiconnessione=0;this.resyncRichiesto=!1;this.terminata=!1;this.lasciata=!1;this.prontaRisolta=!1;this.welcomeRicevuto=!1;this.rosterRicevuto=!1;this.timerRoster=null;this.risolviPronta=()=>{};this.rifiutaPronta=()=>{};this.ascoltatoriStato=new Set;this.ascoltatoriGiocatori=new Set;this.ascoltatoriStatus=new Set;this.ascoltatoriMessaggi=new Set;this.promessaPronta=new Promise((u,m)=>{this.risolviPronta=u,this.rifiutaPronta=m}),this.voice=new ve({invia:u=>this.invia(u),connessa:()=>this.socket?.readyState===ee&&this.welcomeRicevuto&&!this.terminata&&!this.lasciata,you:()=>this.youCorrente,giocatori:()=>this.copiaGiocatori(),rosterPronto:()=>{this.rosterRicevuto=!0,this.risolviProntaSePossibile()}},r,r.voce),a&&(this.rosterRicevuto=!0),this.apri(i)}get mode(){return this.meta.mode}get countdownAt(){return this.meta.countdownAt}get connection(){return this.meta.connection}get metadata(){return structuredClone(this.meta)}get queuedScores(){return structuredClone(this.scores)}onMetadata(e){return this.metaListeners.add(e),()=>this.metaListeners.delete(e)}onConnection(e){return this.connectionListeners.add(e),()=>this.connectionListeners.delete(e)}onError(e){return this.errorListeners.add(e),()=>this.errorListeners.delete(e)}onScoreQueued(e){return this.scoreListeners.add(e),()=>this.scoreListeners.delete(e)}metadataChanged(e){let t=this.meta.connection;this.meta={...this.meta,...e},this.notifica(this.metaListeners,this.metadata),t!==this.meta.connection&&this.notifica(this.connectionListeners,this.meta.connection)}initialMetadata(e){this.metadataChanged({host:e.host,mode:e.mode,countdownAt:e.countdownAt??null,rematch:e.rematch??null,configuration:e.configuration??null,connection:"connected",closedCode:null})}requestRole(e){if(typeof e!="string"||e.length<1||e.length>32)return Promise.reject(f("invalid_role","The role is not valid."));if(this.connection!=="connected"||this.status!=="playing"||!this.meta.configuration?.requestRole)return Promise.reject(f("role_change_unavailable","Roles cannot be requested right now."));if(this.roleRequests.size>=8)return Promise.reject(f("rate_limited","Too many role requests."));let t=++this.roleId;return new Promise((i,r)=>{let o=this.dipendenze.setTimeout(()=>{this.roleRequests.delete(t),r(f("timeout","The role request timed out."))},5e3);this.roleRequests.set(t,{resolve:i,reject:r,timer:o});try{this.invia({t:"request-role",r:t,role:e})}catch(s){this.dipendenze.clearTimeout(o),this.roleRequests.delete(t),r(s)}})}clearRoleRequests(){for(let e of this.roleRequests.values())this.dipendenze.clearTimeout(e.timer),e.reject(f("offline","The room connection ended."));this.roleRequests.clear()}disconnect(){if(this.lasciata)return;this.lasciata=!0;let e=this.socket;this.socket=null,this.voice.termina(),this.fermaInput(),this.fermaPing(),this.fermaRiconnessione(),this.clearRoleRequests(),this.timerRoster!==null&&this.dipendenze.clearTimeout(this.timerRoster),e?.close(1e3),this.segnalaStanza(null),this.metadataChanged({connection:"disconnected",closedCode:null}),this.prontaRisolta||(this.prontaRisolta=!0,this.rifiutaPronta(f("cancelled","The room was disconnected.")))}get state(){return this.statoPubblico}get tick(){return this.tickCorrente}get tickRate(){return this.tickRateCorrente}get latency(){return this.latenzaCorrente}get seed(){return this.seedCorrente}get status(){return this.statusCorrente}get players(){return this.copiaGiocatori()}get you(){return this.youCorrente}get host(){return this.hostCorrente}get code(){return this.codice}get result(){return this.resultCorrente}get delayMs(){return this.delaySpettatore}pronta(){return this.promessaPronta}invite(){return{code:this.codice,url:new URL(`/r/${this.codice}`,this.dipendenze.appOrigin).href}}onState(e){return this.ascoltatoriStato.add(e),()=>{this.ascoltatoriStato.delete(e)}}onPlayers(e){return this.ascoltatoriGiocatori.add(e),()=>{this.ascoltatoriGiocatori.delete(e)}}onStatus(e){return this.ascoltatoriStatus.add(e),()=>{this.ascoltatoriStatus.delete(e)}}onMessage(e){return this.ascoltatoriMessaggi.add(e),()=>{this.ascoltatoriMessaggi.delete(e)}}send(e){if(this.statusCorrente==="finished")return;let t=this.seq+1;this.invia({t:"msg",seq:t,m:e}),this.seq=t,this.ultimoInvioGioco=this.dipendenze.ora(),this.inviiGioco=[...this.inviiGioco.slice(-19),this.ultimoInvioGioco]}input(e){if(!(this.terminata||this.lasciata||this.statusCorrente==="finished")){try{let t=JSON.stringify(e);if(t===void 0)throw new TypeError;this.ultimoInput=t}catch{throw f("invalid_request","Room input must be valid JSON.")}this.programmaInput()}}pulisciInput(){this.fermaInput(),this.ultimoInput=this.inputInviato=null,this.ultimoInvioGioco=-1/0,this.inviiGioco=[]}fermaInput(){this.timerInput!==null&&this.dipendenze.clearTimeout(this.timerInput),this.timerInput=null}programmaInput(){if(this.timerInput!==null||this.ultimoInput===null||this.ultimoInput===this.inputInviato||!this.welcomeRicevuto||this.socket?.readyState!==ee||this.terminata||this.lasciata)return;let e=this.dipendenze.ora(),i=1e3/(this.tickRateCorrente>0?Math.min(20,this.tickRateCorrente):20);this.inviiGioco=this.inviiGioco.filter(s=>e-s<1e3);let r=this.inviiGioco.length>=20?this.inviiGioco[0]+1e3:e,o=Number.isFinite(this.ultimoInvioGioco)?this.ultimoInvioGioco+i:e+i;this.timerInput=this.dipendenze.setTimeout(()=>{if(this.timerInput=null,this.ultimoInput===null||this.ultimoInput===this.inputInviato||!this.welcomeRicevuto||this.socket?.readyState!==ee||this.terminata||this.lasciata)return;let s=this.dipendenze.ora();if(s<this.ultimoInvioGioco+i||this.inviiGioco.filter(u=>s-u<1e3).length>=20){this.programmaInput();return}let a=this.ultimoInput;try{this.send(JSON.parse(a)),this.inputInviato=a}catch{}},Math.max(0,Math.ceil(Math.max(o,r)-e)))}aggiornaTickRate(e){e===void 0||!Number.isInteger(e)||e<0||e>60||e===this.tickRateCorrente||(this.tickRateCorrente=e,this.fermaInput(),this.programmaInput())}ready(e){this.invia({t:"ready",ready:e})}setRole(e){this.invia({t:"role",role:e})}setTeam(e){this.invia({t:"team",team:e})}start(){this.invia({t:"start"})}restart(){if(this.statusCorrente!=="finished")throw f("rematch_unavailable","This room is not waiting for a rematch.");this.invia({t:"restart"})}leave(){if(!this.lasciata){if(this.spettatore||this.voice.leave(),this.lasciata=!0,this.segnalaStanza(null),this.socket?.readyState===ee){let e=this.socket;this.invia({t:"leave"}),this.spettatore&&e.close(1e3)}this.termina(1e3)}}serverTime(){return this.dipendenze.ora()+this.scartoOrario}copiaGiocatori(){return this.giocatoriCorrenti.map(e=>({...e}))}notifica(e,...t){for(let i of e)try{i(...t)}catch{}}invia(e){if(this.socket?.readyState!==ee)throw f("offline","The room is reconnecting.");let t;try{t=JSON.stringify(e)}catch{throw f("invalid_request","Room messages must be valid JSON.")}this.socket.send(t)}apri(e){let t;try{t=this.dipendenze.apriSocket(e)}catch{this.programmaRiconnessione();return}this.socket=t,t.addEventListener("open",()=>{this.socket===t&&this.avviaPing()}),t.addEventListener("message",i=>{this.socket===t&&typeof i.data=="string"&&this.ricevi(i.data)}),t.addEventListener("close",i=>{this.socket===t&&this.chiuso(i.code,i.reason)})}avviaPing(){if(this.socket?.readyState!==ee||this.terminata||this.lasciata)return;let e=this.statusCorrente==="playing"?Wt:Zt;this.timerPing!==null&&this.intervalloPing===e||(this.timerPing!==null&&this.dipendenze.clearInterval(this.timerPing),this.intervalloPing=e,this.timerPing=this.dipendenze.setInterval(()=>{if(this.socket?.readyState===ee)try{this.invia({t:"ping",c:this.dipendenze.ora()})}catch{}},e))}fermaPing(){this.timerPing!==null&&(this.dipendenze.clearInterval(this.timerPing),this.timerPing=null,this.intervalloPing=null)}ricevi(e){let t;try{let i=JSON.parse(e),r=Y(i);if(r===null||typeof r.t!="string")return;t=r}catch{return}try{if(t.t==="watching")this.riceviWatching(t);else if(t.t==="welcome")this.riceviWelcome(t);else if(t.t==="players")this.riceviGiocatori(t.players,t.host);else if(t.t==="status")this.riceviStatus(t);else if(t.t==="state")this.riceviDiff(t);else if(t.t==="snapshot")this.riceviSnapshot(t);else if(t.t==="msg")this.notifica(this.ascoltatoriMessaggi,Q(t.m));else if(t.t==="pong")this.riceviPong(t);else if(t.t==="error")this.notifica(this.errorListeners,{code:t.code,message:t.message});else if(t.t==="flush")this.richiediFlush();else if(t.t==="score-queued"&&!this.spettatore)this.scores.push(structuredClone(t.score)),this.scores=this.scores.slice(-32),this.notifica(this.scoreListeners,structuredClone(t.score));else if(t.t==="role-result"){let i=this.roleRequests.get(t.r);i&&(this.dipendenze.clearTimeout(i.timer),this.roleRequests.delete(t.r),t.ok?i.resolve():i.reject(f(t.code??"role_change_refused","The role change was not accepted.")))}else t.t==="voice"&&this.voice.ricevi(t)}catch{(t.t==="state"||t.t==="snapshot")&&this.chiediResync()}}riceviWatching(e){let t=e.room;!this.spettatore||t.id!==this.roomId||(this.aggiornaTickRate(t.tickRate),this.seedCorrente=t.seed,this.hostCorrente=t.host,this.statusCorrente=t.status,this.avviaPing(),this.resultCorrente=Q(t.result??null),t.status==="finished"&&this.pulisciInput(),this.giocatoriCorrenti=e.players.map(i=>({...i})),this.delaySpettatore=e.delayMs,this.aggiornaStato(e.state,t.tick,t.serverTime),this.scartoOrario=t.serverTime-this.dipendenze.ora(),this.resyncRichiesto=!1,this.welcomeRicevuto=!0,this.ritardoIndice=0,this.tempoRiconnessione=0,this.notifica(this.ascoltatoriGiocatori,this.copiaGiocatori()),this.notifica(this.ascoltatoriStatus,this.statusCorrente,this.resultCorrente,t.serverTime),this.initialMetadata(t),this.programmaInput(),this.risolviProntaSePossibile())}riceviWelcome(e){let t=e.room;t.id===this.roomId&&(this.youCorrente=e.you,this.aggiornaTickRate(t.tickRate),this.seedCorrente=t.seed,this.hostCorrente=t.host,this.statusCorrente=t.status,this.avviaPing(),this.resultCorrente=Q(t.result??null),t.status==="finished"&&this.pulisciInput(),this.giocatoriCorrenti=e.players.map(i=>({...i})),this.aggiornaStato(e.state,t.tick,t.serverTime),this.scartoOrario=t.serverTime-this.dipendenze.ora(),this.resyncRichiesto=!1,this.welcomeRicevuto=!0,!this.rosterRicevuto&&this.timerRoster===null&&(this.timerRoster=this.dipendenze.setTimeout(()=>{this.timerRoster=null,this.rosterRicevuto=!0,this.risolviProntaSePossibile()},Yt)),this.ritardoIndice=0,this.tempoRiconnessione=0,this.notifica(this.ascoltatoriGiocatori,this.copiaGiocatori()),this.voice.giocatoriCambiati(),this.voice.socketRiconnesso(),this.notifica(this.ascoltatoriStatus,this.statusCorrente,this.resultCorrente,t.serverTime),this.initialMetadata(t),this.programmaInput(),this.risolviProntaSePossibile())}riceviGiocatori(e,t){this.giocatoriCorrenti=e.map(i=>({...i})),t!==void 0?this.hostCorrente=t:this.giocatoriCorrenti.some(i=>i.id===this.hostCorrente&&i.connected)||(this.hostCorrente=this.giocatoriCorrenti.find(i=>i.connected)?.id??null),this.metadataChanged({host:this.hostCorrente}),this.notifica(this.ascoltatoriGiocatori,this.copiaGiocatori()),this.voice.giocatoriCambiati()}riceviStatus(e){this.statusCorrente=e.status,e.host!==void 0&&(this.hostCorrente=e.host),this.resultCorrente=Q(e.result),e.status==="finished"&&(this.pulisciInput(),this.clearRoleRequests()),e.status==="ended"?(this.terminata=!0,this.clearRoleRequests(),this.segnalaStanza(null),this.spettatore||this.voice.termina(),this.fermaPing(),this.fermaRiconnessione(),this.fermaInput(),this.ultimoInput=null):this.avviaPing(),this.metadataChanged({rematch:e.rematch??null,host:this.hostCorrente,countdownAt:e.countdownAt??(e.status==="countdown"?e.at:null),...e.status==="ended"?{connection:"ended",closedCode:4004}:{}}),this.notifica(this.ascoltatoriStatus,this.statusCorrente,this.resultCorrente,e.at)}riceviDiff(e){if(this.aggiornaTickRate(e.tickRate),e.base!==this.tickCorrente){this.chiediResync();return}let t=ii(this.statoSincronizzato,e.patch);if(!t.ok){this.chiediResync();return}this.resyncRichiesto=!1,this.aggiornaStato(t.state,e.tick,e.serverTime)}riceviSnapshot(e){e.tick<this.tickCorrente||(this.aggiornaTickRate(e.tickRate),this.resyncRichiesto=!1,this.aggiornaStato(e.state,e.tick,e.serverTime))}aggiornaStato(e,t,i){this.statoSincronizzato=Q(e),this.statoPubblico=Q(e),this.tickCorrente=t,this.notifica(this.ascoltatoriStato,this.statoPubblico,t,i)}chiediResync(){if(!(this.resyncRichiesto||this.socket?.readyState!==ee)){this.resyncRichiesto=!0;try{this.invia({t:"resync"})}catch{this.resyncRichiesto=!1}}}riceviPong(e){let t=this.dipendenze.ora();if(!Number.isFinite(e.c)||!Number.isFinite(e.s)||e.c>t)return;let i=t-e.c;this.latenzaCorrente=this.latenzaCorrente===null?i:this.latenzaCorrente*.8+i*.2,this.scartoOrario=e.s-(e.c+t)/2}chiuso(e,t){if(this.socket=null,this.welcomeRicevuto=!1,this.latenzaCorrente=null,this.fermaInput(),this.inputInviato=null,this.ultimoInvioGioco=-1/0,this.inviiGioco=[],this.fermaPing(),!(this.lasciata||this.terminata)){if(Xt.has(e)){let i=e===4009&&t==="message_too_large"?"message_too_large":void 0;i&&this.notifica(this.errorListeners,{code:i,message:"The room message is too large."}),this.termina(e,i);return}this.clearRoleRequests(),this.spettatore||this.voice.socketDisconnesso(),this.programmaRiconnessione()}}programmaRiconnessione(){if(this.terminata||this.lasciata||this.timerRiconnessione!==null)return;this.metadataChanged({connection:"reconnecting"});let e=Math.min(this.ritardoIndice,rt.length-1),t=rt[e];if(this.tempoRiconnessione+t>Kt){this.termina("timeout");return}this.ritardoIndice++,this.tempoRiconnessione+=t,this.timerRiconnessione=this.dipendenze.setTimeout(()=>{this.timerRiconnessione=null,this.riconnetti()},t)}async riconnetti(){if(!(this.terminata||this.lasciata))try{let e=this.spettatore?await this.api.watchRoom(this.roomId):await this.api.joinRoom(this.roomId);if(this.terminata||this.lasciata)return;let t=this.codice!==e.code;this.codice=e.code,t&&this.prontaRisolta&&!this.terminata&&!this.lasciata&&this.segnalaStanza({code:this.codice}),this.apri(e.url)}catch(e){e instanceof Error&&"code"in e&&["version_mismatch","version_outdated","room_not_found"].includes(String(e.code))?(this.notifica(this.errorListeners,{code:String(e.code),message:e.message}),this.termina(4004,String(e.code))):this.programmaRiconnessione()}}fermaRiconnessione(){this.timerRiconnessione!==null&&(this.dipendenze.clearTimeout(this.timerRiconnessione),this.timerRiconnessione=null)}termina(e,t){this.clearRoleRequests(),this.metadataChanged({connection:e===1e3?"disconnected":e===4006?"replaced":"closed",closedCode:typeof e=="number"?e:null});let i={closed:e},r=this.statusCorrente!=="ended"||JSON.stringify(this.resultCorrente)!==JSON.stringify(i);if(this.terminata=!0,this.fermaInput(),this.ultimoInput=null,this.segnalaStanza(null),this.statusCorrente="ended",this.resultCorrente=i,this.spettatore||this.voice.termina(),this.fermaPing(),this.fermaRiconnessione(),r&&this.notifica(this.ascoltatoriStatus,"ended",i,this.serverTime()),!this.prontaRisolta){this.prontaRisolta=!0;let s=t??(typeof e=="number"?{4003:"kicked",4004:"room_ended",4005:"version_closed",4006:"replaced",4008:"rate_limited",4009:"invalid_request"}[e]??"offline":"offline");this.rifiutaPronta(f(s,"The room connection ended."))}}risolviProntaSePossibile(){this.prontaRisolta||!this.welcomeRicevuto||!this.rosterRicevuto||(this.timerRoster!==null&&(this.dipendenze.clearTimeout(this.timerRoster),this.timerRoster=null),this.prontaRisolta=!0,!this.spettatore&&!this.terminata&&!this.lasciata&&this.segnalaStanza({code:this.codice}),this.risolviPronta())}richiediFlush(){this.flushRichiesto=!0,!(this.flushInCorso||this.timerFlush!==null)&&(this.timerFlush=this.dipendenze.setTimeout(()=>{this.timerFlush=null,this.eseguiFlush()},Qt))}async eseguiFlush(){if(!(this.flushInCorso||!this.flushRichiesto)){this.flushInCorso=!0,this.flushRichiesto=!1;try{await this.api.flush(this.roomId)}catch{}finally{this.flushInCorso=!1,this.flushRichiesto&&this.richiediFlush()}}}};function be(n=null){return{invited:n,reload(){typeof window<"u"&&window.location.reload()},onError(){return()=>{}},async create(){throw j()},async join(){throw j()},async watch(){throw j()},async match(){throw j()}}}function at(n,e){let t,i=new Set,r=ni({...n,onVersionError(d,c){t=c;for(let y of i)try{y(d)}catch{}}}),o=!1,s=null,a=d=>{let c=d?.code??null;o&&c===s||(o=!0,s=c,n.segnalaStanza?.(d))},u=async d=>{let c=new ye(d.roomId,d.code,d.url,n,r,a);return await c.pronta(),c},m=async d=>{let c=new ye(d.roomId,d.code,d.url,n,r,()=>{},!0);return await c.pronta(),{get mode(){return c.mode},get countdownAt(){return c.countdownAt},get connection(){return c.connection},get metadata(){return c.metadata},onMetadata:y=>c.onMetadata(y),onConnection:y=>c.onConnection(y),disconnect:()=>c.disconnect(),get state(){return c.state},get tick(){return c.tick},get tickRate(){return c.tickRate},get latency(){return c.latency},get seed(){return c.seed},get status(){return c.status},get players(){return c.players},get host(){return c.host},get code(){return c.code},get result(){return c.result},get delayMs(){return c.delayMs},onState:y=>c.onState(y),onPlayers:y=>c.onPlayers(y),onStatus:y=>c.onStatus(y),onMessage:y=>c.onMessage(y),leave:()=>{c.leave()},serverTime:()=>c.serverTime()}},g=(d,c)=>new Promise((y,k)=>{let C,D=!1,E=()=>{C.removeEventListener("message",I),C.removeEventListener("close",U),C.removeEventListener("error",G),c.signal?.removeEventListener("abort",B)},$=()=>{try{C.close(1e3)}catch{}},V=(F,P)=>{D||(D=!0,E(),P&&$(),k(F))};function B(){V(f("cancelled","The matchmaking search was cancelled."),!0)}function U(){V(j(),!1)}function G(){V(j(),!0)}function I(F){let P=null;try{P=typeof F.data=="string"?Y(JSON.parse(F.data)):null}catch{}if(P===null||typeof P.t!="string"){V(f("internal_error","The matchmaking service sent an invalid message."),!0);return}if(P.t==="waiting"){if(!Number.isInteger(P.players)||!Number.isInteger(P.min)||!Number.isInteger(P.max)){V(f("internal_error","The matchmaking service sent an invalid message."),!0);return}try{c.onWaiting?.({players:P.players,min:P.min,max:P.max})}catch{}return}if(P.t==="matched"){if(!st(P)){V(f("internal_error","The matchmaking service sent an invalid message."),!0);return}D=!0,E(),$(),y(P);return}if(P.t==="no_match"){V(f("no_match","No match was found before the timeout."),!0);return}if(P.t==="error"){V(f(typeof P.code=="string"?P.code:"internal_error",typeof P.message=="string"?P.message:"The matchmaking service could not complete the search."),!0);return}P.t!=="pong"&&V(f("internal_error","The matchmaking service sent an invalid message."),!0)}try{C=n.apriSocket(d)}catch{k(j());return}C.addEventListener("message",I),C.addEventListener("close",U),C.addEventListener("error",G),c.signal?.addEventListener("abort",B,{once:!0}),c.signal?.aborted===!0&&B()});return{invited:e,reload(){n.reload?.(t)},onError(d){return i.add(d),()=>{i.delete(d)}},async create(d){return u(await r.create(d.mode))},async join(d){let c=d??e;if(c==null||c.length===0)throw f("invalid_request","A room invitation code is required.");return u(await r.joinCode(c))},async watch(d){if(typeof d!="string"||d.length===0)throw f("invalid_request","A room invitation code is required.");return m(await r.watchCode(d))},async match(d){let c=()=>d.signal?.aborted===!0;if(c())throw f("cancelled","The matchmaking search was cancelled.");let y=await r.match(d);if(c())throw f("cancelled","The matchmaking search was cancelled.");return u(await g(y.url,d))}}}var oe="caisual:save:",oi=/^[a-z0-9][a-z0-9_-]{0,31}$/;function Me(n){if(!oi.test(n))throw f("invalid_request","Save keys must use lowercase letters, numbers, underscores, or hyphens.")}function lt(n){if(n===null)return null;try{return JSON.parse(n)}catch{return null}}function ct(n){let e=[];for(let t=0;t<n.length;t++){let i=n.key(t);i?.startsWith(oe)&&e.push(i.slice(oe.length))}return e}function ri(n,e){let t=()=>{if(n===null)throw j();return n};return{async set(i,r){Me(i);let o=t(),s=JSON.stringify({value:r}),a=new TextEncoder().encode(s).byteLength;if(a>262144)throw f("payload_too_large","The save is larger than 262144 bytes.");if(o.getItem(oe+i)===null&&ct(o).length>=32)throw f("save_limit","A game can store at most 32 save keys.");let u={value:r,bytes:a,updatedAt:e()};return o.setItem(oe+i,JSON.stringify(u)),{key:i,bytes:a,updatedAt:u.updatedAt}},async get(i){return Me(i),lt(t().getItem(oe+i))?.value??null},async remove(i){Me(i),t().removeItem(oe+i)},async list(){let i=t();return ct(i).flatMap(r=>{let o=lt(i.getItem(oe+r));return o===null?[]:[{key:r,bytes:o.bytes,updatedAt:o.updatedAt}]}).sort((r,o)=>r.key.localeCompare(o.key))}}}async function Ie(n,e=null){let t=n.ora(),i=Ce(t),r=await Pe(n.hostname,i,n.subtle);return{connected:!1,player:{id:"local",name:"Guest",guest:!0},daily:he({day:i,seed:r,expiresAt:ae(t)},n.ora,async()=>{let o=n.ora(),s=Ce(o);return{day:s,seed:await Pe(n.hostname,s,n.subtle),expiresAt:ae(o)}}),time:{now:n.ora},save:ri(n.archivio,n.ora),board:{async submit(){return{accepted:!1,reason:"offline",verified:!1}},async top(o,s={}){if(s.day!==void 0&&(!le(s.day)||s.daily===!1))throw f("invalid_request","day must be a real UTC date and cannot be combined with daily: false.");return{day:s.day??(s.daily?i:null),entries:[],me:null}}},room:be(e)}}function si(n){let e=n?.querySelector(\'meta[name="caisual-app"]\')?.getAttribute("content");if(e==null)return null;try{let t=new URL(e);return t.origin===e&&(t.protocol==="https:"||t.protocol==="http:")?e:null}catch{return null}}function ai(){try{return typeof localStorage>"u"?null:localStorage}catch{return null}}function li(){return{finestra:typeof window>"u"?null:window,documento:typeof document>"u"?null:document,fetcher:(n,e)=>globalThis.fetch(n,e),archivio:ai(),language:typeof navigator>"u"?"en":navigator.language,pathname:typeof location>"u"?"/":location.pathname,hostname:typeof location>"u"?"":location.hostname,subtle:globalThis.crypto.subtle,ora:Date.now,sonda:()=>$e()}}async function ci(n){let e=si(n.documento),t=n.finestra===null||n.finestra.parent===n.finestra;if(e===null||t)return ut(n);let i=await tt(n.finestra,e,n.timeoutHandshake);if(i===null)return ut(n);let r=Ae(i.ticket,i.porta,n.finestra,n.ora,n.timeoutRinnovo,"portal"),o=Ye(e,n.fetcher,r),s=n.ora(),a;try{a=await o.me()}catch{let c=await Ie(n,i.invite);return ze(c,i,n)}let u=n.ora(),m=a.serverTime-(s+u)/2,g=i.live===null?be(i.invite):at({appOrigin:e,n:i.n,reload:c=>i.porta.postMessage({type:"caisual:reload",target:c}),liveOrigin:i.live,fetcher:n.fetcher,biglietto:Ae(null,i.porta,n.finestra,n.ora,n.timeoutRinnovo,"live"),apriSocket(c){if(n.apriSocket!==void 0)return n.apriSocket(c);if(typeof WebSocket>"u")throw j();return new WebSocket(c)},ora:n.ora,setTimeout:(c,y)=>globalThis.setTimeout(c,y),clearTimeout:c=>globalThis.clearTimeout(c),setInterval:(c,y)=>globalThis.setInterval(c,y),clearInterval:c=>globalThis.clearInterval(c),voce:n.voce,segnalaStanza(c){try{i.porta.postMessage({type:"caisual:room",room:c})}catch{}}},i.invite),d={connected:!0,player:a.player,daily:he({day:a.day,seed:a.seed,expiresAt:a.expiresAt??ae(a.serverTime)},()=>n.ora()+m,async()=>{let c=await o.me();return{day:c.day,seed:c.seed,expiresAt:c.expiresAt??ae(c.serverTime)}}),time:{now:()=>n.ora()+m},save:{set:(c,y)=>o.saveSet(c,y),get:c=>o.saveGet(c),remove:c=>o.saveRemove(c),list:()=>o.saveList()},board:{async submit(c,y,k={}){try{return await o.boardSubmit(c,y,k.daily===!0)}catch(C){if(typeof C=="object"&&C!==null&&"code"in C&&C.code==="offline")return{accepted:!1,reason:"offline",verified:!1};throw C}},top:(c,y={})=>o.boardTop(c,y)},room:g};return ze(d,i,n)}function ze(n,e,t){let i=We(n,e?.overlay?.configuration??null,n.connected&&e?.live!=null);if(e?.overlay){let a=Qe(e.porta,e.overlay,i);i.session.capabilities.overlay&&typeof window<"u"&&t?.finestra===window&&window.addEventListener("pagehide",a,{once:!0})}let r=e?.languagePreferences?.length?e.languagePreferences:[e?.language??t?.language??"en"],o=Ee(r,e?.gameLanguages??(e?.overlay?we(e.overlay.configuration.manifest):void 0)),s=Be(e?.uiLanguage??e?.language??t?.language);return{...n,player:{...n.player,language:o,uiLanguage:s},text:Ue(t?.fetcher??globalThis.fetch,o,t?.pathname),room:i.rooms,session:i.session,overlay:i.overlay}}async function ut(n){return ze(await Ie(n),void 0,n)}function dt(){return{webgl2:!1,webgpu:!1,wasm:!1,threads:!1,isolated:!1,gpu:"none",memoryMb:null,cores:null,mobile:!1,tier:"low"}}async function ui(n){let e;try{return await Promise.race([Promise.resolve().then(n).catch(()=>dt()),new Promise(t=>{e=globalThis.setTimeout(()=>t(dt()),1500)})])}finally{e!==void 0&&globalThis.clearTimeout(e)}}function mt(n=li()){let e=null;return{connect(){return e??(e=Promise.all([ci(n),ui(n.sonda)]).then(([t,i])=>({...t,device:i}))),e}}}var pt=mt();globalThis.caisual=pt;var to=pt;export{pt as caisual,to as default};\n');
5271
+ response.end(request.method === "HEAD" ? void 0 : '// Caisual game kit v0.16.0\nvar kt=["www","api","app","play","live","multi","cdn","assets","static","mail","mx","ns1","ns2","autodiscover","_dmarc","admin","login","account","auth","pay","secure","support","help","blog","status","dev","staging","test","caisual","shipz"],xt=new Set(kt),Mt=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,Ct=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;function ge(n){return n.length>=3&&n.length<=32&&Mt.test(n)||Ct.test(n)}function Ne(n){return xt.has(n)}function G(n){if(typeof n!="string"||n.length>128)return null;try{return Intl.getCanonicalLocales(n)[0]??null}catch{return null}}function Ce(n){return n.languages?.length?[...n.languages]:[n.language??"en"]}function Pt(n,e="en"){let t=[],i=G(n);for(;i;){t.push(i);let r=i.split("-");r.pop(),r.at(-1)?.length===1&&r.pop(),i=r.join("-")}return t.push(G(e)??e),[...new Set(t)]}function je(n,e=[]){let t=e.map(G).filter(r=>r!==null),i=n.map(G).filter(r=>r!==null);if(!t.length)return i[0]??"en";for(let r of i)for(let s of Pt(r,r))if(t.includes(s))return s;return t[0]}function Ge(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)&&Object.values(n).every(e=>typeof e=="string")}function Ue(n,e){let t=e===null?void 0:n.modes.find(i=>i.id===e);if(e!==null&&t===void 0)throw new Error("The selected game mode does not exist.");return{players:{...t?.players??n.players},lobby:t?.lobby??n.lobby}}function ve(n,e){return e!==null&&n.modes.some(t=>t.id===e&&t.execution==="local")}var P=24;var Tt=3e3,Je=32,At=new Set(["overlay","manifest","id","name","description","cover","card","icon","screenshots","tags","languages","language","platform","orientation","input","visibility","network","isolated","requires","players","lobby","persistent","replays","spectators","boards","roles","teams","voice","modes"]),It=new Set(["keyboard","mouse","touch","gamepad"]),Ot=new Set(["desktop","mobile","both"]),Et=new Set(["landscape","portrait"]),_t=new Set(["public","unlisted"]),zt=new Set(["none","room","team","proximity"]),Vt=new Set(["light","medium","heavy"]),Lt=/^[a-z0-9-]+$/,qe=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,$t=/^[a-z0-9][a-z0-9-]{0,31}$/,Dt=/^[a-z0-9][a-z0-9_-]{0,31}$/;function J(n){return typeof n!="object"||n===null||Array.isArray(n)?null:n}function Be(n){if(n===""||n.startsWith("/")||n.includes("\\\\")||n.includes("\\0")||n.includes("?")||n.includes("#"))return!1;let e=n.split("/");if(e.some(t=>t===""||t==="."||t===".."))return!1;try{return!e.map(i=>decodeURIComponent(i)).some(i=>i===""||i==="."||i===".."||i.includes("/"))}catch{return!1}}function Nt(n){return n.length===0||n.length>253||n.includes("://")||/[/:?#@]/.test(n)?!1:n.split(".").every(t=>t.length>=1&&t.length<=63&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(t))}function V(n,e,t){return typeof n=="number"&&Number.isInteger(n)&&n>=e&&n<=t}function Pe(n,e,t,i){let r=n[e];return r===void 0?t:typeof r!="string"?(i.push(`${e}: must be a string.`),t):r}function ye(n,e,t,i,r){if(n[e]===void 0)return;let s=(h,d)=>{if(typeof h!="string"||h.trim().length===0||h.trim().length>t||/[\\r\\n\\u0000-\\u001f]/.test(h)){r.push(`${d}: must contain 1-${t} characters on one line.`);return}return h.trim()},o=n[e],a=i?`${i}.${e}`:e;if(typeof o=="string")return s(o,a);let c=J(o);if(!c||Object.keys(c).length===0){r.push(`${a}: must be a string or a non-empty language-to-text object.`);return}let m={};for(let[h,d]of Object.entries(c)){let u=G(h);if(!u){r.push(`${a}.${h}: must be a BCP 47 language tag.`);continue}Object.hasOwn(m,u)&&r.push(`${a}.${h}: duplicate language.`);let v=s(d,`${a}.${h}`);v!==void 0&&(m[u]=v)}return m}function Te(n){let e=[],t=J(n);if(t===null)return{ok:!1,errori:["manifest: must be a JSON object."]};for(let g of Object.keys(t))At.has(g)||e.push(`${g}: unknown field.`);t.manifest===void 0?e.push("manifest: is required and must be 1."):t.manifest!==1&&e.push("manifest: must be exactly 1.");let i=Pe(t,"id","",e);t.id===void 0?e.push("id: is required."):typeof t.id=="string"&&(ge(i)?Ne(i)&&e.push("id: this slug is reserved."):e.push("id: must be 3-32 lowercase ASCII letters or digits with internal hyphens; historical UUID v4 slugs may be 36 characters."));let r=Pe(t,"name","",e);t.name===void 0?e.push("name: is required."):typeof t.name=="string"&&(r.trim()===""||r.length>60)&&e.push("name: must contain 1-60 characters.");let s=t.description===""?"":ye(t,"description",500,"",e)??"",o={cover:"",card:"",icon:""},a=new Set;for(let g of["cover","card","icon"]){let f=t[g];if(f==null)e.push(`${g}: is required.`);else if(typeof f!="string"||!Be(f))e.push(`${g}: must be a relative file path inside client/ without query, fragment, or parent segments.`);else{/\\.(png|jpe?g|webp)$/i.test(f)||e.push(`${g}: must be a PNG, JPEG or WebP file.`);let L=decodeURIComponent(f);a.has(L)&&e.push(`${g}: each image must use a different file; cover, card and icon cannot share a path.`),a.add(L),o[g]=f}}let{cover:c,card:m,icon:h}=o,d=[];if(t.screenshots!==void 0)if(!Array.isArray(t.screenshots))e.push("screenshots: must be an array of relative file paths.");else{t.screenshots.length>8&&e.push("screenshots: must contain at most 8 paths.");for(let[g,f]of t.screenshots.entries())typeof f!="string"||!Be(f)?e.push(`screenshots[${g}]: must be a relative file path without query, fragment, or parent segments.`):d.push(f)}let u=[];if(t.tags!==void 0)if(!Array.isArray(t.tags))e.push("tags: must be an array.");else{t.tags.length>10&&e.push("tags: must contain at most 10 tags.");for(let[g,f]of t.tags.entries())typeof f!="string"||f.length>24||!Lt.test(f)?e.push(`tags[${g}]: must be 1-24 lowercase letters, digits, or hyphens.`):u.push(f)}let v=Pe(t,"language","en",e);/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(v)||e.push("language: must be a BCP 47 language tag such as en, it, or pt-BR.");let x=[];if(!Array.isArray(t.languages)||t.languages.length===0)e.push("languages: must be a non-empty array of BCP 47 language tags.");else for(let[g,f]of t.languages.entries()){let L=G(f);L?x.includes(L)?e.push(`languages[${g}]: duplicate language ${L}.`):x.push(L):e.push(`languages[${g}]: must be a BCP 47 language tag.`)}x.includes("en")||e.push("languages: English is always required alongside the game\'s own languages.");let w=x[0]??v;if(typeof s=="object")for(let g of Object.keys(s))x.includes(g)||e.push(`description.${g}: language must be declared in languages.`);t.language!==void 0&&t.languages!==void 0&&v.toLowerCase()!==w.toLowerCase()&&e.push("language: must match the first entry in languages when both are present.");let N="both";t.platform===void 0?e.push("platform: is required."):typeof t.platform!="string"||!Ot.has(t.platform)?e.push("platform: must be desktop, mobile, or both."):N=t.platform;let _="landscape";t.orientation!==void 0&&(typeof t.orientation!="string"||!Et.has(t.orientation)?e.push("orientation: must be landscape or portrait."):_=t.orientation);let j=[];if(t.input!==void 0)if(!Array.isArray(t.input))e.push("input: must be an array.");else for(let[g,f]of t.input.entries())typeof f!="string"||!It.has(f)?e.push(`input[${g}]: must be keyboard, mouse, touch, or gamepad.`):j.includes(f)?e.push(`input[${g}]: duplicate value ${f}.`):j.push(f);let z="public";t.visibility!==void 0&&(typeof t.visibility!="string"||!_t.has(t.visibility)?e.push("visibility: must be public or unlisted."):z=t.visibility);let B=[];if(t.network!==void 0)if(!Array.isArray(t.network))e.push("network: must be an array of host names.");else for(let[g,f]of t.network.entries())typeof f!="string"||!Nt(f)?e.push(`network[${g}]: must be a host name without scheme, port, path, query, or fragment.`):B.includes(f)?e.push(`network[${g}]: duplicate host ${f}.`):B.push(f);t.isolated!==void 0&&typeof t.isolated!="boolean"&&e.push("isolated: must be a boolean.");let U={webgl2:!1,webgpu:!1,wasm:!1,threads:!1,memoryMb:null,performance:"light"};if(t.requires!==void 0){let g=J(t.requires);if(g===null)e.push("requires: must be an object.");else{for(let f of Object.keys(g))["webgl2","webgpu","wasm","threads","memoryMb","performance"].includes(f)||e.push(`requires.${f}: unknown field.`);for(let f of["webgl2","webgpu","wasm","threads"])g[f]!==void 0&&(typeof g[f]!="boolean"?e.push(`requires.${f}: must be a boolean.`):U[f]=g[f]);g.memoryMb!==void 0&&(g.memoryMb!==null&&(!V(g.memoryMb,512,32768)||g.memoryMb%256!==0)?e.push("requires.memoryMb: must be an integer from 512 to 32768 in steps of 256, or null."):U.memoryMb=g.memoryMb),g.performance!==void 0&&(typeof g.performance!="string"||!Vt.has(g.performance)?e.push("requires.performance: must be light, medium, or heavy."):U.performance=g.performance)}}let F={min:1,max:1};if(t.players!==void 0){let g=J(t.players);if(g===null)e.push("players: must be an object with min and max.");else{for(let f of Object.keys(g))f!=="min"&&f!=="max"&&e.push(`players.${f}: unknown field.`);V(g.min,1,P)||e.push(`players.min: must be an integer from 1 to ${P}.`),V(g.max,1,P)||e.push(`players.max: must be an integer from 1 to ${P} in manifest version 1.`),V(g.min,1,P)&&V(g.max,1,P)&&(g.min>g.max?e.push("players.max: must be greater than or equal to players.min."):F={min:g.min,max:g.max})}}let W=!1;t.lobby!==void 0&&(typeof t.lobby!="boolean"?e.push("lobby: must be a boolean."):W=t.lobby);let A=!1;t.persistent!==void 0&&(typeof t.persistent!="boolean"?e.push("persistent: must be a boolean."):A=t.persistent);let M=t.replays===!0;t.replays!==void 0&&typeof t.replays!="boolean"&&e.push("replays: must be a boolean.");let ue={delayMs:Tt};if(t.spectators===!1||t.spectators===null)ue=null;else if(t.spectators!==void 0&&t.spectators!==!0){let g=J(t.spectators);if(g===null)e.push("spectators: must be a boolean or an object with delayMs.");else{for(let f of Object.keys(g))f!=="delayMs"&&e.push(`spectators.${f}: unknown field.`);V(g.delayMs,0,3e4)?ue={delayMs:g.delayMs}:e.push("spectators.delayMs: must be an integer from 0 to 30000.")}}let ee=null;if(t.overlay!==void 0&&t.overlay!==null){let g=J(t.overlay);if(g===null)e.push("overlay: must be an object or null.");else{for(let f of Object.keys(g))["version","accent"].includes(f)||e.push(`overlay.${f}: unknown field.`);g.version!==1&&e.push("overlay.version: must be exactly 1."),g.accent!==void 0&&(typeof g.accent!="string"||!/^#[0-9a-fA-F]{6}$/.test(g.accent))&&e.push("overlay.accent: must be a six-digit hexadecimal color, such as #336699."),ee={version:1,...typeof g.accent=="string"?{accent:g.accent}:{}}}}let I={};if(t.boards!==void 0){let g=J(t.boards);if(g===null)e.push("boards: must be an object of board ids.");else{Object.keys(g).length>Je&&e.push(`boards: at most ${Je} boards.`);for(let[f,L]of Object.entries(g)){let S=!0;Dt.test(f)||(e.push(`boards.${f}: invalid board id.`),S=!1);let b=J(L);if(b===null){e.push(`boards.${f}.source: must be "client" or "server".`);continue}for(let E of Object.keys(b))["source","label","periods","day"].includes(E)||e.push(`boards.${f}.${E}: unknown field.`);b.source!=="client"&&b.source!=="server"&&(e.push(`boards.${f}.source: must be "client" or "server".`),S=!1),b.day!==void 0&&b.day!=="submit"&&b.day!=="start"&&e.push(`boards.${f}.day: must be "submit" or "start".`),b.day==="start"&&b.source!=="server"&&e.push(`boards.${f}.day: start requires source "server".`);let O=ye(b,"label",48,`boards.${f}`,e),T=["all-time"];b.periods!==void 0&&(!Array.isArray(b.periods)||b.periods.length<1||b.periods.length>2||b.periods.some(E=>E!=="daily"&&E!=="all-time")||new Set(b.periods).size!==b.periods.length?e.push(`boards.${f}.periods: must contain daily, all-time, or both without duplicates.`):T=[...b.periods]),S&&Object.defineProperty(I,f,{value:{source:b.source,periods:T,...b.day===void 0?{}:{day:b.day},...O===void 0?{}:{label:O}},enumerable:!0,configurable:!0,writable:!0})}}}let te=[];if(t.roles!==void 0)if(!Array.isArray(t.roles))e.push("roles: must be an array.");else{let g=new Set;for(let[f,L]of t.roles.entries()){let S=J(L);if(S===null){e.push(`roles[${f}]: must be an object.`);continue}for(let l of Object.keys(S))["id","min","max","label"].includes(l)||e.push(`roles[${f}].${l}: unknown field.`);let b=S.id,O=S.min,T=S.max,E=!0;typeof b!="string"||b.length>32||!qe.test(b)?(e.push(`roles[${f}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`),E=!1):g.has(b)?(e.push(`roles[${f}].id: duplicate role ${b}.`),E=!1):g.add(b),V(O,0,P)||(e.push(`roles[${f}].min: must be an integer from 0 to ${P}.`),E=!1),T!==void 0&&!V(T,0,P)&&(e.push(`roles[${f}].max: must be an integer from 0 to ${P} when present.`),E=!1),typeof O=="number"&&typeof T=="number"&&O>T&&(e.push(`roles[${f}].max: must be greater than or equal to min.`),E=!1);let ie=ye(S,"label",32,`roles[${f}]`,e);E&&te.push({id:b,min:O,...T===void 0?{}:{max:T},...ie===void 0?{}:{label:ie}})}}let Y=null;if(t.teams!==void 0&&t.teams!==null){let g=J(t.teams);if(g===null)e.push("teams: must be null or an object with min and max.");else{for(let f of Object.keys(g))f!=="min"&&f!=="max"&&e.push(`teams.${f}: unknown field.`);V(g.min,2,P)||e.push(`teams.min: must be an integer from 2 to ${P}.`),V(g.max,2,P)||e.push(`teams.max: must be an integer from 2 to ${P}.`),V(g.min,2,P)&&V(g.max,2,P)&&(g.min>g.max?e.push("teams.max: must be greater than or equal to teams.min."):Y={min:g.min,max:g.max})}}let re="none";t.voice!==void 0&&(typeof t.voice!="string"||!zt.has(t.voice)?e.push("voice: must be none, room, team, or proximity."):re=t.voice);let H=[];if(t.modes!==void 0)if(!Array.isArray(t.modes))e.push("modes: must be an array.");else{let g=new Set;for(let[f,L]of t.modes.entries()){let S=J(L);if(S===null){e.push(`modes[${f}]: must be an object.`);continue}for(let l of Object.keys(S))["id","players","lobby","matchmaking","execution","label","instructions"].includes(l)||e.push(`modes[${f}].${l}: unknown field.`);if(typeof S.id!="string"||S.id.length>32||!qe.test(S.id)){e.push(`modes[${f}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`);continue}if(g.has(S.id)){e.push(`modes[${f}].id: duplicate mode ${S.id}.`);continue}g.add(S.id);let b={id:S.id};for(let[l,y]of[["label",48],["instructions",160]]){let R=ye(S,l,y,`modes[${f}]`,e);R!==void 0&&(b[l]=R)}if(S.execution!==void 0&&(S.execution!=="local"&&S.execution!=="room"?e.push(`modes[${f}].execution: must be local or room.`):b.execution=S.execution),ee!==null&&b.execution===void 0&&e.push(`modes[${f}].execution: is required with the standard overlay.`),S.players!==void 0){let l=`modes[${f}].players`,y=J(S.players);if(y===null)e.push(`${l}: must be an object with min and max.`);else{for(let R of Object.keys(y))R!=="min"&&R!=="max"&&e.push(`${l}.${R}: unknown field.`);V(y.min,1,P)||e.push(`${l}.min: must be an integer from 1 to ${P}.`),V(y.max,1,P)||e.push(`${l}.max: must be an integer from 1 to ${P}.`),V(y.min,1,P)&&V(y.max,1,P)&&(y.min>y.max?e.push(`${l}.max: must be greater than or equal to min.`):b.players={min:y.min,max:y.max})}}if(S.lobby!==void 0&&(typeof S.lobby!="boolean"?e.push(`modes[${f}].lobby: must be a boolean.`):b.lobby=S.lobby),b.execution==="local"){let l=b.players??F;(l.min!==1||l.max!==1)&&e.push(`modes[${f}].players: local execution requires min and max to be 1.`),(b.lobby??W)&&e.push(`modes[${f}].lobby: local execution requires false.`),S.matchmaking!==void 0&&e.push(`modes[${f}].matchmaking: local execution cannot use matchmaking.`)}if(S.matchmaking===void 0){H.push(b);continue}let O=J(S.matchmaking);if(O===null){e.push(`modes[${f}].matchmaking: must be an object.`);continue}for(let l of Object.keys(O))["key","timeoutMs","defaults"].includes(l)||e.push(`modes[${f}].matchmaking.${l}: unknown field.`);let T=!0,E=[];if(!Array.isArray(O.key)||O.key.length<1||O.key.length>8)e.push(`modes[${f}].matchmaking.key: must contain from 1 to 8 fields.`),T=!1;else for(let[l,y]of O.key.entries())typeof y!="string"||!$t.test(y)?(e.push(`modes[${f}].matchmaking.key[${l}]: must be 1-32 lowercase letters, digits, or hyphens and start with a letter or digit.`),T=!1):E.includes(y)?(e.push(`modes[${f}].matchmaking.key[${l}]: duplicate field ${y}.`),T=!1):E.push(y);V(O.timeoutMs,1e3,3e5)||(e.push(`modes[${f}].matchmaking.timeoutMs: must be an integer from 1000 to 300000.`),T=!1);let ie;if(O.defaults!==void 0){let l=J(O.defaults);if(l===null||Object.keys(l).length!==E.length||E.some(y=>!Object.hasOwn(l,y)))e.push(`modes[${f}].matchmaking.defaults: must contain exactly the declared key fields.`);else{ie={};for(let[y,R]of Object.entries(l))!(typeof R=="string"&&R.length>=1&&R.length<=64&&/^[A-Za-z0-9_.:-]+$/.test(R))&&!Number.isSafeInteger(R)?e.push(`modes[${f}].matchmaking.defaults.${y}: must be a string of 1-64 characters or a safe integer.`):Object.defineProperty(ie,y,{value:R,enumerable:!0})}}T&&H.push({...b,matchmaking:{...ie===void 0?{}:{defaults:ie},key:E,timeoutMs:O.timeoutMs}})}}return ee!==null&&H.length===0&&e.push("modes: at least one explicit mode is required with the standard overlay."),e.length>0?{ok:!1,errori:e}:{ok:!0,manifest:{manifest:1,overlay:ee,id:i,name:r,description:s,cover:c,card:m,icon:h,screenshots:d,tags:u,languages:x,language:w,platform:N,orientation:_,input:j,visibility:z,network:B,requires:U,players:F,lobby:W,persistent:A,replays:M,spectators:ue,boards:I,roles:te,teams:Y,voice:re,modes:H}}}function jt(n){return n.gpu!=="hardware"||n.memoryMb!==null&&n.memoryMb<=2048?"low":n.mobile||n.memoryMb!==null&&n.memoryMb<=4096||n.cores!==null&&n.cores<=4?"mid":"high"}function Fe(n){try{n?.getExtension("WEBGL_lose_context")?.loseContext()}catch{}}function Gt(n){let e;try{e=n.navigator}catch{e=void 0}let t=null;try{let o=e?.deviceMemory,a=typeof o=="number"?o*1024:NaN;Number.isFinite(a)&&(t=a)}catch{t=null}let i=null;try{let o=e?.hardwareConcurrency;typeof o=="number"&&Number.isFinite(o)&&(i=o)}catch{i=null}let r=!1;try{r=typeof e?.userAgentData?.mobile=="boolean"?e.userAgentData.mobile:/Android|iPhone|iPad|iPod|Mobile/i.test(e?.userAgent??"")}catch{r=!1}let s=!1;try{s=n.crossOriginIsolated===!0}catch{s=!1}return{webgl2:!1,webgpu:!1,wasm:!1,threads:!1,isolated:s,gpu:"none",memoryMb:t,cores:i,mobile:r}}async function We(n,e=1500){let t=n??globalThis,i=Gt(t),r=Promise.resolve().then(()=>{try{let m=t.document?.createElement("canvas");if(m===void 0)return;let h=m.getContext("webgl2",{failIfMajorPerformanceCaveat:!0});if(h!==null){i.webgl2=!0,i.gpu="hardware",Fe(h);return}let d=m.getContext("webgl2");d!==null&&(i.webgl2=!0,i.gpu="software",Fe(d))}catch{i.webgl2=!1,i.gpu="none"}}),s=Promise.resolve().then(async()=>{let m;try{let h=t.navigator?.gpu;if(h===void 0)return;let d=await h.requestAdapter();if(d===null)return;m=await d.requestDevice(),i.webgpu=!0}catch{i.webgpu=!1}finally{try{m?.destroy?.()}catch{}}}),o=Promise.resolve().then(()=>{try{i.wasm=t.WebAssembly?.validate(new Uint8Array([0,97,115,109,1,0,0,0]))===!0}catch{i.wasm=!1}}),a=Promise.resolve().then(()=>{try{if(t.WebAssembly===void 0)return;new t.WebAssembly.Memory({initial:1,maximum:1,shared:!0}),i.threads=!0}catch{i.threads=!1}}),c;return await Promise.race([Promise.all([r,s,o,a]),new Promise(m=>{c=setTimeout(m,Math.max(0,e))})]),c!==void 0&&clearTimeout(c),{...i,tier:jt(i)}}function Ae(n){return typeof n=="number"&&Number.isSafeInteger(n)&&n>0}var de=/^[A-Za-z0-9_-]{22}$/;function Bt(n,e=null,t=null,i=null){let r=Te(n);if(!r.ok)throw new Error("The overlay manifest is invalid.");return{manifest:r.manifest,coverUrl:e,iconUrl:i,invite:t}}function D(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)?n:null}function Ut(n){let e=D(n),t=D(e?.configuration);return e?.v===1&&typeof e.epoch=="string"&&e.epoch.length>0&&e.epoch.length<=128&&t!==null&&(t.coverUrl===null||typeof t.coverUrl=="string")&&(t.iconUrl===null||typeof t.iconUrl=="string")&&(t.invite===null||typeof t.invite=="string"&&/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(t.invite))&&Te(t.manifest).ok}function He(n){return Ut(n)?{v:1,epoch:n.epoch,configuration:Bt(n.configuration.manifest,n.configuration.coverUrl,n.configuration.invite,n.configuration.iconUrl)}:null}function Ft(n){let e=D(n);return e!==null&&Object.keys(e).length===4&&["top","right","bottom","left"].every(t=>typeof e[t]=="number"&&Number.isFinite(e[t])&&Number(e[t])>=0&&Number(e[t])<=1e5)}function Oe(n){let e=D(n);return e!==null&&Object.keys(e).every(t=>["inputBlocked","reservedRects","safeArea","shortcutEnabled"].includes(t))&&(e.safeArea===void 0||Ft(e.safeArea))&&(e.shortcutEnabled===void 0||typeof e.shortcutEnabled=="boolean")&&typeof e.inputBlocked=="boolean"&&Array.isArray(e.reservedRects)&&e.reservedRects.length<=8&&e.reservedRects.every(t=>{let i=D(t);return i!==null&&Object.keys(i).length===4&&["x","y","width","height"].every(r=>typeof i[r]=="number"&&Number.isFinite(i[r])&&i[r]>=0&&i[r]<=1e5)})}function Ke(n){let e=D(n),t=D(e?.args);if(e?.type!=="caisual:overlay"||e.v!==1||typeof e.epoch!="string"||e.epoch.length<1||e.epoch.length>128||typeof e.requestId!="string"||!(/^[1-9][0-9]{0,15}$/.test(e.requestId)&&Number.isSafeInteger(Number(e.requestId)))||t===null||Object.keys(e).some(s=>!["type","v","epoch","requestId","sessionId","op","args"].includes(s))||!(e.sessionId===void 0||e.sessionId===null||typeof e.sessionId=="string"&&/^[1-9][0-9]{0,15}$/.test(e.sessionId)))return!1;let i=(...s)=>Object.keys(t).every(o=>s.includes(o)),r=s=>typeof t[s]=="string"&&t[s].length>=1&&t[s].length<=64;switch(e.op){case"replay.play":case"replay.pause":return i()&&typeof e.sessionId=="string";case"replay.seek":return i("positionMs")&&typeof e.sessionId=="string"&&typeof t.positionMs=="number"&&Number.isFinite(t.positionMs)&&t.positionMs>=0&&t.positionMs<=18e5;case"replay.speed":return i("speed")&&typeof e.sessionId=="string"&&[.5,1,2,4].includes(Number(t.speed))&&typeof t.speed=="number";case"local.start":return i("mode")&&r("mode");case"room.create":return i("mode")&&(t.mode===null||r("mode"));case"room.join":return i("code")&&(t.code===void 0||r("code"));case"room.watch":return i("code")&&r("code");case"room.match":{let s=D(t.key);return i("mode","key")&&r("mode")&&(t.key===void 0||s!==null&&Object.keys(s).length<=8&&Object.values(s).every(o=>typeof o=="string"&&o.length>=1&&o.length<=64||typeof o=="number"&&Number.isSafeInteger(o)))}case"room.ready":return i("ready")&&typeof t.ready=="boolean";case"room.role":case"room.requestRole":return i("role")&&r("role");case"room.team":return i("team")&&Number.isInteger(t.team)&&t.team>=1&&t.team<=24;case"room.restart":case"room.start":case"session.cancel":case"session.leave":case"session.disconnect":case"session.resume":return i();case"voice.join":case"voice.leave":return i()&&typeof e.sessionId=="string";case"voice.mute":return i("muted")&&typeof t.muted=="boolean"&&typeof e.sessionId=="string";case"voice.setVolume":return i("playerId","volume")&&typeof e.sessionId=="string"&&typeof t.playerId=="string"&&t.playerId.length>0&&t.playerId.length<=128&&typeof t.volume=="number"&&Number.isFinite(t.volume)&&t.volume>=0&&t.volume<=1;case"overlay.view":return Oe(t);default:return!1}}function me(n){if(typeof n!="string"||!/^\\d{4}-\\d{2}-\\d{2}$/.test(n))return!1;let e=Date.parse(`${n}T00:00:00Z`);return Number.isFinite(e)&&new Date(e).toISOString().slice(0,10)===n}function Ye(n,e){let t=a=>a!==null&&typeof a=="object"&&!Array.isArray(a)?a:null,i=t(n);if(!i||!Array.isArray(i.standings))return null;let r=new Set(e),s=new Set,o=[];for(let a of i.standings){let c=t(a);!c||typeof c.playerId!="string"||!r.has(c.playerId)||s.has(c.playerId)||(s.add(c.playerId),o.push({playerId:c.playerId,...typeof c.score=="number"&&Number.isFinite(c.score)?{score:c.score}:{},...typeof c.rank=="number"&&Number.isSafeInteger(c.rank)&&c.rank>0?{rank:c.rank}:{}}))}return o.length?{standings:o,...Array.isArray(i.winners)?{winners:[...new Set(i.winners.filter(a=>typeof a=="string"&&s.has(a)))]}:{},...typeof i.draw=="boolean"?{draw:i.draw}:{},...typeof i.unit=="string"?{unit:i.unit}:{}}:null}function p(n,e,t={}){return Object.assign(new Error(e),{name:"CaisualError",code:n,...t})}function $(){return p("offline","Caisual services are unavailable.")}function pe(n){return typeof n=="object"&&n!==null&&"code"in n?n.code:null}async function Wt(n){let e={};try{e=await n.json()}catch{}return p(typeof e.error?.code=="string"?e.error.code:n.status===401?"invalid_ticket":"internal_error",typeof e.error?.message=="string"?e.error.message:`The request failed with status ${n.status}.`,{currentVersion:e.error?.currentVersion,roomVersion:e.error?.roomVersion})}function be(n,e,t,i){async function r(s,o,a,c){let m=new Headers({Authorization:`Bearer ${a}`}),h;if(c!==void 0){m.set("Content-Type","application/json");try{h=JSON.stringify(c)}catch{throw p("invalid_request","The value must be valid JSON.")}}try{return await t(new URL(e+s,n),{method:o,headers:m,body:h,credentials:"omit"})}catch{throw $()}}return async function(o,a,c,m=!1){let h;try{h=m?await i.rinnova():await i.ottieni()}catch{throw $()}let d=await r(o,a,h,c);if(d.status===401){try{h=await i.rinnova()}catch{throw $()}d=await r(o,a,h,c)}if(!d.ok)throw await Wt(d);try{return await d.json()}catch{throw p("internal_error","The service returned an invalid response.")}}}var Ee=.02,Ze=300,Ht=200,Qe=3e3,Kt=1e4,Yt=[1e3,2e3,4e3];function Xe(n){return Number.isNaN(n)?1:Math.min(1,Math.max(0,n))}function Zt(n){let e=globalThis,t=e.AudioContext??e.webkitAudioContext;return typeof RTCPeerConnection>"u"||typeof MediaStream>"u"||t===void 0||typeof navigator>"u"||navigator.mediaDevices?.getUserMedia===void 0||typeof document>"u"?null:{...n,creaPeerConnection:i=>new RTCPeerConnection(i),getUserMedia:i=>navigator.mediaDevices.getUserMedia(i),creaAudioContext:()=>new t,creaAudioElement:()=>document.createElement("audio"),creaMediaStream:i=>new MediaStream(i)}}var we=class{constructor(e,t,i){this.contesto=e;this.modeCorrente="none";this.stateCorrente="off";this.mutedCorrente=!1;this.speakingCorrente=!1;this.roster=[];this.gains=new Map;this.volumi=new Map;this.speakingPeers=new Map;this.ultimoAudio=new Map;this.zeroDa=new Map;this.timerZero=new Map;this.ascoltatoriPeers=new Set;this.ascoltatoriState=new Set;this.richieste=new Map;this.riproduzioni=new Map;this.sfuAttive=new Map;this.midGiocatori=new Map;this.negati=new Set;this.mesh=new Map;this.stream=null;this.tracciaMic=null;this.audioContext=null;this.analyser=null;this.peerSfu=null;this.sessioneSfu=null;this.connessioneSfuAttesa=!1;this.trasporto=null;this.intervalloAudio=null;this.timerConnessione=null;this.cancellaAttesaConnessione=null;this.timerRiconnessione=null;this.ultimoAudioMic=Number.NEGATIVE_INFINITY;this.sequenzaRichieste=0;this.generazione=0;this.tentativoRiconnessione=0;this.desiderata=!1;this.micDesiderato=!0;this.promessaIngresso=null;this.negoziazione=Promise.resolve();this.dipendenze=i??Zt(t)}get mode(){return this.modeCorrente}get state(){return this.stateCorrente}get mic(){return this.stateCorrente==="on"&&this.tracciaMic!==null}get muted(){return this.mutedCorrente}get speaking(){return this.speakingCorrente}get peers(){return this.copiaPeers()}async join(e={}){if(this.stateCorrente==="on")return;if(this.stateCorrente==="joining"){this.promessaIngresso!==null&&await this.promessaIngresso;return}if(this.stateCorrente==="reconnecting"&&this.desiderata)return;let t=this.scegliMic(e);this.verificaIngresso(t),this.micDesiderato=t,this.desiderata=!0,this.tentativoRiconnessione=0,this.aggiornaState("joining");let i=++this.generazione,r=this.completaIngresso(i);this.promessaIngresso=r;try{await r}finally{this.promessaIngresso===r&&(this.promessaIngresso=null)}}async completaIngresso(e){try{await this.entra(e)}catch(t){if(e!==this.generazione)return;throw this.desiderata=!1,this.chiudiRisorse(),this.aggiornaState("off"),this.mappaErrore(t)}}leave(){let e=this.desiderata||this.stateCorrente!=="off";this.desiderata=!1,this.generazione++,this.fermaRiconnessione(),e&&this.contesto.connessa()&&this.richiedi({t:"voice",op:"stop"}).catch(()=>{}),this.rifiutaRichieste(p("offline","Voice has stopped.")),this.chiudiRisorse(),this.aggiornaState("off")}mute(e=!0){if(this.stateCorrente!=="on"||this.tracciaMic===null)throw p("not_publishing","Join voice before changing mute.");this.mutedCorrente=e,this.tracciaMic.enabled=!e,e&&(this.speakingCorrente=!1),this.notificaPeers(),this.richiedi({t:"voice",op:"mute",muted:e}).catch(()=>{})}setVolume(e,t){let i=Xe(t);this.volumi.set(e,i),this.aggiornaGuadagno(e),this.notificaPeers()}onPeers(e){return this.ascoltatoriPeers.add(e),()=>{this.ascoltatoriPeers.delete(e)}}onState(e){return this.ascoltatoriState.add(e),()=>{this.ascoltatoriState.delete(e)}}ricevi(e){if("r"in e){let t=this.richieste.get(e.r);t!==void 0&&(this.richieste.delete(e.r),"error"in e?t.reject(p(e.error.code,e.error.message)):t.resolve(e));return}if(e.op==="roster"){this.negati.clear(),this.modeCorrente=e.mode;let t=new Set(e.peers.map(i=>i.id));this.roster=[...e.peers.map(i=>({...i,mic:!0})),...e.listeners.flatMap(i=>t.has(i)?[]:[{id:i,mic:!1,muted:!0}])];for(let i of this.roster)i.muted&&this.speakingPeers.set(i.id,!1);this.pulisciPeerAssenti(),this.contesto.rosterPronto(),this.notificaPeers(),this.accodaRiconciliazione();return}if(e.op==="gain"){this.negati.clear();for(let[t,i]of Object.entries(e.gains))this.gains.set(t,Xe(i)),this.aggiornaZero(t),this.aggiornaGuadagno(t);this.notificaPeers(),this.accodaRiconciliazione();return}if(e.op==="closed"){for(let t of e.mids){let i=this.midGiocatori.get(t);if(i===void 0)continue;let r=this.sfuAttive.get(i);r?.mid===t&&!this.riproduzioni.has(i)&&r.receiver?.track.stop(),r?.mid===t&&this.sfuAttive.delete(i),this.midGiocatori.delete(t),this.scollegaTraccia(i),this.negati.add(i)}this.notificaPeers();return}e.op==="signal"&&this.riceviSegnale(e.from,e.data)}giocatoriCambiati(){this.negati.clear();let e=new Set(this.contesto.giocatori().map(t=>t.id));for(let t of this.gains.keys()){if(e.has(t))continue;this.gains.delete(t),this.zeroDa.delete(t);let i=this.timerZero.get(t);i!==void 0&&this.dipendenze?.clearTimeout(i),this.timerZero.delete(t),this.aggiornaGuadagno(t)}this.notificaPeers(),this.accodaRiconciliazione()}socketDisconnesso(){this.sequenzaRichieste=0,this.rifiutaRichieste(p("offline","The room is reconnecting.")),this.desiderata&&(this.generazione++,this.chiudiRisorse(),this.tentativoRiconnessione=0,this.aggiornaState("reconnecting"))}socketRiconnesso(){this.sequenzaRichieste=0,this.desiderata&&this.stateCorrente==="reconnecting"&&this.programmaRiconnessione()}termina(){this.desiderata=!1,this.generazione++,this.fermaRiconnessione(),this.rifiutaRichieste(p("offline","The room connection ended.")),this.chiudiRisorse(),this.aggiornaState("off")}scegliMic(e){return e.mic!==void 0?e.mic:this.contesto.giocatori().find(i=>i.id===this.contesto.you())?.role!=="spectator"}verificaIngresso(e=this.micDesiderato){if(!this.contesto.connessa())throw p("offline","The room is not connected.");if(this.modeCorrente==="none")throw p("voice_disabled","Voice is disabled for this room.");if(this.contesto.giocatori().find(i=>i.id===this.contesto.you())?.role==="spectator"&&e)throw p("spectator","Spectators cannot publish voice.");if(this.dipendenze===null)throw p("unsupported","Voice is not supported in this browser.")}async entra(e){this.verificaIngresso();let t=this.richiediDipendenze(),i=t.creaAudioContext();if(this.audioContext=i,this.micDesiderato){let s;try{s=await t.getUserMedia({audio:!0})}catch(a){throw this.permessoNegato(a)?p("permission_denied","Microphone permission was denied."):p("voice_error","The microphone could not be opened.")}try{this.controllaGenerazione(e)}catch(a){for(let c of s.getTracks())c.stop();throw a}let o=s.getAudioTracks()[0];if(o===void 0)throw p("voice_error","The microphone has no audio track.");this.stream=s,this.tracciaMic=o,o.enabled=!this.mutedCorrente,this.preparaAnalizzatore(s)}try{await i.resume()}catch{}this.controllaGenerazione(e);let r=await this.richiedi({t:"voice",op:"ice"});if(this.controllaGenerazione(e),r.op!=="ice")throw p("voice_error","The voice service returned an invalid response.");if(this.modeCorrente=r.mode,r.mode==="none")throw p("voice_disabled","Voice is disabled for this room.");this.trasporto=r.transport,r.transport==="sfu"?await this.entraSfu(r.iceServers,e):await this.richiedi({t:"voice",op:"publish",mic:this.micDesiderato}),this.micDesiderato&&this.mutedCorrente&&await this.richiedi({t:"voice",op:"mute",muted:!0}),this.controllaGenerazione(e),this.tentativoRiconnessione=0,this.aggiornaState("on"),this.avviaMisuraAudio();for(let s of this.gains.keys())this.aggiornaZero(s);this.accodaRiconciliazione()}async entraSfu(e,t){let i=this.richiediDipendenze().creaPeerConnection({iceServers:e,bundlePolicy:"max-bundle"});this.peerSfu=i,i.ontrack=s=>{let o=s.transceiver.mid,a=o===null?void 0:this.midGiocatori.get(o);a!==void 0&&this.collegaTraccia(a,s.track,s.receiver)},this.osservaCaduta(i);let r;if(this.micDesiderato){let s=i.addTransceiver(this.richiediMic(),{direction:"sendonly"}),o=await i.createOffer();await i.setLocalDescription(o),this.controllaGenerazione(t);let a=s.mid,c=i.localDescription?.sdp;if(a===null||c===void 0)throw p("voice_error","The voice connection could not create an offer.");r=await this.richiedi({t:"voice",op:"session",sdp:c,mid:a})}else r=await this.richiedi({t:"voice",op:"session"});if(r.op!=="session")throw p("voice_error","The voice service returned an invalid response.");if(this.sessioneSfu=r.session,this.micDesiderato){if(r.sdp===null)throw p("voice_error","The voice service returned an invalid response.");await i.setRemoteDescription({type:"answer",sdp:r.sdp}),await this.attendiConnessione(i,t),this.connessioneSfuAttesa=!0;return}if(r.sdp!==null)throw p("voice_error","The voice service returned an invalid response.");this.publisherDesiderati().length>0&&await this.riconciliaSfu()}attendiConnessione(e,t){if(e.connectionState==="connected")return Promise.resolve();let i=this.richiediDipendenze();return new Promise((r,s)=>{let o=()=>{e.removeEventListener("connectionstatechange",a),this.timerConnessione!==null&&i.clearTimeout(this.timerConnessione),this.timerConnessione=null,this.cancellaAttesaConnessione=null},a=()=>{t!==this.generazione?(o(),s(p("offline","Voice was stopped."))):e.connectionState==="connected"?(o(),r()):(e.connectionState==="failed"||e.connectionState==="closed")&&(o(),s(p("voice_error","The voice connection failed.")))};e.addEventListener("connectionstatechange",a),this.cancellaAttesaConnessione=()=>{o(),s(p("offline","Voice was stopped."))},this.timerConnessione=i.setTimeout(()=>{o(),s(p("voice_error","The voice connection timed out."))},Kt)})}accodaRiconciliazione(){this.stateCorrente==="on"&&(this.negoziazione=this.negoziazione.then(async()=>{this.stateCorrente==="on"&&(this.trasporto==="sfu"?await this.riconciliaSfu():this.trasporto==="mesh"&&this.riconciliaMesh())}).catch(()=>this.avviaRiconnessione()))}async riconciliaSfu(){let e=this.sessioneSfu,t=this.peerSfu;if(e===null||t===null)return;let i=new Map(this.publisherDesiderati().map(m=>[m.id,m])),r=[];for(let[m,h]of this.sfuAttive){let d=i.get(m);d!==void 0&&d.session===h.session&&d.track===h.track||(r.push(h),this.riproduzioni.has(m)||h.receiver?.track.stop(),this.sfuAttive.delete(m),this.midGiocatori.delete(h.mid),this.scollegaTraccia(m))}r.length>0&&await this.richiedi({t:"voice",op:"close",session:e,mids:r.map(m=>m.mid)});let s=[...i.values()].filter(m=>!this.sfuAttive.has(m.id));if(s.length===0)return;let o;try{o=await this.richiedi({t:"voice",op:"subscribe",session:e,tracks:s.map(m=>({session:m.session,track:m.track}))})}catch(m){if(pe(m)!=="not_allowed")throw m;for(let h of s)this.negati.add(h.id);return}if(o.op!=="subscribe")throw p("voice_error","The voice service returned an invalid response.");for(let m of o.tracks){let h=s.find(d=>d.session===m.session&&d.track===m.track);m.error==="not_allowed"&&h!==void 0&&this.negati.add(h.id),!(m?.mid===null||m?.mid===void 0||m.error!==null||h===void 0)&&(this.midGiocatori.set(m.mid,h.id),this.sfuAttive.set(h.id,{session:h.session,track:h.track,mid:m.mid,receiver:null}))}await t.setRemoteDescription({type:"offer",sdp:o.sdp});let a=await t.createAnswer();await t.setLocalDescription(a);let c=t.localDescription?.sdp;if(c===void 0)throw p("voice_error","The voice answer is missing.");await this.richiedi({t:"voice",op:"answer",session:e,sdp:c}),this.connessioneSfuAttesa||(await this.attendiConnessione(t,this.generazione),this.connessioneSfuAttesa=!0)}riconciliaMesh(){let e=new Map(this.peerDesiderati().map(t=>[t.id,t]));for(let[t,i]of this.mesh)e.has(t)||(i.pc.close(),this.mesh.delete(t),this.scollegaTraccia(t));for(let t of e.values())this.mesh.has(t.id)||this.creaMesh(t)}creaMesh(e){let t=e.id,i=this.richiediDipendenze().creaPeerConnection(),r={pc:i,makingOffer:!1,ignoreOffer:!1,settingRemoteAnswer:!1,polite:this.contesto.you()>t,receiver:null};this.mesh.set(t,r),i.onicecandidate=s=>{s.candidate!==null&&this.inviaSegnale(t,{kind:"candidate",candidate:s.candidate.toJSON()})},r.polite||(i.onnegotiationneeded=()=>{this.offriMesh(t,r)}),i.ontrack=s=>{r.receiver=s.receiver,this.collegaTraccia(t,s.track,s.receiver)},this.osservaCaduta(i),this.micDesiderato?i.addTransceiver(this.richiediMic(),{direction:e.mic?"sendrecv":"sendonly"}):i.addTransceiver("audio",{direction:"recvonly"})}async offriMesh(e,t){try{t.makingOffer=!0;let i=await t.pc.createOffer();await t.pc.setLocalDescription(i);let r=t.pc.localDescription?.sdp;r!==void 0&&await this.inviaSegnale(e,{kind:"offer",sdp:r})}finally{t.makingOffer=!1}}async riceviSegnale(e,t){if(this.trasporto!=="mesh"||this.stateCorrente!=="on")return;let i=this.peerDesiderati().find(o=>o.id===e);if(i===void 0)return;this.mesh.has(e)||this.creaMesh(i);let r=this.mesh.get(e);if(r===void 0||typeof t!="object"||t===null||Array.isArray(t))return;let s=t;try{if(s.kind==="candidate"){r.ignoreOffer||await r.pc.addIceCandidate(s.candidate);return}if(s.kind!=="offer"&&s.kind!=="answer"||typeof s.sdp!="string")return;let o=!r.makingOffer&&(r.pc.signalingState==="stable"||r.settingRemoteAnswer),a=s.kind==="offer"&&!o;if(r.ignoreOffer=!r.polite&&a,r.ignoreOffer)return;if(r.settingRemoteAnswer=s.kind==="answer",await r.pc.setRemoteDescription({type:s.kind,sdp:s.sdp}),r.settingRemoteAnswer=!1,s.kind==="offer"){let c=await r.pc.createAnswer();await r.pc.setLocalDescription(c);let m=r.pc.localDescription?.sdp;m!==void 0&&await this.inviaSegnale(e,{kind:"answer",sdp:m})}}catch{this.avviaRiconnessione()}}async inviaSegnale(e,t){try{await this.richiedi({t:"voice",op:"signal",to:e,data:t})}catch(i){if(pe(i)!=="not_allowed")throw i;this.mesh.get(e)?.pc.close(),this.mesh.delete(e),this.scollegaTraccia(e),this.negati.add(e)}}peerDesiderati(){let e=this.contesto.you(),t=this.contesto.giocatori(),i=t.find(r=>r.id===e);return this.roster.filter(r=>{if(r.id===e||this.negati.has(r.id)||!this.micDesiderato&&!r.mic)return!1;if(this.modeCorrente==="team"){let s=t.find(o=>o.id===r.id);if(i?.role!=="spectator"&&s?.team!==i?.team)return!1}return!0})}publisherDesiderati(){return this.peerDesiderati().filter(e=>{if(!e.mic)return!1;let t=this.zeroDa.get(e.id);return t===void 0||this.richiediDipendenze().ora()-t<Qe})}aggiornaZero(e){let t=this.dipendenze;if(t===null)return;let i=this.timerZero.get(e);if(i!==void 0&&t.clearTimeout(i),this.timerZero.delete(e),(this.gains.get(e)??1)>0){this.zeroDa.delete(e);return}this.zeroDa.has(e)||this.zeroDa.set(e,t.ora());let r=t.ora()-(this.zeroDa.get(e)??t.ora()),s=t.setTimeout(()=>{this.timerZero.delete(e),this.accodaRiconciliazione()},Math.max(0,Qe-r));this.timerZero.set(e,s)}collegaTraccia(e,t,i){this.scollegaTraccia(e);let r=this.richiediDipendenze(),s=r.creaMediaStream([t]),o=this.richiediAudioContext().createMediaStreamSource(s),a=this.richiediAudioContext().createGain();o.connect(a),a.connect(this.richiediAudioContext().destination);let c=null;try{c=this.richiediAudioContext().createAnalyser(),c.fftSize=256,o.connect(c)}catch{c=null}let m=r.creaAudioElement();m.srcObject=s,m.muted=!0,m.playsInline=!0,m.play().catch(()=>{}),this.riproduzioni.set(e,{source:o,gain:a,analyser:c,audio:m,track:t,receiver:i});let h=this.sfuAttive.get(e);h!==void 0&&(h.receiver=i),this.aggiornaGuadagno(e)}scollegaTraccia(e){let t=this.riproduzioni.get(e);t!==void 0&&(t.source.disconnect(),t.gain.disconnect(),t.analyser?.disconnect(),t.track.stop(),t.audio.pause(),t.audio.srcObject=null,this.riproduzioni.delete(e),this.speakingPeers.delete(e),this.ultimoAudio.delete(e))}aggiornaGuadagno(e){let t=this.riproduzioni.get(e);t!==void 0&&(t.gain.gain.value=(this.volumi.get(e)??1)*(this.gains.get(e)??1))}preparaAnalizzatore(e){let t=this.richiediAudioContext(),i=t.createAnalyser();i.fftSize=256,t.createMediaStreamSource(e).connect(i),this.analyser=i}avviaMisuraAudio(){let e=this.richiediDipendenze();this.intervalloAudio!==null&&e.clearInterval(this.intervalloAudio),this.intervalloAudio=e.setInterval(()=>this.misuraAudio(),Ht)}misuraAudio(){let e=this.dipendenze;if(e===null)return;let t=!1;this.analyser!==null&&(t=this.livelloAnalizzatore(this.analyser)>Ee),t&&(this.ultimoAudioMic=e.ora());let i=!this.mutedCorrente&&e.ora()-this.ultimoAudioMic<=Ze;i!==this.speakingCorrente&&(this.speakingCorrente=i,this.notificaPeers());let r=!1;for(let s of this.copiaPeers()){let o=this.riproduzioni.get(s.id);this.livelloAnalizzatore(o?.analyser??null)>Ee?this.ultimoAudio.set(s.id,e.ora()):(o?.analyser===null||o?.analyser===void 0)&&(o?.receiver?.getSynchronizationSources?.()??[]).some(m=>(m.audioLevel??0)>Ee)&&this.ultimoAudio.set(s.id,e.ora());let a=!s.muted&&e.ora()-(this.ultimoAudio.get(s.id)??0)<=Ze;(this.speakingPeers.get(s.id)??!1)!==a&&(this.speakingPeers.set(s.id,a),r=!0)}r&&this.notificaPeers()}livelloAnalizzatore(e){let t=e;if(t?.getFloatTimeDomainData===void 0)return 0;let i=new Float32Array(t.fftSize);return t.getFloatTimeDomainData(i),Math.sqrt(i.reduce((r,s)=>r+s*s,0)/Math.max(1,i.length))}copiaPeers(){let e=this.contesto.you(),t=this.contesto.giocatori(),i=t.find(r=>r.id===e);return this.roster.flatMap(r=>{if(r.id===e)return[];if(this.modeCorrente==="team"){let s=t.find(o=>o.id===r.id);if(i?.role!=="spectator"&&s?.team!==i?.team)return[]}return[{id:r.id,mic:r.mic,muted:r.muted,speaking:r.mic&&!r.muted&&(this.speakingPeers.get(r.id)??!1),volume:this.volumi.get(r.id)??1,gain:this.gains.get(r.id)??1}]})}pulisciPeerAssenti(){let e=new Set(this.roster.map(t=>t.id));for(let t of this.speakingPeers.keys())e.has(t)||this.speakingPeers.delete(t);for(let t of this.zeroDa.keys()){if(e.has(t))continue;this.zeroDa.delete(t);let i=this.timerZero.get(t);i!==void 0&&this.dipendenze?.clearTimeout(i),this.timerZero.delete(t)}}osservaCaduta(e){e.addEventListener("connectionstatechange",()=>{this.stateCorrente==="on"&&(e.connectionState==="failed"||e.connectionState==="disconnected")&&this.avviaRiconnessione()})}avviaRiconnessione(){!this.desiderata||this.stateCorrente==="reconnecting"||(this.generazione++,this.rifiutaRichieste(p("voice_error","The voice connection was restarted.")),this.chiudiRisorse(),this.tentativoRiconnessione=0,this.aggiornaState("reconnecting"),this.programmaRiconnessione())}programmaRiconnessione(){if(!this.desiderata||!this.contesto.connessa()||this.timerRiconnessione!==null||this.stateCorrente!=="reconnecting")return;let e=Yt[this.tentativoRiconnessione];if(e===void 0){this.desiderata=!1,this.aggiornaState("off");return}this.tentativoRiconnessione++,this.timerRiconnessione=this.richiediDipendenze().setTimeout(()=>{this.timerRiconnessione=null;let t=++this.generazione;this.entra(t).catch(()=>{t!==this.generazione||!this.desiderata||(this.chiudiRisorse(),this.aggiornaState("reconnecting"),this.programmaRiconnessione())})},e)}fermaRiconnessione(){this.timerRiconnessione===null||this.dipendenze===null||(this.dipendenze.clearTimeout(this.timerRiconnessione),this.timerRiconnessione=null)}chiudiRisorse(){let e=this.dipendenze;if(this.cancellaAttesaConnessione?.(),this.cancellaAttesaConnessione=null,e!==null){this.intervalloAudio!==null&&e.clearInterval(this.intervalloAudio),this.timerConnessione!==null&&e.clearTimeout(this.timerConnessione);for(let t of this.timerZero.values())e.clearTimeout(t)}this.intervalloAudio=null,this.timerConnessione=null,this.timerZero.clear();for(let t of[...this.riproduzioni.keys()])this.scollegaTraccia(t);this.peerSfu?.close(),this.peerSfu=null;for(let t of this.mesh.values())t.pc.close();this.mesh.clear(),this.sfuAttive.clear(),this.midGiocatori.clear(),this.negati.clear();for(let t of this.stream?.getTracks()??[])t.stop();this.stream=null,this.tracciaMic=null,this.analyser=null,this.audioContext?.close().catch(()=>{}),this.audioContext=null,this.sessioneSfu=null,this.connessioneSfuAttesa=!1,this.trasporto=null,this.speakingCorrente=!1,this.ultimoAudioMic=Number.NEGATIVE_INFINITY,this.speakingPeers.clear(),this.ultimoAudio.clear(),this.negoziazione=Promise.resolve()}richiedi(e){if(!this.contesto.connessa())return Promise.reject(p("offline","The room is reconnecting."));let t=++this.sequenzaRichieste;return new Promise((i,r)=>{this.richieste.set(t,{resolve:i,reject:r});try{this.contesto.invia({...e,r:t})}catch(s){this.richieste.delete(t),r(s)}})}rifiutaRichieste(e){for(let t of this.richieste.values())t.reject(e);this.richieste.clear()}aggiornaState(e){if(e!==this.stateCorrente){this.stateCorrente=e;for(let t of this.ascoltatoriState)try{t(e)}catch{}}}notificaPeers(){let e=this.copiaPeers();for(let t of this.ascoltatoriPeers)try{t(e)}catch{}}controllaGenerazione(e){if(e!==this.generazione||!this.desiderata)throw p("offline","Voice was stopped.")}richiediDipendenze(){if(this.dipendenze===null)throw p("unsupported","Voice is not supported.");return this.dipendenze}richiediMic(){if(this.tracciaMic===null)throw p("voice_error","The microphone is not ready.");return this.tracciaMic}richiediAudioContext(){if(this.audioContext===null)throw p("voice_error","Audio is not ready.");return this.audioContext}permessoNegato(e){return typeof e=="object"&&e!==null&&"name"in e&&(e.name==="NotAllowedError"||e.name==="SecurityError")}mappaErrore(e){if(typeof e=="object"&&e!==null&&"code"in e){let t=e.code;return t==="voice_disabled"||t==="permission_denied"||t==="unsupported"||t==="spectator"||t==="offline"||t==="voice_error"?e:p("voice_error","Voice could not be started.")}return p("voice_error","Voice could not be started.")}};var ne=1,et=[1e3,2e3,4e3,8e3],Qt=6e4,Xt=5e3,ei=2e4,ti=500,ii=2e3,ni=new Set([4003,4004,4005,4006,4008,4009]);function Q(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)?n:null}function tt(n){let e=Q(n);return e!==null&&typeof e.roomId=="string"&&typeof e.code=="string"&&typeof e.join=="string"&&typeof e.url=="string"}function ri(n){let e=Q(n);return e!==null&&typeof e.roomId=="string"&&typeof e.code=="string"&&typeof e.watch=="string"&&typeof e.url=="string"}function oi(n){let e=Q(n),t=Q(e?.players);return e!==null&&typeof e.url=="string"&&Number.isInteger(e.timeoutMs)&&e.timeoutMs>=1e3&&e.timeoutMs<=3e5&&t!==null&&Number.isInteger(t.min)&&Number.isInteger(t.max)&&t.min>=1&&t.max>=t.min}function Z(n){return JSON.parse(JSON.stringify(n))}function _e(n,e){let t=Z(n);for(let i of e){if(i.path.length===0){if(i.op!=="set")return{ok:!1};t=Z(i.value);continue}let r=t,s=i.path;for(let a=0;a<s.length-1;a++){let c=s[a];if(Array.isArray(r)){if(typeof c!="number"||c>=r.length)return{ok:!1};r=r[c]}else{let m=Q(r);if(m===null||typeof c!="string"||!Object.hasOwn(m,c))return{ok:!1};r=m[c]}}let o=s.at(-1);if(Array.isArray(r)){if(i.op!=="set"||typeof o!="number"||o>=r.length)return{ok:!1};r[o]=Z(i.value)}else{let a=Q(r);if(a===null||typeof o!="string")return{ok:!1};if(i.op==="del"){if(!Object.hasOwn(a,o))return{ok:!1};delete a[o]}else Object.defineProperty(a,o,{configurable:!0,enumerable:!0,value:Z(i.value),writable:!0})}}return{ok:!0,state:t}}function si(n){let e=be(n.liveOrigin,"",n.fetcher,n.biglietto),t=async(o,a,c,m)=>{try{return await e(o,a,{...Q(c),n:n.n},m)}catch(h){if(h instanceof Error&&"code"in h&&["version_outdated","version_mismatch"].includes(String(h.code))){let d=Q(c),u=typeof d?.code=="string"?d.code.toUpperCase().replace(/[\\s-]/g,""):void 0,v=typeof d?.roomId=="string"?d.roomId:void 0;n.onVersionError?.(h,h.code==="version_mismatch"?{code:u,roomId:v,watch:o==="/rooms/watch"}:void 0)}throw h}};async function i(o,a,c=!1){let m=await t(o,"POST",a,c);if(!tt(m))throw p("internal_error","The room service returned an invalid response.");return m}async function r(o){let a=await t("/match","POST",{mode:o.mode,key:o.key});if(!oi(a))throw p("internal_error","The matchmaking service returned an invalid response.");return a}async function s(o,a=!1){let c=await t("/rooms/watch","POST",o,a);if(!ri(c))throw p("internal_error","The room service returned an invalid response.");return c}return{create:o=>i("/rooms",{mode:o}),joinCode:o=>i("/rooms/join",{code:o}),joinRoom:o=>i("/rooms/join",{roomId:o},!0),watchCode:o=>s({code:o}),watchRoom:o=>s({roomId:o},!0),match:r,flush:o=>t(`/rooms/${encodeURIComponent(o)}/flush`,"POST")}}var Se=class{constructor(e,t,i,r,s,o,a=!1){this.roomId=e;this.codice=t;this.dipendenze=r;this.api=s;this.segnalaStanza=o;this.spettatore=a;this.meta={host:null,mode:null,countdownAt:null,configuration:null,connection:"connecting",closedCode:null};this.metaListeners=new Set;this.connectionListeners=new Set;this.scoreListeners=new Set;this.scores=[];this.errorListeners=new Set;this.roleId=0;this.roleRequests=new Map;this.statoPubblico=null;this.statoSincronizzato=null;this.tickCorrente=0;this.tickRateCorrente=0;this.latenzaCorrente=null;this.ultimoInput=null;this.inputInviato=null;this.timerInput=null;this.ultimoInvioGioco=-1/0;this.inviiGioco=[];this.seedCorrente=0;this.statusCorrente="lobby";this.giocatoriCorrenti=[];this.youCorrente="";this.hostCorrente=null;this.resultCorrente=null;this.delaySpettatore=0;this.socket=null;this.seq=0;this.scartoOrario=0;this.timerPing=null;this.intervalloPing=null;this.timerRiconnessione=null;this.timerFlush=null;this.flushInCorso=!1;this.flushRichiesto=!1;this.ritardoIndice=0;this.tempoRiconnessione=0;this.resyncRichiesto=!1;this.terminata=!1;this.lasciata=!1;this.prontaRisolta=!1;this.welcomeRicevuto=!1;this.rosterRicevuto=!1;this.timerRoster=null;this.risolviPronta=()=>{};this.rifiutaPronta=()=>{};this.ascoltatoriStato=new Set;this.ascoltatoriGiocatori=new Set;this.ascoltatoriStatus=new Set;this.ascoltatoriMessaggi=new Set;this.replay=!1;this.promessaPronta=new Promise((c,m)=>{this.risolviPronta=c,this.rifiutaPronta=m}),this.voice=new we({invia:c=>this.invia(c),connessa:()=>this.socket?.readyState===ne&&this.welcomeRicevuto&&!this.terminata&&!this.lasciata,you:()=>this.youCorrente,giocatori:()=>this.copiaGiocatori(),rosterPronto:()=>{this.rosterRicevuto=!0,this.risolviProntaSePossibile()}},r,r.voce),a&&(this.rosterRicevuto=!0),this.apri(i)}get mode(){return this.meta.mode}get countdownAt(){return this.meta.countdownAt}get connection(){return this.meta.connection}get metadata(){return structuredClone(this.meta)}get queuedScores(){return structuredClone(this.scores)}onMetadata(e){return this.metaListeners.add(e),()=>this.metaListeners.delete(e)}onConnection(e){return this.connectionListeners.add(e),()=>this.connectionListeners.delete(e)}onError(e){return this.errorListeners.add(e),()=>this.errorListeners.delete(e)}onScoreQueued(e){return this.scoreListeners.add(e),()=>this.scoreListeners.delete(e)}metadataChanged(e){let t=this.meta.connection;this.meta={...this.meta,...e},this.notifica(this.metaListeners,this.metadata),t!==this.meta.connection&&this.notifica(this.connectionListeners,this.meta.connection)}initialMetadata(e){this.metadataChanged({host:e.host,mode:e.mode,countdownAt:e.countdownAt??null,rematch:e.rematch??null,configuration:e.configuration??null,connection:"connected",closedCode:null})}requestRole(e){if(typeof e!="string"||e.length<1||e.length>32)return Promise.reject(p("invalid_role","The role is not valid."));if(this.connection!=="connected"||this.status!=="playing"||!this.meta.configuration?.requestRole)return Promise.reject(p("role_change_unavailable","Roles cannot be requested right now."));if(this.roleRequests.size>=8)return Promise.reject(p("rate_limited","Too many role requests."));let t=++this.roleId;return new Promise((i,r)=>{let s=this.dipendenze.setTimeout(()=>{this.roleRequests.delete(t),r(p("timeout","The role request timed out."))},5e3);this.roleRequests.set(t,{resolve:i,reject:r,timer:s});try{this.invia({t:"request-role",r:t,role:e})}catch(o){this.dipendenze.clearTimeout(s),this.roleRequests.delete(t),r(o)}})}clearRoleRequests(){for(let e of this.roleRequests.values())this.dipendenze.clearTimeout(e.timer),e.reject(p("offline","The room connection ended."));this.roleRequests.clear()}disconnect(){if(this.lasciata)return;this.lasciata=!0;let e=this.socket;this.socket=null,this.voice.termina(),this.fermaInput(),this.fermaPing(),this.fermaRiconnessione(),this.clearRoleRequests(),this.timerRoster!==null&&this.dipendenze.clearTimeout(this.timerRoster),e?.close(1e3),this.segnalaStanza(null),this.metadataChanged({connection:"disconnected",closedCode:null}),this.prontaRisolta||(this.prontaRisolta=!0,this.rifiutaPronta(p("cancelled","The room was disconnected.")))}get role(){return this.spettatore?"spectator":this.giocatoriCorrenti.find(e=>e.id===this.youCorrente)?.role??null}get state(){return this.statoPubblico}get tick(){return this.tickCorrente}get tickRate(){return this.tickRateCorrente}get latency(){return this.latenzaCorrente}get seed(){return this.seedCorrente}get status(){return this.statusCorrente}get players(){return this.copiaGiocatori()}get you(){return this.youCorrente}get host(){return this.hostCorrente}get code(){return this.codice}get result(){return this.resultCorrente}get delayMs(){return this.delaySpettatore}pronta(){return this.promessaPronta}invite(){return{code:this.codice,url:new URL(`/r/${this.codice}`,this.dipendenze.appOrigin).href}}onState(e){return this.ascoltatoriStato.add(e),()=>{this.ascoltatoriStato.delete(e)}}onPlayers(e){return this.ascoltatoriGiocatori.add(e),()=>{this.ascoltatoriGiocatori.delete(e)}}onStatus(e){return this.ascoltatoriStatus.add(e),()=>{this.ascoltatoriStatus.delete(e)}}onMessage(e){return this.ascoltatoriMessaggi.add(e),()=>{this.ascoltatoriMessaggi.delete(e)}}send(e){if(this.statusCorrente==="finished")return;let t=this.seq+1;this.invia({t:"msg",seq:t,m:e}),this.seq=t,this.ultimoInvioGioco=this.dipendenze.ora(),this.inviiGioco=[...this.inviiGioco.slice(-29),this.ultimoInvioGioco]}input(e){if(!(this.terminata||this.lasciata||this.statusCorrente==="finished")){try{let t=JSON.stringify(e);if(t===void 0)throw new TypeError;this.ultimoInput=t}catch{throw p("invalid_request","Room input must be valid JSON.")}this.programmaInput()}}pulisciInput(){this.fermaInput(),this.ultimoInput=this.inputInviato=null,this.ultimoInvioGioco=-1/0,this.inviiGioco=[]}fermaInput(){this.timerInput!==null&&this.dipendenze.clearTimeout(this.timerInput),this.timerInput=null}programmaInput(){if(this.timerInput!==null||this.ultimoInput===null||this.ultimoInput===this.inputInviato||!this.welcomeRicevuto||this.socket?.readyState!==ne||this.terminata||this.lasciata)return;let e=this.dipendenze.ora(),i=1e3/(this.tickRateCorrente>0?Math.min(30,this.tickRateCorrente):30);this.inviiGioco=this.inviiGioco.filter(o=>e-o<1e3);let r=this.inviiGioco.length>=30?this.inviiGioco[0]+1e3:e,s=Number.isFinite(this.ultimoInvioGioco)?this.ultimoInvioGioco+i:e+i;this.timerInput=this.dipendenze.setTimeout(()=>{if(this.timerInput=null,this.ultimoInput===null||this.ultimoInput===this.inputInviato||!this.welcomeRicevuto||this.socket?.readyState!==ne||this.terminata||this.lasciata)return;let o=this.dipendenze.ora();if(o<this.ultimoInvioGioco+i||this.inviiGioco.filter(c=>o-c<1e3).length>=30){this.programmaInput();return}let a=this.ultimoInput;try{this.send(JSON.parse(a)),this.inputInviato=a}catch{}},Math.max(0,Math.ceil(Math.max(s,r)-e)))}aggiornaTickRate(e){e===void 0||!Number.isInteger(e)||e<0||e>60||e===this.tickRateCorrente||(this.tickRateCorrente=e,this.fermaInput(),this.programmaInput())}ready(e){this.invia({t:"ready",ready:e})}setRole(e){this.invia({t:"role",role:e})}setTeam(e){this.invia({t:"team",team:e})}start(){this.invia({t:"start"})}restart(){if(this.statusCorrente!=="finished")throw p("rematch_unavailable","This room is not waiting for a rematch.");this.invia({t:"restart"})}leave(){if(!this.lasciata){if(this.spettatore||this.voice.leave(),this.lasciata=!0,this.segnalaStanza(null),this.socket?.readyState===ne){let e=this.socket;this.invia({t:"leave"}),this.spettatore&&e.close(1e3)}this.termina(1e3)}}serverTime(){return this.dipendenze.ora()+this.scartoOrario}copiaGiocatori(){return this.giocatoriCorrenti.map(e=>({...e}))}notifica(e,...t){for(let i of e)try{i(...t)}catch{}}invia(e){if(this.socket?.readyState!==ne)throw p("offline","The room is reconnecting.");let t;try{t=JSON.stringify(e)}catch{throw p("invalid_request","Room messages must be valid JSON.")}this.socket.send(t)}apri(e){let t;try{t=this.dipendenze.apriSocket(e)}catch{this.programmaRiconnessione();return}this.socket=t,t.addEventListener("open",()=>{this.socket===t&&this.avviaPing()}),t.addEventListener("message",i=>{this.socket===t&&typeof i.data=="string"&&this.ricevi(i.data)}),t.addEventListener("close",i=>{this.socket===t&&this.chiuso(i.code,i.reason)})}avviaPing(){if(this.socket?.readyState!==ne||this.terminata||this.lasciata)return;let e=this.statusCorrente==="playing"?Xt:ei;this.timerPing!==null&&this.intervalloPing===e||(this.timerPing!==null&&this.dipendenze.clearInterval(this.timerPing),this.intervalloPing=e,this.timerPing=this.dipendenze.setInterval(()=>{if(this.socket?.readyState===ne)try{this.invia({t:"ping",c:this.dipendenze.ora()})}catch{}},e))}fermaPing(){this.timerPing!==null&&(this.dipendenze.clearInterval(this.timerPing),this.timerPing=null,this.intervalloPing=null)}ricevi(e){let t;try{let i=JSON.parse(e),r=Q(i);if(r===null||typeof r.t!="string")return;t=r}catch{return}try{if(t.t==="watching")this.riceviWatching(t);else if(t.t==="welcome")this.riceviWelcome(t);else if(t.t==="replay-ready"&&/^[A-Za-z0-9_-]{22}$/.test(t.id))this.metadataChanged({replayId:t.id});else if(t.t==="players")this.riceviGiocatori(t.players,t.host);else if(t.t==="status")this.riceviStatus(t);else if(t.t==="state")this.riceviDiff(t);else if(t.t==="snapshot")this.riceviSnapshot(t);else if(t.t==="msg")this.notifica(this.ascoltatoriMessaggi,Z(t.m));else if(t.t==="pong")this.riceviPong(t);else if(t.t==="error")this.notifica(this.errorListeners,{code:t.code,message:t.message});else if(t.t==="flush")this.richiediFlush();else if(t.t==="score-queued"&&!this.spettatore)this.scores.push(structuredClone(t.score)),this.scores=this.scores.slice(-32),this.notifica(this.scoreListeners,structuredClone(t.score));else if(t.t==="role-result"){let i=this.roleRequests.get(t.r);i&&(this.dipendenze.clearTimeout(i.timer),this.roleRequests.delete(t.r),t.ok?i.resolve():i.reject(p(t.code??"role_change_refused","The role change was not accepted.")))}else t.t==="voice"&&this.voice.ricevi(t)}catch{(t.t==="state"||t.t==="snapshot")&&this.chiediResync()}}riceviWatching(e){let t=e.room;!this.spettatore||t.id!==this.roomId||(this.aggiornaTickRate(t.tickRate),this.seedCorrente=t.seed,this.hostCorrente=t.host,this.statusCorrente=t.status,this.avviaPing(),this.resultCorrente=Z(t.result??null),t.status==="finished"&&this.pulisciInput(),this.giocatoriCorrenti=e.players.map(i=>({...i})),this.delaySpettatore=e.delayMs,this.aggiornaStato(e.state,t.tick,t.serverTime),this.scartoOrario=t.serverTime-this.dipendenze.ora(),this.resyncRichiesto=!1,this.welcomeRicevuto=!0,this.ritardoIndice=0,this.tempoRiconnessione=0,this.notifica(this.ascoltatoriGiocatori,this.copiaGiocatori()),this.notifica(this.ascoltatoriStatus,this.statusCorrente,this.resultCorrente,t.serverTime),this.initialMetadata(t),this.programmaInput(),this.risolviProntaSePossibile())}riceviWelcome(e){let t=e.room;t.id===this.roomId&&(this.youCorrente=e.you,this.aggiornaTickRate(t.tickRate),this.seedCorrente=t.seed,this.hostCorrente=t.host,this.statusCorrente=t.status,this.avviaPing(),this.resultCorrente=Z(t.result??null),t.status==="finished"&&this.pulisciInput(),this.giocatoriCorrenti=e.players.map(i=>({...i})),this.aggiornaStato(e.state,t.tick,t.serverTime),this.scartoOrario=t.serverTime-this.dipendenze.ora(),this.resyncRichiesto=!1,this.welcomeRicevuto=!0,!this.rosterRicevuto&&this.timerRoster===null&&(this.timerRoster=this.dipendenze.setTimeout(()=>{this.timerRoster=null,this.rosterRicevuto=!0,this.risolviProntaSePossibile()},ii)),this.ritardoIndice=0,this.tempoRiconnessione=0,this.notifica(this.ascoltatoriGiocatori,this.copiaGiocatori()),this.voice.giocatoriCambiati(),this.voice.socketRiconnesso(),this.notifica(this.ascoltatoriStatus,this.statusCorrente,this.resultCorrente,t.serverTime),this.initialMetadata(t),this.programmaInput(),this.risolviProntaSePossibile())}riceviGiocatori(e,t){this.giocatoriCorrenti=e.map(i=>({...i})),t!==void 0?this.hostCorrente=t:this.giocatoriCorrenti.some(i=>i.id===this.hostCorrente&&i.connected)||(this.hostCorrente=this.giocatoriCorrenti.find(i=>i.connected)?.id??null),this.metadataChanged({host:this.hostCorrente}),this.notifica(this.ascoltatoriGiocatori,this.copiaGiocatori()),this.voice.giocatoriCambiati()}riceviStatus(e){(e.status==="playing"||e.status==="countdown"||e.status==="lobby")&&this.metadataChanged({replayId:null}),this.statusCorrente=e.status,e.host!==void 0&&(this.hostCorrente=e.host),this.resultCorrente=Z(e.result),e.status==="finished"&&(this.pulisciInput(),this.clearRoleRequests()),e.status==="ended"?(this.terminata=!0,this.clearRoleRequests(),this.segnalaStanza(null),this.spettatore||this.voice.termina(),this.fermaPing(),this.fermaRiconnessione(),this.fermaInput(),this.ultimoInput=null):this.avviaPing(),this.metadataChanged({rematch:e.rematch??null,host:this.hostCorrente,countdownAt:e.countdownAt??(e.status==="countdown"?e.at:null),...e.status==="ended"?{connection:"ended",closedCode:4004}:{}}),this.notifica(this.ascoltatoriStatus,this.statusCorrente,this.resultCorrente,e.at)}riceviDiff(e){if(this.aggiornaTickRate(e.tickRate),e.base!==this.tickCorrente){this.chiediResync();return}let t=_e(this.statoSincronizzato,e.patch);if(!t.ok){this.chiediResync();return}this.resyncRichiesto=!1,this.aggiornaStato(t.state,e.tick,e.serverTime)}riceviSnapshot(e){e.tick<this.tickCorrente||(this.aggiornaTickRate(e.tickRate),this.resyncRichiesto=!1,this.aggiornaStato(e.state,e.tick,e.serverTime))}aggiornaStato(e,t,i){this.statoSincronizzato=Z(e),this.statoPubblico=Z(e),this.tickCorrente=t,this.notifica(this.ascoltatoriStato,this.statoPubblico,t,i)}chiediResync(){if(!(this.resyncRichiesto||this.socket?.readyState!==ne)){this.resyncRichiesto=!0;try{this.invia({t:"resync"})}catch{this.resyncRichiesto=!1}}}riceviPong(e){let t=this.dipendenze.ora();if(!Number.isFinite(e.c)||!Number.isFinite(e.s)||e.c>t)return;let i=t-e.c;this.latenzaCorrente=this.latenzaCorrente===null?i:this.latenzaCorrente*.8+i*.2,this.scartoOrario=e.s-(e.c+t)/2}chiuso(e,t){if(this.socket=null,this.welcomeRicevuto=!1,this.latenzaCorrente=null,this.fermaInput(),this.inputInviato=null,this.ultimoInvioGioco=-1/0,this.inviiGioco=[],this.fermaPing(),!(this.lasciata||this.terminata)){if(ni.has(e)){let i=e===4009&&t==="message_too_large"?"message_too_large":void 0;i&&this.notifica(this.errorListeners,{code:i,message:"The room message is too large."}),this.termina(e,i);return}this.clearRoleRequests(),this.spettatore||this.voice.socketDisconnesso(),this.programmaRiconnessione()}}programmaRiconnessione(){if(this.terminata||this.lasciata||this.timerRiconnessione!==null)return;this.metadataChanged({connection:"reconnecting"});let e=Math.min(this.ritardoIndice,et.length-1),t=et[e];if(this.tempoRiconnessione+t>Qt){this.termina("timeout");return}this.ritardoIndice++,this.tempoRiconnessione+=t,this.timerRiconnessione=this.dipendenze.setTimeout(()=>{this.timerRiconnessione=null,this.riconnetti()},t)}async riconnetti(){if(!(this.terminata||this.lasciata))try{let e=this.spettatore?await this.api.watchRoom(this.roomId):await this.api.joinRoom(this.roomId);if(this.terminata||this.lasciata)return;let t=this.codice!==e.code;this.codice=e.code,t&&this.prontaRisolta&&!this.terminata&&!this.lasciata&&this.segnalaStanza({code:this.codice}),this.apri(e.url)}catch(e){e instanceof Error&&"code"in e&&["version_mismatch","version_outdated","room_not_found"].includes(String(e.code))?(this.notifica(this.errorListeners,{code:String(e.code),message:e.message}),this.termina(4004,String(e.code))):this.programmaRiconnessione()}}fermaRiconnessione(){this.timerRiconnessione!==null&&(this.dipendenze.clearTimeout(this.timerRiconnessione),this.timerRiconnessione=null)}termina(e,t){this.clearRoleRequests(),this.metadataChanged({connection:e===1e3?"disconnected":e===4006?"replaced":"closed",closedCode:typeof e=="number"?e:null});let i={closed:e},r=this.statusCorrente!=="ended"||JSON.stringify(this.resultCorrente)!==JSON.stringify(i);if(this.terminata=!0,this.fermaInput(),this.ultimoInput=null,this.segnalaStanza(null),this.statusCorrente="ended",this.resultCorrente=i,this.spettatore||this.voice.termina(),this.fermaPing(),this.fermaRiconnessione(),r&&this.notifica(this.ascoltatoriStatus,"ended",i,this.serverTime()),!this.prontaRisolta){this.prontaRisolta=!0;let o=t??(typeof e=="number"?{4003:"kicked",4004:"room_ended",4005:"version_closed",4006:"replaced",4008:"rate_limited",4009:"invalid_request"}[e]??"offline":"offline");this.rifiutaPronta(p(o,"The room connection ended."))}}risolviProntaSePossibile(){this.prontaRisolta||!this.welcomeRicevuto||!this.rosterRicevuto||(this.timerRoster!==null&&(this.dipendenze.clearTimeout(this.timerRoster),this.timerRoster=null),this.prontaRisolta=!0,!this.spettatore&&!this.terminata&&!this.lasciata&&this.segnalaStanza({code:this.codice}),this.risolviPronta())}richiediFlush(){this.flushRichiesto=!0,!(this.flushInCorso||this.timerFlush!==null)&&(this.timerFlush=this.dipendenze.setTimeout(()=>{this.timerFlush=null,this.eseguiFlush()},ti))}async eseguiFlush(){if(!(this.flushInCorso||!this.flushRichiesto)){this.flushInCorso=!0,this.flushRichiesto=!1;try{await this.api.flush(this.roomId)}catch{}finally{this.flushInCorso=!1,this.flushRichiesto&&this.richiediFlush()}}}};function he(n=null){return{replay:!1,invited:n,reload(){typeof window<"u"&&window.location.reload()},onError(){return()=>{}},async create(){throw $()},async join(){throw $()},async watch(){throw $()},async match(){throw $()}}}function it(n,e){let t,i=new Set,r=si({...n,onVersionError(d,u){t=u;for(let v of i)try{v(d)}catch{}}}),s=!1,o=null,a=d=>{let u=d?.code??null;s&&u===o||(s=!0,o=u,n.segnalaStanza?.(d))},c=async d=>{let u=new Se(d.roomId,d.code,d.url,n,r,a);return await u.pronta(),u},m=async d=>{let u=new Se(d.roomId,d.code,d.url,n,r,()=>{},!0);return await u.pronta(),{role:"spectator",replay:!1,get mode(){return u.mode},get countdownAt(){return u.countdownAt},get connection(){return u.connection},get metadata(){return u.metadata},onMetadata:v=>u.onMetadata(v),onConnection:v=>u.onConnection(v),disconnect:()=>u.disconnect(),get state(){return u.state},get tick(){return u.tick},get tickRate(){return u.tickRate},get latency(){return u.latency},get seed(){return u.seed},get status(){return u.status},get players(){return u.players},get host(){return u.host},get code(){return u.code},get result(){return u.result},get delayMs(){return u.delayMs},onState:v=>u.onState(v),onPlayers:v=>u.onPlayers(v),onStatus:v=>u.onStatus(v),onMessage:v=>u.onMessage(v),leave:()=>{u.leave()},serverTime:()=>u.serverTime()}},h=(d,u)=>new Promise((v,x)=>{let w,N=!1,_=()=>{w.removeEventListener("message",W),w.removeEventListener("close",U),w.removeEventListener("error",F),u.signal?.removeEventListener("abort",B)},j=()=>{try{w.close(1e3)}catch{}},z=(A,M)=>{N||(N=!0,_(),M&&j(),x(A))};function B(){z(p("cancelled","The matchmaking search was cancelled."),!0)}function U(){z($(),!1)}function F(){z($(),!0)}function W(A){let M=null;try{M=typeof A.data=="string"?Q(JSON.parse(A.data)):null}catch{}if(M===null||typeof M.t!="string"){z(p("internal_error","The matchmaking service sent an invalid message."),!0);return}if(M.t==="waiting"){if(!Number.isInteger(M.players)||!Number.isInteger(M.min)||!Number.isInteger(M.max)){z(p("internal_error","The matchmaking service sent an invalid message."),!0);return}try{u.onWaiting?.({players:M.players,min:M.min,max:M.max})}catch{}return}if(M.t==="matched"){if(!tt(M)){z(p("internal_error","The matchmaking service sent an invalid message."),!0);return}N=!0,_(),j(),v(M);return}if(M.t==="no_match"){z(p("no_match","No match was found before the timeout."),!0);return}if(M.t==="error"){z(p(typeof M.code=="string"?M.code:"internal_error",typeof M.message=="string"?M.message:"The matchmaking service could not complete the search."),!0);return}M.t!=="pong"&&z(p("internal_error","The matchmaking service sent an invalid message."),!0)}try{w=n.apriSocket(d)}catch{x($());return}w.addEventListener("message",W),w.addEventListener("close",U),w.addEventListener("error",F),u.signal?.addEventListener("abort",B,{once:!0}),u.signal?.aborted===!0&&B()});return{replay:!1,invited:e,reload(){n.reload?.(t)},onError(d){return i.add(d),()=>{i.delete(d)}},async create(d){return c(await r.create(d.mode))},async join(d){let u=d??e;if(u==null||u.length===0)throw p("invalid_request","A room invitation code is required.");return c(await r.joinCode(u))},async watch(d){if(typeof d!="string"||d.length===0)throw p("invalid_request","A room invitation code is required.");return m(await r.watchCode(d))},async match(d){let u=()=>d.signal?.aborted===!0;if(u())throw p("cancelled","The matchmaking search was cancelled.");let v=await r.match(d);if(u())throw p("cancelled","The matchmaking search was cancelled.");return c(await h(v.url,d))}}}var K=()=>p("replay_invalid","The replay is incomplete or invalid.");function q(n,...e){for(let t of n)try{t(...e)}catch{}}function le(n,e){return n.add(e),()=>{n.delete(e)}}var li={now:()=>performance.now(),setInterval:(n,e)=>globalThis.setInterval(n,e),clearInterval:n=>globalThis.clearInterval(n)},ze=class{constructor(e,t,i=li){this.index=e;this.events=t;this.clock=i;this.replay=!0;this.role="spectator";this.delayMs=0;this.code="";this.latency=null;this.countdownAt=null;this.cursor=1;this.position=0;this.paused=!0;this.speedValue=1;this.timer=null;this.last=0;this.lastEmitted=-1/0;this.disconnected=!1;this.checkpoints=[];this.states=new Set;this.playersListeners=new Set;this.statuses=new Set;this.metadataListeners=new Set;this.connections=new Set;this.playbackListeners=new Set;this.step=()=>{let e=this.clock.now();this.position=Math.min(this.index.durationMs,this.position+Math.max(0,e-this.last)*this.speedValue),this.last=e,this.advance(!0),this.position>=this.index.durationMs&&this.pause(),e-this.lastEmitted>=100&&(this.lastEmitted=e,q(this.playbackListeners,this.playback))};if(t[0]?.message.t!=="start"||t[0].at!==0||t[0].message.room.id!==e.roomId)throw K();let r=t[0].message;this.picture=structuredClone({room:r.room,players:r.players,state:r.state,result:null}),this.checkpoints.push({cursor:1,at:0,picture:structuredClone(this.picture)});let s=Math.max(1,Math.ceil(t.length/32)),o=0;for(let a=1;a<t.length;a++){let c=t[a];if(!Number.isFinite(c.at)||c.at<o||c.at>e.durationMs||c.message.t==="start")throw K();this.apply(c.message,!1),a%s===0&&this.checkpoints.push({cursor:a+1,at:c.at,picture:structuredClone(this.picture)}),o=c.at}this.seek(0)}get mode(){return this.picture.room.mode}get connection(){return this.disconnected?"disconnected":"connected"}get metadata(){return{host:this.host,mode:this.mode,countdownAt:null,configuration:null,rematch:null,connection:this.connection,closedCode:null}}get state(){return structuredClone(this.picture.state)}get tick(){return this.picture.room.tick}get tickRate(){return this.picture.room.tickRate}get seed(){return this.picture.room.seed}get status(){return this.picture.room.status}get players(){return structuredClone(this.picture.players)}get host(){return this.picture.room.host}get result(){return structuredClone(this.picture.result)}get playback(){return{positionMs:this.position,durationMs:this.index.durationMs,speed:this.speedValue,paused:this.paused,truncated:this.index.truncated}}serverTime(){return this.index.startedAt+this.position}onState(e){return le(this.states,e)}onStatus(e){return le(this.statuses,e)}onPlayers(e){return le(this.playersListeners,e)}onMetadata(e){return le(this.metadataListeners,e)}onConnection(e){return le(this.connections,e)}onMessage(e){return()=>{}}onPlayback(e){return le(this.playbackListeners,e)}apply(e,t){let i=this.picture;switch(e.t){case"start":throw K();case"snapshot":i.state=structuredClone(e.state);break;case"state":{if(i.room.tick!==e.base)throw K();let r=_e(i.state,e.patch);if(!r.ok)throw K();i.state=r.state;break}case"players":i.players=structuredClone(e.players),e.host!==void 0&&(i.room.host=e.host),t&&q(this.playersListeners,this.players);return;case"status":i.room.status=e.status,i.result=structuredClone(e.result),e.host!==void 0&&(i.room.host=e.host),t&&q(this.statuses,this.status,this.result,e.at);return;default:throw K()}i.room.tick=e.tick,i.room.serverTime=e.serverTime,i.room.tickRate=e.tickRate??i.room.tickRate,t&&q(this.states,this.state,this.tick,e.serverTime)}seek(e){if(!Number.isFinite(e)||this.disconnected)return;this.position=Math.max(0,Math.min(e,this.index.durationMs));let t=[...this.checkpoints].reverse().find(i=>i.at<=this.position);this.picture=structuredClone(t.picture),this.cursor=t.cursor,this.advance(!1),this.last=this.clock.now(),q(this.playersListeners,this.players),q(this.statuses,this.status,this.result,this.serverTime()),q(this.states,this.state,this.tick,this.serverTime()),q(this.metadataListeners,this.metadata),this.position>=this.index.durationMs&&this.pause(),q(this.playbackListeners,this.playback)}advance(e){for(;this.events[this.cursor]&&this.events[this.cursor].at<=this.position;)this.apply(this.events[this.cursor++].message,e)}play(){!this.paused||this.disconnected||this.index.durationMs===0||(this.position>=this.index.durationMs&&this.seek(0),this.paused=!1,this.last=this.clock.now(),this.timer=this.clock.setInterval(this.step,16),q(this.playbackListeners,this.playback))}pause(){this.timer!==null&&this.clock.clearInterval(this.timer),this.timer=null,this.paused=!0,q(this.playbackListeners,this.playback)}speed(e){if(![.5,1,2,4].includes(e))throw K();this.paused||this.step(),this.speedValue=e,q(this.playbackListeners,this.playback)}disconnect(){this.disconnected||(this.pause(),this.disconnected=!0,q(this.connections,this.connection),q(this.metadataListeners,this.metadata))}leave(){this.disconnect()}};async function rt(n,e){let t=await n(e,{credentials:"omit",cache:"no-store"});if(!t.ok)throw p("replay_unavailable","This replay is no longer available.");let i=await t.json();if(i.format!==1||!de.test(i.id)||!Number.isInteger(i.chunks)||i.chunks<1||i.chunks>Math.ceil(10485760/524288)+1||!Number.isInteger(i.bytes)||i.bytes>10485760||i.bytes<1||!Number.isFinite(i.startedAt)||!Number.isFinite(i.durationMs)||i.durationMs<0||i.durationMs>18e5)throw K();let r=[],s=0;for(let o=0;o<i.chunks;o++){let a=await n(`${e}?chunk=${o}`,{credentials:"omit",cache:"no-store"});if(!a.ok)throw K();let c=await a.text();if(s+=new TextEncoder().encode(c).byteLength,s>i.bytes||!c.endsWith(`\n`))throw K();for(let m of c.trimEnd().split(`\n`))r.push(JSON.parse(m))}if(s!==i.bytes)throw K();try{return new ze(i,r)}catch{throw K()}}var ci=["en","it","es","fr","de","pt","ja"];function ot(n){let e=G(n);return e&&ci.includes(e.split("-")[0])?e:"en"}function st(n,e,t="/"){let i,r=t.match(/^\\/rt\\/[^/]+\\/[1-9][0-9]*\\//)?.[0]??"/";return()=>i??(i=(async()=>{let s={};try{let o=await n(`${r}__caisual/text/${encodeURIComponent(e)}.json`);if(o.ok){let a=await o.json();Ge(a)&&(s=a)}}catch{}return(o,a={})=>Object.hasOwn(s,o)?s[o].replace(/\\{([^{}]+)\\}/g,(m,h)=>Object.hasOwn(a,h)?String(a[h]):m):o})())}var at="caisual-session-v1";function lt(n){let e=D(n);return!e||typeof e.code!="string"||!/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(e.code)||!(e.mode===void 0||e.mode===null||typeof e.mode=="string")?null:{version:1,code:e.code,mode:typeof e.mode=="string"?e.mode:null,updatedAt:typeof e.updatedAt=="number"&&Number.isFinite(e.updatedAt)?e.updatedAt:0}}function ct(n,e){let t=null,i=!1,r=Promise.resolve(),s=async()=>{let a={version:1,imported:!0,resume:t};return r=r.catch(()=>{}).then(async()=>{try{await n.set(at,a),i=!1}catch(c){throw i=!0,c}finally{e()}}),r},o=(async()=>{try{let a=await n.get(at),c=D(a);if(c?.version===1&&c.imported===!0)t=lt(c.resume);else{let m=await n.get("resume");t=lt(m),(a!==null||m!==null)&&await s()}}catch{i=!0}e()})();return{loaded:o,get value(){return t===null?null:{...t}},get error(){return i},async set(a){await o,t=a,e(),await s()}}}function X(n,e){for(let t of n)try{t(e)}catch{}}function ut(n,e=null,t=n.connected,i=null){let r=i!==null||e?.manifest.overlay?.version===1,s=e?.manifest,o={kind:"idle"},a=!1,c=0,m=0,h=null,d=null,u=null,v=null,x=[],w=!1,N="",_={inputBlocked:r,reservedRects:[],safeArea:{top:0,right:0,bottom:0,left:0}},j=new Set,z=new Set,B=new Set,U=new Set,F=new Set,W=new Set,A=null,M=()=>({local:i===null,rooms:i===null&&t,overlay:r,requestRole:o.kind==="room"&&o.room.metadata.configuration?.requestRole===!0});function ue(){if(o.kind!=="room"||!s||s.voice==="none")return null;let l=o.room,y=l.voice;return!y||y.mode==="none"||l.players.find(R=>R.id===l.you)?.role==="spectator"?null:{mode:y.mode,state:y.state,mic:y.mic,muted:y.muted,speaking:y.speaking,peers:y.peers.map(({id:R,mic:C,muted:k,speaking:ae,volume:oe})=>({id:R,mic:C,muted:k,speaking:ae,volume:oe}))}}function ee(){let l=o.kind==="room"||o.kind==="watch"?o.room:null,y=l?.metadata.configuration,R=l?Ye(l.result,l.players.map(k=>k.id)):null,C=s&&l&&(l.mode===null||s.modes.some(k=>k.id===l.mode))?Ue(s,l.mode):{players:{min:1,max:1},lobby:!1};return{kind:h??(o.kind==="idle"?a?"home":"boot":o.kind),id:o.kind==="idle"?null:o.id,mode:h?d:o.kind==="local"?o.mode:l?.mode??null,localStatus:o.kind==="local"?o.status:null,ready:a,capabilities:M(),room:l?{...l===i?{replay:i.playback}:{},...l.metadata.replayId?{replayId:l.metadata.replayId}:{},...l.metadata.rematch?.keepSetup||l.metadata.rematch?.autoStart?{rematch:l.metadata.rematch}:{},code:l.code,mode:l.mode,status:l.status,host:l.host,you:o.kind==="room"?o.room.you:null,players:l.players.map(k=>({id:k.id,name:k.name,guest:k.guest,role:k.role,team:k.team,ready:k.ready,connected:k.connected})),...R?{result:R}:{},countdownAt:l.countdownAt,connection:l.connection,closedCode:l.metadata.closedCode,limits:{...y?.players??C.players},lobby:y?.lobby??C.lobby,persistent:y?.persistent??s?.persistent??!1,delayMs:o.kind==="watch"?o.room.delayMs:null,requestRole:y?.requestRole??!1}:null,voice:h?null:ue(),waiting:u?{...u}:null,resume:A?.value??null,resumeError:A?.error??!1}}function I(){if(w)return;let l=ee(),y=JSON.stringify(l);y!==N&&(N=y,X(B,l))}function te(){X(j,{...o}),I()}function Y(){if(o.kind!=="room")throw p("no_room","There is no active player room.");return o.room}function re(){let l=Y();if(l.players.find(y=>y.id===l.you)?.role==="spectator")throw p("spectator","Spectators cannot use voice controls.");if(!s||s.voice==="none"||l.voice.mode==="none")throw p("voice_disabled","Voice is disabled for this room.");return l.voice}function H(){c++,v?.abort(),v=null,h=null,u=null,I()}function g(l){x.splice(0).forEach(y=>y()),(o.kind==="room"||o.kind==="watch")&&(l?o.room.disconnect():o.room.leave()),o={kind:"idle"},te()}async function f(l){A?.value?.code===l&&await A.set(null).catch(()=>{})}async function L(l,y,R){if(R!==c||w)throw l.leave(),p("cancelled","The operation was cancelled.");g(!1),o=y?{kind:"watch",room:l,id:String(++m)}:{kind:"room",room:l,id:String(++m)};let C=l;if(x=[C.onPlayers(I),C.onMetadata(()=>{C.connection==="disconnected"&&(o.kind==="room"||o.kind==="watch")&&o.room===C?(x.splice(0).forEach(k=>k()),!y&&C.metadata.closedCode===1e3&&f(C.code),o={kind:"idle"},te()):I()}),C.onStatus(()=>{I(),!y&&C.connection==="ended"&&f(C.code)})],l===i&&x.push(i.onPlayback(I)),!y){let k=l,ae=o.id;k.voice&&x.push(k.voice.onState(I),k.voice.onPeers(I)),x.push(k.onError(oe=>X(F,{sessionId:ae,error:{...oe}}))),x.push(k.onScoreQueued(oe=>X(W,{...oe})));for(let oe of k.queuedScores)X(W,{...oe})}return h=null,u=null,te(),!y&&A&&C.connection!=="ended"&&await A.set({version:1,code:C.code,mode:C.mode,updatedAt:n.time.now()}).catch(()=>{}),l}async function S(l,y,R,C=!1){H();let k=c;v=new AbortController,h=l,d=y,I();try{let ae=await R(v.signal,k);if(await L(ae,C,k),k!==c||w)throw p("cancelled","The operation was cancelled.");return ae}finally{k===c&&(h=null,u=null,v=null,I())}}let b=n.room,O=b.onError(l=>{!r||w||(l.code==="version_mismatch"?b.reload():l.code==="version_outdated"&&X(F,{sessionId:o.kind==="idle"?null:o.id,error:l}))}),T=i||!r?b:{invited:b.invited,reload:()=>b.reload(),onError:l=>b.onError(l),create(l){return s&&ve(s,l.mode)?Promise.reject(p("invalid_request","Local modes cannot create rooms.")):S("attaching",l.mode,()=>b.create(l))},join(l){return S("attaching",null,()=>b.join(l))},watch(l){return S("attaching",null,()=>b.watch(l),!0)},match(l){return s&&ve(s,l.mode)?Promise.reject(p("invalid_request","Local modes cannot use matchmaking.")):S("matching",l.mode,(y,R)=>{let C=()=>{c===R&&H()};return l.signal?.addEventListener("abort",C,{once:!0}),l.signal?.aborted&&C(),b.match({...l,signal:y,onWaiting(k){R===c&&(u={...k},I(),l.onWaiting?.(k))}}).finally(()=>l.signal?.removeEventListener("abort",C))})}};return r&&!i&&(A=ct(n.save,I)),i&&(a=!0,L(i,!0,c)),{session:{get current(){return{...o}},get capabilities(){return M()},onChange(l){return j.add(l),X(new Set([l]),{...o}),()=>{j.delete(l)}},ready(){w||a||(a=!0,I())},finish(){if(o.kind==="room"||o.kind==="watch")throw p("not_local","Only a local session can be finished by the client.");o.kind==="local"&&(o={...o,status:"ended"},te())}},overlay:{open(l){if(!["home","room","invite","friends","voice","boards"].includes(l))throw p("invalid_request","Unknown overlay panel.");r&&X(U,l)},onChange(l){return z.add(l),X(new Set([l]),structuredClone(_)),()=>{z.delete(l)}}},rooms:T,snapshot:ee,serverTime:()=>o.kind==="room"||o.kind==="watch"?o.room.serverTime():n.time.now(),onState(l){return B.add(l),l(ee()),()=>{B.delete(l)}},onOpen(l){return U.add(l),()=>{U.delete(l)}},onError(l){return F.add(l),()=>{F.delete(l)}},onScore(l){return W.add(l),()=>{W.delete(l)}},async execute(l){if(!r)throw p("overlay_disabled","This game uses its own room flow.");if(l.op==="overlay.view"){if(!Oe(l.args))throw p("invalid_request","The overlay geometry is invalid.");if(_={...structuredClone(l.args),safeArea:{top:0,right:0,bottom:0,left:0,...l.args.safeArea}},typeof document<"u")for(let[y,R]of Object.entries(_.safeArea))document.documentElement.style.setProperty(`--caisual-safe-${y}`,`${R}px`);X(z,structuredClone(_));return}if(l.sessionId!==void 0&&l.sessionId!==(o.kind==="idle"?null:o.id))throw p("session_replaced","The active session changed.");if(l.op.startsWith("replay.")&&(!i||o.kind!=="watch"||o.room!==i||l.sessionId!==o.id))throw p("session_replaced","The replay is no longer active.");if(i&&!l.op.startsWith("replay.")&&!["session.leave","session.disconnect"].includes(l.op))throw p("replay_readonly","Replays are read only.");if(l.op.startsWith("voice.")&&l.sessionId!==(o.kind==="idle"?null:o.id))throw p("session_replaced","The active session changed.");if(!a)throw p("game_not_ready","The game is still loading.");switch(l.op){case"replay.play":i.play();return;case"replay.pause":i.pause();return;case"replay.seek":i.seek(l.args.positionMs);return;case"replay.speed":i.speed(l.args.speed);return;case"local.start":{if(!s||!ve(s,l.args.mode))throw p("invalid_mode","This is not a local mode.");H();let y=c;if(o.kind==="room"&&await f(o.room.code),y!==c||w)throw p("cancelled","The operation was cancelled.");g(!1),o={kind:"local",id:String(++m),mode:l.args.mode,status:"playing"},te();return}case"room.create":await T.create(l.args);return;case"room.join":await T.join(l.args.code);return;case"room.watch":await T.watch(l.args.code);return;case"room.match":{let y=s?.modes.find(C=>C.id===l.args.mode),R=l.args.key??y?.matchmaking?.defaults;if(!R)throw p("invalid_request","Matchmaking needs a complete key.");await T.match({mode:l.args.mode,key:R});return}case"voice.join":{let y=Y();if(await re().join(),o.kind!=="room"||o.room!==y)throw p("session_replaced","The active session changed.");I();return}case"voice.mute":re().mute(l.args.muted),I();return;case"voice.leave":re().leave(),I();return;case"voice.setVolume":{let y=re();if(!y.peers.some(R=>R.id===l.args.playerId))throw p("voice_peer_missing","This voice participant is no longer available.");y.setVolume(l.args.playerId,l.args.volume),I();return}case"room.ready":Y().ready(l.args.ready);return;case"room.role":Y().setRole(l.args.role);return;case"room.requestRole":await Y().requestRole(l.args.role);return;case"room.team":Y().setTeam(l.args.team);return;case"room.start":Y().start();return;case"room.restart":Y().restart();return;case"session.cancel":H();return;case"session.resume":{await S("attaching",null,async y=>{if(await A?.loaded,y.aborted)throw p("cancelled","The operation was cancelled.");if(!A?.value)throw p("no_resume","There is no saved room.");return b.join(A.value.code)});return}case"session.disconnect":{H();let y=c;if(o.kind==="room"&&o.room.connection!=="ended"&&A&&await A.set({version:1,code:o.room.code,mode:o.room.mode,updatedAt:n.time.now()}),y!==c||w)throw p("cancelled","The operation was cancelled.");g(!0);return}case"session.leave":{H();let y=c;if(o.kind==="room"&&await f(o.room.code),y!==c||w)throw p("cancelled","The operation was cancelled.");g(!1);return}}},dispose(){O(),H(),g(!0),w=!0,j.clear(),z.clear(),B.clear(),U.clear(),W.clear(),F.clear()}}}function dt(n,e,t){let i=!0,r=!1,s=e.onChange(a=>{i=a.shortcutEnabled!==!1,r=a.inputBlocked}),o=a=>{let c=a.target;!i||r||a.repeat||a.key!=="Tab"||!a.shiftKey||a.ctrlKey||a.altKey||a.metaKey||c?.closest?.(\'input,textarea,select,[contenteditable="true"]\')||(a.preventDefault(),a.stopImmediatePropagation(),t())};return n.addEventListener("keydown",o,!0),()=>{s(),n.removeEventListener("keydown",o,!0)}}function mt(n,e,t){let i=!1,r=0,s=0,o=0,a=new Map,c=d=>{if(!i)try{n.postMessage(d)}catch{}},m=[...e.configuration.manifest.overlay&&typeof window<"u"?[dt(window,t.overlay,()=>c({type:"caisual:overlay-shortcut",v:1,epoch:e.epoch}))]:[],t.onState(d=>c({type:"caisual:overlay-state",v:1,epoch:e.epoch,seq:++r,serverTime:t.serverTime(),state:d})),t.onOpen(d=>c({type:"caisual:overlay-open",v:1,epoch:e.epoch,panel:d})),t.onError(({sessionId:d,error:u})=>c({type:"caisual:overlay-error",v:1,epoch:e.epoch,sessionId:d,error:u})),t.onScore(d=>c({type:"caisual:overlay-score",v:1,epoch:e.epoch,score:d}))],h=d=>{let u=D(d.data);if(u?.type!=="caisual:overlay"||u.epoch!==e.epoch||i)return;let v={type:"caisual:overlay-response",v:1,epoch:e.epoch,requestId:typeof u.requestId=="string"?u.requestId:""};if(!Ke(u)){c({...v,ok:!1,error:{code:"invalid_request",message:"The overlay request is invalid."}});return}let x=JSON.stringify([u.op,u.args,u.sessionId]),w=a.get(u.requestId);if(w){w.fingerprint!==x?c({...v,ok:!1,error:{code:"duplicate_request",message:"The request id was already used."}}):w.response.then(c);return}if(Number(u.requestId)<=s||o>=32){c({...v,ok:!1,error:{code:"stale_request",message:"The request is stale or too many requests are pending."}});return}s=Number(u.requestId),o++;let N=Promise.resolve().then(()=>t.execute(u)).then(()=>({...v,ok:!0}),_=>({...v,ok:!1,error:{code:typeof D(_)?.code=="string"?D(_).code:"internal_error",message:_ instanceof Error?_.message:"The operation could not be completed."}}));a.set(u.requestId,{fingerprint:x,response:N}),N.then(_=>{if(o--,c(_),a.size>64)for(let j of a.keys())Number(j)<s-64&&a.delete(j)})};return n.addEventListener("message",h),n.start(),()=>{i=!0,n.removeEventListener("message",h),m.forEach(d=>d()),t.dispose(),a.clear()}}function pt(n,e,t){let i=be(n,"/api/kit",e,t);return{me:()=>i("/me","GET"),saveSet:(r,s)=>i(`/saves/${encodeURIComponent(r)}`,"PUT",{value:s}),async saveGet(r){try{return(await i(`/saves/${encodeURIComponent(r)}`,"GET")).value}catch(s){if(pe(s)==="not_found")return null;throw s}},async saveRemove(r){await i(`/saves/${encodeURIComponent(r)}`,"DELETE")},async saveList(){return(await i("/saves","GET")).saves},async boardSubmit(r,s,o){let a=await i("/scores","POST",{board:r,score:s,daily:o});return{accepted:!0,best:a.best,rank:a.rank,day:a.day,verified:a.verified}},async boardTop(r,s){if(s.day!==void 0&&(!me(s.day)||s.daily===!1))throw p("invalid_request","day must be a real UTC date and cannot be combined with daily: false.");let o=new URLSearchParams;s.day!==void 0&&o.set("day",s.day),s.daily&&o.set("daily","1"),s.limit!==void 0&&o.set("limit",String(s.limit)),s.guests&&o.set("guests","1");let a=o.size===0?"":`?${o.toString()}`,{day:c,entries:m,me:h}=await i(`/scores/${encodeURIComponent(r)}${a}`,"GET");return{day:c,entries:m,me:h}}}}function ce(n){return(Math.floor(n/864e5)+1)*864e5}function Re(n,e,t){let i=new Set,r={...n},s,o=!1;function a(){!i.size||s!==void 0||o||(s=setTimeout(c,Math.max(0,Math.min(2147483647,r.expiresAt-e()))),s.unref?.())}async function c(){s=void 0,o=!0;try{let m=await t(),h=m.day!==r.day;if(r={...m},h)for(let d of[...i])try{d({...m})}catch{}}catch{}finally{o=!1,r.expiresAt<=e()&&(r.expiresAt=e()+3e4),a()}}return{...n,random:ft(n.seed),rng:()=>ft(n.seed),onChange(m){return i.add(m),a(),()=>{i.delete(m),!i.size&&s!==void 0&&(clearTimeout(s),s=void 0)}}}}function Ve(n){return new Date(n).toISOString().slice(0,10)}async function Le(n,e,t){let i=new TextEncoder().encode(`caisual:${n}:${e}`),r=new Uint8Array(await t.digest("SHA-256",i));return(r[0]??0)*16777216+((r[1]??0)<<16)+((r[2]??0)<<8)+(r[3]??0)>>>0}function ft(n){let e=n>>>0;return()=>{e=e+1831565813>>>0;let t=e;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}}function ke(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)?n:null}function ht(n,e){return ke(n)?.type===e}function ui(n){if(typeof n!="string")return null;try{let e=new URL(n);return e.origin===n&&(e.protocol==="https:"||e.protocol==="http:")?n:null}catch{return null}}function gt(n,e,t=3e3){return new Promise(i=>{let r=!1,s=globalThis.crypto.randomUUID(),o=h=>{r||(r=!0,n.removeEventListener("message",c),n.clearTimeout(m),i(h))},a=()=>{n.parent.postMessage({type:"caisual:ready",instance:s,overlayVersion:1},e)},c=h=>{if(h.origin!==e||h.source!==n.parent)return;if(ht(h.data,"caisual:ready?")){a();return}if(!ht(h.data,"caisual:hello"))return;let d=ke(h.data),u=h.ports[0];if(typeof d?.ticket!="string"||!Ae(d.n)||u===void 0)return;u.start();let v=He(d.overlay),x=w=>Array.isArray(w)?w.map(G).filter(N=>N!==null):void 0;o({...v?{overlay:v}:{},...G(d.language)?{language:G(d.language)}:{},uiLanguage:G(d.uiLanguage)??void 0,languagePreferences:x(d.languagePreferences),gameLanguages:x(d.gameLanguages),...typeof d.replay=="string"&&de.test(d.replay)?{replay:d.replay}:{},ticket:d.ticket,n:d.n,live:ui(d.live),invite:typeof d.invite=="string"?d.invite:null,porta:u})};n.addEventListener("message",c);let m=n.setTimeout(()=>o(null),t);a()})}function di(n){let e=n.split(".")[1];if(e===void 0)return null;let t=e.replace(/-/g,"+").replace(/_/g,"/").padEnd(Math.ceil(e.length/4)*4,"=");try{let i=ke(JSON.parse(globalThis.atob(t)));return typeof i?.exp=="number"&&Number.isFinite(i.exp)?i.exp*1e3:null}catch{return null}}function mi(n,e,t,i){return new Promise((r,s)=>{let o=!1,a=h=>{o||(o=!0,n.removeEventListener("message",c),e.clearTimeout(m),h===null?s(new Error("Ticket refresh timed out.")):r(h))},c=h=>{let d=ke(h.data),u=d?.aud===void 0?"portal":d.aud;d?.type==="caisual:ticket"&&u===i&&typeof d.ticket=="string"&&a(d.ticket)};n.addEventListener("message",c);let m=e.setTimeout(()=>a(null),t);try{n.postMessage(i==="live"?{type:"caisual:ticket",aud:"live"}:{type:"caisual:ticket"})}catch{a(null)}})}function $e(n,e,t,i,r=3e3,s="portal"){let o=n,a=null,c=()=>{if(a!==null)return a;let h=mi(e,t,r,s).then(d=>(o=d,d)).finally(()=>{a===h&&(a=null)});return a=h,h};return{async ottieni(){if(o===null)return c();let m=di(o);return m!==null&&m-i()<3e4?c():o},rinnova:c}}var se="caisual:save:",pi=/^[a-z0-9][a-z0-9_-]{0,31}$/;function De(n){if(!pi.test(n))throw p("invalid_request","Save keys must use lowercase letters, numbers, underscores, or hyphens.")}function yt(n){if(n===null)return null;try{return JSON.parse(n)}catch{return null}}function vt(n){let e=[];for(let t=0;t<n.length;t++){let i=n.key(t);i?.startsWith(se)&&e.push(i.slice(se.length))}return e}function fi(n,e){let t=()=>{if(n===null)throw $();return n};return{async set(i,r){De(i);let s=t(),o=JSON.stringify({value:r}),a=new TextEncoder().encode(o).byteLength;if(a>262144)throw p("payload_too_large","The save is larger than 262144 bytes.");if(s.getItem(se+i)===null&&vt(s).length>=64)throw p("save_limit","A game can store at most 64 save keys.");let c={value:r,bytes:a,updatedAt:e()};return s.setItem(se+i,JSON.stringify(c)),{key:i,bytes:a,updatedAt:c.updatedAt}},async get(i){return De(i),yt(t().getItem(se+i))?.value??null},async remove(i){De(i),t().removeItem(se+i)},async list(){let i=t();return vt(i).flatMap(r=>{let s=yt(i.getItem(se+r));return s===null?[]:[{key:r,bytes:s.bytes,updatedAt:s.updatedAt}]}).sort((r,s)=>r.key.localeCompare(s.key))}}}async function xe(n,e=null){let t=n.ora(),i=Ve(t),r=await Le(n.hostname,i,n.subtle);return{connected:!1,player:{id:"local",name:"Guest",guest:!0},daily:Re({day:i,seed:r,expiresAt:ce(t)},n.ora,async()=>{let s=n.ora(),o=Ve(s);return{day:o,seed:await Le(n.hostname,o,n.subtle),expiresAt:ce(s)}}),time:{now:n.ora},save:fi(n.archivio,n.ora),board:{async submit(){return{accepted:!1,reason:"offline",verified:!1}},async top(s,o={}){if(o.day!==void 0&&(!me(o.day)||o.daily===!1))throw p("invalid_request","day must be a real UTC date and cannot be combined with daily: false.");return{day:o.day??(o.daily?i:null),entries:[],me:null}}},room:he(e)}}function hi(n){let e=n?.querySelector(\'meta[name="caisual-app"]\')?.getAttribute("content");if(e==null)return null;try{let t=new URL(e);return t.origin===e&&(t.protocol==="https:"||t.protocol==="http:")?e:null}catch{return null}}function gi(){try{return typeof localStorage>"u"?null:localStorage}catch{return null}}function yi(){return{finestra:typeof window>"u"?null:window,documento:typeof document>"u"?null:document,fetcher:(n,e)=>globalThis.fetch(n,e),archivio:gi(),language:typeof navigator>"u"?"en":navigator.language,pathname:typeof location>"u"?"/":location.pathname,hostname:typeof location>"u"?"":location.hostname,subtle:globalThis.crypto.subtle,ora:Date.now,sonda:()=>We()}}async function vi(n){let e=hi(n.documento),t=n.finestra===null||n.finestra.parent===n.finestra;if(e===null||t)return bt(n);let i=await gt(n.finestra,e,n.timeoutHandshake);if(i===null)return bt(n);if(i.replay){let u=i.overlay?.configuration.manifest.id;if(!u)throw new Error("The replay game is missing.");let v=await rt(n.fetcher,`${e}/api/replays/${encodeURIComponent(u)}/${i.replay}`),x=await xe(n),w=he();return x.connected=!0,x.room=Object.assign(v,{invited:null,reload:()=>w.reload(),onError:w.onError,create:w.create,join:w.join,match:w.match,watch:async()=>v}),Me(x,i,n,v)}let r=$e(i.ticket,i.porta,n.finestra,n.ora,n.timeoutRinnovo,"portal"),s=pt(e,n.fetcher,r),o=n.ora(),a;try{a=await s.me()}catch{let u=await xe(n,i.invite);return Me(u,i,n)}let c=n.ora(),m=a.serverTime-(o+c)/2,h=i.live===null?he(i.invite):it({appOrigin:e,n:i.n,reload:u=>i.porta.postMessage({type:"caisual:reload",target:u}),liveOrigin:i.live,fetcher:n.fetcher,biglietto:$e(null,i.porta,n.finestra,n.ora,n.timeoutRinnovo,"live"),apriSocket(u){if(n.apriSocket!==void 0)return n.apriSocket(u);if(typeof WebSocket>"u")throw $();return new WebSocket(u)},ora:n.ora,setTimeout:(u,v)=>globalThis.setTimeout(u,v),clearTimeout:u=>globalThis.clearTimeout(u),setInterval:(u,v)=>globalThis.setInterval(u,v),clearInterval:u=>globalThis.clearInterval(u),voce:n.voce,segnalaStanza(u){try{i.porta.postMessage({type:"caisual:room",room:u})}catch{}}},i.invite),d={connected:!0,player:a.player,daily:Re({day:a.day,seed:a.seed,expiresAt:a.expiresAt??ce(a.serverTime)},()=>n.ora()+m,async()=>{let u=await s.me();return{day:u.day,seed:u.seed,expiresAt:u.expiresAt??ce(u.serverTime)}}),time:{now:()=>n.ora()+m},save:{set:(u,v)=>s.saveSet(u,v),get:u=>s.saveGet(u),remove:u=>s.saveRemove(u),list:()=>s.saveList()},board:{async submit(u,v,x={}){try{return await s.boardSubmit(u,v,x.daily===!0)}catch(w){if(typeof w=="object"&&w!==null&&"code"in w&&w.code==="offline")return{accepted:!1,reason:"offline",verified:!1};throw w}},top:(u,v={})=>s.boardTop(u,v)},room:h};return Me(d,i,n)}function Me(n,e,t,i=null){let r=ut(n,e?.overlay?.configuration??null,n.connected&&e?.live!=null,i);if(e?.overlay){let c=mt(e.porta,e.overlay,r);r.session.capabilities.overlay&&typeof window<"u"&&t?.finestra===window&&window.addEventListener("pagehide",c,{once:!0})}let s=e?.languagePreferences?.length?e.languagePreferences:[e?.language??t?.language??"en"],o=je(s,e?.gameLanguages??(e?.overlay?Ce(e.overlay.configuration.manifest):void 0)),a=ot(e?.uiLanguage??e?.language??t?.language);return{...n,player:{...n.player,language:o,uiLanguage:a},text:st(t?.fetcher??globalThis.fetch,o,t?.pathname),room:r.rooms,session:r.session,overlay:r.overlay}}async function bt(n){return Me(await xe(n),void 0,n)}function wt(){return{webgl2:!1,webgpu:!1,wasm:!1,threads:!1,isolated:!1,gpu:"none",memoryMb:null,cores:null,mobile:!1,tier:"low"}}async function bi(n){let e;try{return await Promise.race([Promise.resolve().then(n).catch(()=>wt()),new Promise(t=>{e=globalThis.setTimeout(()=>t(wt()),1500)})])}finally{e!==void 0&&globalThis.clearTimeout(e)}}function St(n=yi()){let e=null;return{connect(){return e??(e=Promise.all([vi(n),bi(n.sonda)]).then(([t,i])=>({...t,device:i}))),e}}}var Rt=St();globalThis.caisual=Rt;var xr=Rt;export{Rt as caisual,xr as default};\n');
5032
5272
  return;
5033
5273
  }
5034
5274
  const textMatch = url.pathname.match(/^\/__caisual\/text\/([^/]+)\.json$/);
@@ -5051,13 +5291,13 @@ var DevService = class {
5051
5291
  return;
5052
5292
  }
5053
5293
  const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
5054
- const candidate = resolve(this.clientRoot, relativePath);
5055
- if (relative2(this.clientRoot, candidate).startsWith(`..${sep2}`) || candidate === this.clientRoot) {
5294
+ const candidate = resolve2(this.clientRoot, relativePath);
5295
+ if (relative2(this.clientRoot, candidate).startsWith(`..${sep3}`) || candidate === this.clientRoot) {
5056
5296
  sendError(response, new DevHttpError(404, "not_found", "The game file was not found."));
5057
5297
  return;
5058
5298
  }
5059
5299
  const real = await fs3.realpath(candidate).catch(() => null);
5060
- if (real === null || real !== this.clientRoot && !real.startsWith(`${this.clientRoot}${sep2}`)) {
5300
+ if (real === null || real !== this.clientRoot && !real.startsWith(`${this.clientRoot}${sep3}`)) {
5061
5301
  sendError(response, new DevHttpError(404, "not_found", "The game file was not found."));
5062
5302
  return;
5063
5303
  }
@@ -5083,7 +5323,7 @@ var DevService = class {
5083
5323
  }
5084
5324
  if (url.pathname === "/__caisual/overlay/v1.js" && (request.method === "GET" || request.method === "HEAD")) {
5085
5325
  response.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
5086
- response.end(request.method === "HEAD" ? void 0 : '// ../contracts/src/slug.ts\nvar NOMI_RISERVATI = [\n "www",\n "api",\n "app",\n "play",\n "live",\n "multi",\n "cdn",\n "assets",\n "static",\n "mail",\n "mx",\n "ns1",\n "ns2",\n "autodiscover",\n "_dmarc",\n "admin",\n "login",\n "account",\n "auth",\n "pay",\n "secure",\n "support",\n "help",\n "blog",\n "status",\n "dev",\n "staging",\n "test",\n "caisual",\n "shipz"\n];\nvar RISERVATI = new Set(NOMI_RISERVATI);\nvar SLUG_NUOVO = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nvar SLUG_STORICO = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;\nfunction isValidSlug(value) {\n return value.length >= 3 && value.length <= 32 && SLUG_NUOVO.test(value) || SLUG_STORICO.test(value);\n}\nfunction isReservedSlug(value) {\n return RISERVATI.has(value);\n}\n\n// ../contracts/src/i18n.ts\nfunction normalizeLanguage(value) {\n if (typeof value !== "string" || value.length > 128) return null;\n try {\n return Intl.getCanonicalLocales(value)[0] ?? null;\n } catch {\n return null;\n }\n}\nfunction manifestLanguages(manifest) {\n return manifest.languages?.length ? [...manifest.languages] : [manifest.language ?? "en"];\n}\nfunction languageFallbacks(language, defaultLanguage = "en") {\n const result = [];\n let tag = normalizeLanguage(language);\n while (tag) {\n result.push(tag);\n const parts = tag.split("-");\n parts.pop();\n if (parts.at(-1)?.length === 1) parts.pop();\n tag = parts.join("-");\n }\n result.push(normalizeLanguage(defaultLanguage) ?? defaultLanguage);\n return [...new Set(result)];\n}\nfunction resolveText(value, language, defaultLanguage = "en", key = "") {\n if (typeof value === "string") return value;\n if (value) {\n for (const tag of languageFallbacks(language, defaultLanguage)) {\n const name = Object.keys(value).find((name2) => name2.toLowerCase() === tag.toLowerCase());\n if (name !== void 0 && typeof value[name] === "string") return value[name];\n }\n }\n return key;\n}\n\n// ../contracts/src/manifest.ts\nfunction risolviModalita(manifest, mode) {\n const scelta = mode === null ? void 0 : manifest.modes.find((voce) => voce.id === mode);\n if (mode !== null && scelta === void 0) throw new Error("The selected game mode does not exist.");\n return { players: { ...scelta?.players ?? manifest.players }, lobby: scelta?.lobby ?? manifest.lobby };\n}\nfunction risolviPresentazione(manifest, mode, language = manifestLanguages(manifest)[0]) {\n risolviModalita(manifest, mode);\n const scelta = manifest.modes.find((voce) => voce.id === mode);\n return {\n execution: scelta?.execution ?? null,\n label: resolveText(scelta?.label, language, manifestLanguages(manifest)[0], scelta?.id ?? manifest.name ?? "Play"),\n instructions: resolveText(scelta?.instructions, language, manifestLanguages(manifest)[0]) || null\n };\n}\nvar TETTO_GIOCATORI = 24;\nvar RITARDO_SPETTATORI_MS = 3e3;\nvar MASSIMO_CLASSIFICHE = 32;\nvar CAMPI = /* @__PURE__ */ new Set([\n "overlay",\n "manifest",\n "id",\n "name",\n "description",\n "cover",\n "card",\n "icon",\n "screenshots",\n "tags",\n "languages",\n "language",\n "platform",\n "orientation",\n "input",\n "visibility",\n "network",\n "isolated",\n "requires",\n "players",\n "lobby",\n "persistent",\n "spectators",\n "boards",\n "roles",\n "teams",\n "voice",\n "modes"\n]);\nvar INPUT = /* @__PURE__ */ new Set(["keyboard", "mouse", "touch", "gamepad"]);\nvar PLATFORM = /* @__PURE__ */ new Set(["desktop", "mobile", "both"]);\nvar ORIENTATION = /* @__PURE__ */ new Set(["landscape", "portrait"]);\nvar VISIBILITY = /* @__PURE__ */ new Set(["public", "unlisted"]);\nvar VOICE = /* @__PURE__ */ new Set(["none", "room", "team", "proximity"]);\nvar PERFORMANCE = /* @__PURE__ */ new Set(["light", "medium", "heavy"]);\nvar TAG = /^[a-z0-9-]+$/;\nvar ID_INTERNO = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nvar CAMPO_MATCHMAKING = /^[a-z0-9][a-z0-9-]{0,31}$/;\nvar ID_CLASSIFICA = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nfunction oggetto(value) {\n if (typeof value !== "object" || value === null || Array.isArray(value)) return null;\n return value;\n}\nfunction percorsoRelativo(value) {\n if (value === "" || value.startsWith("/") || value.includes("\\\\") || value.includes("\\0")) return false;\n if (value.includes("?") || value.includes("#")) return false;\n const parti = value.split("/");\n if (parti.some((parte) => parte === "" || parte === "." || parte === "..")) return false;\n try {\n const decoded = parti.map((parte) => decodeURIComponent(parte));\n return !decoded.some((parte) => parte === "" || parte === "." || parte === ".." || parte.includes("/"));\n } catch {\n return false;\n }\n}\nfunction hostValido(value) {\n if (value.length === 0 || value.length > 253) return false;\n if (value.includes("://") || /[/:?#@]/.test(value)) return false;\n const parti = value.split(".");\n return parti.every(\n (parte) => parte.length >= 1 && parte.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(parte)\n );\n}\nfunction interoTra(value, min, max) {\n return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;\n}\nfunction stringaDefault(dati, campo, valoreDefault, errori) {\n const value = dati[campo];\n if (value === void 0) return valoreDefault;\n if (typeof value !== "string") {\n errori.push(`${campo}: must be a string.`);\n return valoreDefault;\n }\n return value;\n}\nfunction testoFacoltativo(value, key, max, path, errors) {\n if (value[key] === void 0) return void 0;\n const check = (text2, field2) => {\n if (typeof text2 !== "string" || text2.trim().length === 0 || text2.trim().length > max || /[\\r\\n\\u0000-\\u001f]/.test(text2)) {\n errors.push(`${field2}: must contain 1-${max} characters on one line.`);\n return void 0;\n }\n return text2.trim();\n };\n const text = value[key], field = path ? `${path}.${key}` : key;\n if (typeof text === "string") return check(text, field);\n const translations = oggetto(text);\n if (!translations || Object.keys(translations).length === 0) {\n errors.push(`${field}: must be a string or a non-empty language-to-text object.`);\n return void 0;\n }\n const result = {};\n for (const [raw, text2] of Object.entries(translations)) {\n const tag = normalizeLanguage(raw);\n if (!tag) {\n errors.push(`${field}.${raw}: must be a BCP 47 language tag.`);\n continue;\n }\n if (Object.hasOwn(result, tag)) errors.push(`${field}.${raw}: duplicate language.`);\n const checked = check(text2, `${field}.${raw}`);\n if (checked !== void 0) result[tag] = checked;\n }\n return result;\n}\nfunction validaManifest(valore) {\n const errori = [];\n const dati = oggetto(valore);\n if (dati === null) return { ok: false, errori: ["manifest: must be a JSON object."] };\n for (const campo of Object.keys(dati)) {\n if (!CAMPI.has(campo)) errori.push(`${campo}: unknown field.`);\n }\n if (dati.manifest === void 0) errori.push("manifest: is required and must be 1.");\n else if (dati.manifest !== 1) errori.push("manifest: must be exactly 1.");\n const id = stringaDefault(dati, "id", "", errori);\n if (dati.id === void 0) errori.push("id: is required.");\n else if (typeof dati.id === "string") {\n if (!isValidSlug(id)) {\n errori.push("id: must be 3-32 lowercase ASCII letters or digits with internal hyphens; historical UUID v4 slugs may be 36 characters.");\n } else if (isReservedSlug(id)) errori.push("id: this slug is reserved.");\n }\n const name = stringaDefault(dati, "name", "", errori);\n if (dati.name === void 0) errori.push("name: is required.");\n else if (typeof dati.name === "string" && (name.trim() === "" || name.length > 60)) {\n errori.push("name: must contain 1-60 characters.");\n }\n const description = dati.description === "" ? "" : testoFacoltativo(dati, "description", 500, "", errori) ?? "";\n const immagini = { cover: "", card: "", icon: "" };\n const usati = /* @__PURE__ */ new Set();\n for (const campo of ["cover", "card", "icon"]) {\n const path = dati[campo];\n if (path === void 0 || path === null) errori.push(`${campo}: is required.`);\n else if (typeof path !== "string" || !percorsoRelativo(path)) errori.push(`${campo}: must be a relative file path inside client/ without query, fragment, or parent segments.`);\n else {\n if (!/\\.(png|jpe?g|webp)$/i.test(path)) errori.push(`${campo}: must be a PNG, JPEG or WebP file.`);\n const canonical = decodeURIComponent(path);\n if (usati.has(canonical)) errori.push(`${campo}: each image must use a different file; cover, card and icon cannot share a path.`);\n usati.add(canonical);\n immagini[campo] = path;\n }\n }\n const { cover, card, icon } = immagini;\n const screenshots = [];\n if (dati.screenshots !== void 0) {\n if (!Array.isArray(dati.screenshots)) errori.push("screenshots: must be an array of relative file paths.");\n else {\n if (dati.screenshots.length > 8) errori.push("screenshots: must contain at most 8 paths.");\n for (const [indice, value] of dati.screenshots.entries()) {\n if (typeof value !== "string" || !percorsoRelativo(value)) {\n errori.push(`screenshots[${indice}]: must be a relative file path without query, fragment, or parent segments.`);\n } else screenshots.push(value);\n }\n }\n }\n const tags = [];\n if (dati.tags !== void 0) {\n if (!Array.isArray(dati.tags)) errori.push("tags: must be an array.");\n else {\n if (dati.tags.length > 10) errori.push("tags: must contain at most 10 tags.");\n for (const [indice, value] of dati.tags.entries()) {\n if (typeof value !== "string" || value.length > 24 || !TAG.test(value)) {\n errori.push(`tags[${indice}]: must be 1-24 lowercase letters, digits, or hyphens.`);\n } else tags.push(value);\n }\n }\n }\n const legacyLanguage = stringaDefault(dati, "language", "en", errori);\n if (!/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(legacyLanguage)) {\n errori.push("language: must be a BCP 47 language tag such as en, it, or pt-BR.");\n }\n const languages2 = [];\n if (!Array.isArray(dati.languages) || dati.languages.length === 0) {\n errori.push("languages: must be a non-empty array of BCP 47 language tags.");\n } else for (const [index, raw] of dati.languages.entries()) {\n const tag = normalizeLanguage(raw);\n if (!tag) errori.push(`languages[${index}]: must be a BCP 47 language tag.`);\n else if (languages2.includes(tag)) errori.push(`languages[${index}]: duplicate language ${tag}.`);\n else languages2.push(tag);\n }\n if (!languages2.includes("en")) errori.push("languages: English is always required alongside the game\'s own languages.");\n const language = languages2[0] ?? legacyLanguage;\n if (typeof description === "object") {\n for (const tag of Object.keys(description)) {\n if (!languages2.includes(tag)) errori.push(`description.${tag}: language must be declared in languages.`);\n }\n }\n if (dati.language !== void 0 && dati.languages !== void 0 && legacyLanguage.toLowerCase() !== language.toLowerCase()) {\n errori.push("language: must match the first entry in languages when both are present.");\n }\n let platform = "both";\n if (dati.platform === void 0) errori.push("platform: is required.");\n else if (typeof dati.platform !== "string" || !PLATFORM.has(dati.platform)) {\n errori.push("platform: must be desktop, mobile, or both.");\n } else platform = dati.platform;\n let orientation = "landscape";\n if (dati.orientation !== void 0) {\n if (typeof dati.orientation !== "string" || !ORIENTATION.has(dati.orientation)) {\n errori.push("orientation: must be landscape or portrait.");\n } else orientation = dati.orientation;\n }\n const input = [];\n if (dati.input !== void 0) {\n if (!Array.isArray(dati.input)) errori.push("input: must be an array.");\n else for (const [indice, value] of dati.input.entries()) {\n if (typeof value !== "string" || !INPUT.has(value)) {\n errori.push(`input[${indice}]: must be keyboard, mouse, touch, or gamepad.`);\n } else if (input.includes(value)) errori.push(`input[${indice}]: duplicate value ${value}.`);\n else input.push(value);\n }\n }\n let visibility = "public";\n if (dati.visibility !== void 0) {\n if (typeof dati.visibility !== "string" || !VISIBILITY.has(dati.visibility)) {\n errori.push("visibility: must be public or unlisted.");\n } else visibility = dati.visibility;\n }\n const network = [];\n if (dati.network !== void 0) {\n if (!Array.isArray(dati.network)) errori.push("network: must be an array of host names.");\n else for (const [indice, value] of dati.network.entries()) {\n if (typeof value !== "string" || !hostValido(value)) {\n errori.push(`network[${indice}]: must be a host name without scheme, port, path, query, or fragment.`);\n } else if (network.includes(value)) errori.push(`network[${indice}]: duplicate host ${value}.`);\n else network.push(value);\n }\n }\n let isolated = false;\n if (dati.isolated !== void 0) {\n if (typeof dati.isolated !== "boolean") errori.push("isolated: must be a boolean.");\n else isolated = dati.isolated;\n }\n const requires = {\n webgl2: false,\n webgpu: false,\n wasm: false,\n threads: false,\n memoryMb: null,\n performance: "light"\n };\n if (dati.requires !== void 0) {\n const value = oggetto(dati.requires);\n if (value === null) errori.push("requires: must be an object.");\n else {\n for (const campo of Object.keys(value)) {\n if (!["webgl2", "webgpu", "wasm", "threads", "memoryMb", "performance"].includes(campo)) {\n errori.push(`requires.${campo}: unknown field.`);\n }\n }\n for (const campo of ["webgl2", "webgpu", "wasm", "threads"]) {\n if (value[campo] === void 0) continue;\n if (typeof value[campo] !== "boolean") errori.push(`requires.${campo}: must be a boolean.`);\n else requires[campo] = value[campo];\n }\n if (value.memoryMb !== void 0) {\n if (value.memoryMb !== null && (!interoTra(value.memoryMb, 512, 32768) || value.memoryMb % 256 !== 0)) {\n errori.push("requires.memoryMb: must be an integer from 512 to 32768 in steps of 256, or null.");\n } else requires.memoryMb = value.memoryMb;\n }\n if (value.performance !== void 0) {\n if (typeof value.performance !== "string" || !PERFORMANCE.has(value.performance)) {\n errori.push("requires.performance: must be light, medium, or heavy.");\n } else requires.performance = value.performance;\n }\n if (requires.threads && !isolated) errori.push("requires.threads: needs isolated: true.");\n }\n }\n let players = { min: 1, max: 1 };\n if (dati.players !== void 0) {\n const value = oggetto(dati.players);\n if (value === null) errori.push("players: must be an object with min and max.");\n else {\n for (const campo of Object.keys(value)) {\n if (campo !== "min" && campo !== "max") errori.push(`players.${campo}: unknown field.`);\n }\n if (!interoTra(value.min, 1, TETTO_GIOCATORI)) errori.push(`players.min: must be an integer from 1 to ${TETTO_GIOCATORI}.`);\n if (!interoTra(value.max, 1, TETTO_GIOCATORI)) errori.push(`players.max: must be an integer from 1 to ${TETTO_GIOCATORI} in manifest version 1.`);\n if (interoTra(value.min, 1, TETTO_GIOCATORI) && interoTra(value.max, 1, TETTO_GIOCATORI)) {\n if (value.min > value.max) errori.push("players.max: must be greater than or equal to players.min.");\n else players = { min: value.min, max: value.max };\n }\n }\n }\n let lobby = false;\n if (dati.lobby !== void 0) {\n if (typeof dati.lobby !== "boolean") errori.push("lobby: must be a boolean.");\n else lobby = dati.lobby;\n }\n let persistent = false;\n if (dati.persistent !== void 0) {\n if (typeof dati.persistent !== "boolean") errori.push("persistent: must be a boolean.");\n else persistent = dati.persistent;\n }\n let spectators = { delayMs: RITARDO_SPETTATORI_MS };\n if (dati.spectators === false || dati.spectators === null) spectators = null;\n else if (dati.spectators !== void 0 && dati.spectators !== true) {\n const value = oggetto(dati.spectators);\n if (value === null) {\n errori.push("spectators: must be a boolean or an object with delayMs.");\n } else {\n for (const campo of Object.keys(value)) {\n if (campo !== "delayMs") errori.push(`spectators.${campo}: unknown field.`);\n }\n if (!interoTra(value.delayMs, 0, 3e4)) {\n errori.push("spectators.delayMs: must be an integer from 0 to 30000.");\n } else spectators = { delayMs: value.delayMs };\n }\n }\n let overlay = null;\n if (dati.overlay !== void 0 && dati.overlay !== null) {\n const value = oggetto(dati.overlay);\n if (value === null) errori.push("overlay: must be an object or null.");\n else {\n for (const key of Object.keys(value)) if (!["version", "accent"].includes(key)) errori.push(`overlay.${key}: unknown field.`);\n if (value.version !== 1) errori.push("overlay.version: must be exactly 1.");\n if (value.accent !== void 0 && (typeof value.accent !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value.accent))) {\n errori.push("overlay.accent: must be a six-digit hexadecimal color, such as #336699.");\n }\n overlay = { version: 1, ...typeof value.accent === "string" ? { accent: value.accent } : {} };\n }\n }\n const boards = {};\n if (dati.boards !== void 0) {\n const value = oggetto(dati.boards);\n if (value === null) errori.push("boards: must be an object of board ids.");\n else {\n if (Object.keys(value).length > MASSIMO_CLASSIFICHE) {\n errori.push(`boards: at most ${MASSIMO_CLASSIFICHE} boards.`);\n }\n for (const [id2, raw] of Object.entries(value)) {\n let valido = true;\n if (!ID_CLASSIFICA.test(id2)) {\n errori.push(`boards.${id2}: invalid board id.`);\n valido = false;\n }\n const board = oggetto(raw);\n if (board === null) {\n errori.push(`boards.${id2}.source: must be "client" or "server".`);\n continue;\n }\n for (const campo of Object.keys(board)) {\n if (!["source", "label", "periods", "day"].includes(campo)) errori.push(`boards.${id2}.${campo}: unknown field.`);\n }\n if (board.source !== "client" && board.source !== "server") {\n errori.push(`boards.${id2}.source: must be "client" or "server".`);\n valido = false;\n }\n if (board.day !== void 0 && board.day !== "submit" && board.day !== "start") errori.push(`boards.${id2}.day: must be "submit" or "start".`);\n if (board.day === "start" && board.source !== "server") errori.push(`boards.${id2}.day: start requires source "server".`);\n const label = testoFacoltativo(board, "label", 48, `boards.${id2}`, errori);\n let periods = ["all-time"];\n if (board.periods !== void 0) {\n if (!Array.isArray(board.periods) || board.periods.length < 1 || board.periods.length > 2 || board.periods.some((period) => period !== "daily" && period !== "all-time") || new Set(board.periods).size !== board.periods.length) {\n errori.push(`boards.${id2}.periods: must contain daily, all-time, or both without duplicates.`);\n } else periods = [...board.periods];\n }\n if (valido) Object.defineProperty(boards, id2, { value: {\n source: board.source,\n periods,\n ...board.day === void 0 ? {} : { day: board.day },\n ...label === void 0 ? {} : { label }\n }, enumerable: true, configurable: true, writable: true });\n }\n }\n }\n const roles = [];\n if (dati.roles !== void 0) {\n if (!Array.isArray(dati.roles)) errori.push("roles: must be an array.");\n else {\n const ids = /* @__PURE__ */ new Set();\n for (const [indice, raw] of dati.roles.entries()) {\n const value = oggetto(raw);\n if (value === null) {\n errori.push(`roles[${indice}]: must be an object.`);\n continue;\n }\n for (const campo of Object.keys(value)) {\n if (!["id", "min", "max", "label"].includes(campo)) errori.push(`roles[${indice}].${campo}: unknown field.`);\n }\n const idRuolo = value.id;\n const min = value.min;\n const max = value.max;\n let valido = true;\n if (typeof idRuolo !== "string" || idRuolo.length > 32 || !ID_INTERNO.test(idRuolo)) {\n errori.push(`roles[${indice}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`);\n valido = false;\n } else if (ids.has(idRuolo)) {\n errori.push(`roles[${indice}].id: duplicate role ${idRuolo}.`);\n valido = false;\n } else ids.add(idRuolo);\n if (!interoTra(min, 0, TETTO_GIOCATORI)) {\n errori.push(`roles[${indice}].min: must be an integer from 0 to ${TETTO_GIOCATORI}.`);\n valido = false;\n }\n if (max !== void 0 && !interoTra(max, 0, TETTO_GIOCATORI)) {\n errori.push(`roles[${indice}].max: must be an integer from 0 to ${TETTO_GIOCATORI} when present.`);\n valido = false;\n }\n if (typeof min === "number" && typeof max === "number" && min > max) {\n errori.push(`roles[${indice}].max: must be greater than or equal to min.`);\n valido = false;\n }\n const label = testoFacoltativo(value, "label", 32, `roles[${indice}]`, errori);\n if (valido) roles.push({\n id: idRuolo,\n min,\n ...max === void 0 ? {} : { max },\n ...label === void 0 ? {} : { label }\n });\n }\n }\n }\n let teams = null;\n if (dati.teams !== void 0 && dati.teams !== null) {\n const value = oggetto(dati.teams);\n if (value === null) errori.push("teams: must be null or an object with min and max.");\n else {\n for (const campo of Object.keys(value)) {\n if (campo !== "min" && campo !== "max") errori.push(`teams.${campo}: unknown field.`);\n }\n if (!interoTra(value.min, 2, TETTO_GIOCATORI)) errori.push(`teams.min: must be an integer from 2 to ${TETTO_GIOCATORI}.`);\n if (!interoTra(value.max, 2, TETTO_GIOCATORI)) errori.push(`teams.max: must be an integer from 2 to ${TETTO_GIOCATORI}.`);\n if (interoTra(value.min, 2, TETTO_GIOCATORI) && interoTra(value.max, 2, TETTO_GIOCATORI)) {\n if (value.min > value.max) errori.push("teams.max: must be greater than or equal to teams.min.");\n else teams = { min: value.min, max: value.max };\n }\n }\n }\n let voice = "none";\n if (dati.voice !== void 0) {\n if (typeof dati.voice !== "string" || !VOICE.has(dati.voice)) {\n errori.push("voice: must be none, room, team, or proximity.");\n } else voice = dati.voice;\n }\n const modes = [];\n if (dati.modes !== void 0) {\n if (!Array.isArray(dati.modes)) errori.push("modes: must be an array.");\n else {\n const ids = /* @__PURE__ */ new Set();\n for (const [indice, raw] of dati.modes.entries()) {\n const value = oggetto(raw);\n if (value === null) {\n errori.push(`modes[${indice}]: must be an object.`);\n continue;\n }\n for (const campo of Object.keys(value)) {\n if (!["id", "players", "lobby", "matchmaking", "execution", "label", "instructions"].includes(campo)) errori.push(`modes[${indice}].${campo}: unknown field.`);\n }\n if (typeof value.id !== "string" || value.id.length > 32 || !ID_INTERNO.test(value.id)) {\n errori.push(`modes[${indice}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`);\n continue;\n }\n if (ids.has(value.id)) {\n errori.push(`modes[${indice}].id: duplicate mode ${value.id}.`);\n continue;\n }\n ids.add(value.id);\n const modo = { id: value.id };\n for (const [key2, max] of [["label", 48], ["instructions", 160]]) {\n const text = testoFacoltativo(value, key2, max, `modes[${indice}]`, errori);\n if (text !== void 0) modo[key2] = text;\n }\n if (value.execution !== void 0) {\n if (value.execution !== "local" && value.execution !== "room") errori.push(`modes[${indice}].execution: must be local or room.`);\n else modo.execution = value.execution;\n }\n if (overlay !== null && modo.execution === void 0) errori.push(`modes[${indice}].execution: is required with the standard overlay.`);\n if (value.players !== void 0) {\n const campo = `modes[${indice}].players`;\n const range = oggetto(value.players);\n if (range === null) errori.push(`${campo}: must be an object with min and max.`);\n else {\n for (const key2 of Object.keys(range)) {\n if (key2 !== "min" && key2 !== "max") errori.push(`${campo}.${key2}: unknown field.`);\n }\n if (!interoTra(range.min, 1, TETTO_GIOCATORI)) errori.push(`${campo}.min: must be an integer from 1 to ${TETTO_GIOCATORI}.`);\n if (!interoTra(range.max, 1, TETTO_GIOCATORI)) errori.push(`${campo}.max: must be an integer from 1 to ${TETTO_GIOCATORI}.`);\n if (interoTra(range.min, 1, TETTO_GIOCATORI) && interoTra(range.max, 1, TETTO_GIOCATORI)) {\n if (range.min > range.max) errori.push(`${campo}.max: must be greater than or equal to min.`);\n else modo.players = { min: range.min, max: range.max };\n }\n }\n }\n if (value.lobby !== void 0) {\n if (typeof value.lobby !== "boolean") errori.push(`modes[${indice}].lobby: must be a boolean.`);\n else modo.lobby = value.lobby;\n }\n if (modo.execution === "local") {\n const range = modo.players ?? players;\n if (range.min !== 1 || range.max !== 1) errori.push(`modes[${indice}].players: local execution requires min and max to be 1.`);\n if (modo.lobby ?? lobby) errori.push(`modes[${indice}].lobby: local execution requires false.`);\n if (value.matchmaking !== void 0) errori.push(`modes[${indice}].matchmaking: local execution cannot use matchmaking.`);\n }\n if (value.matchmaking === void 0) {\n modes.push(modo);\n continue;\n }\n const matchmaking = oggetto(value.matchmaking);\n if (matchmaking === null) {\n errori.push(`modes[${indice}].matchmaking: must be an object.`);\n continue;\n }\n for (const campo of Object.keys(matchmaking)) {\n if (!["key", "timeoutMs", "defaults"].includes(campo)) {\n errori.push(`modes[${indice}].matchmaking.${campo}: unknown field.`);\n }\n }\n let valido = true;\n const key = [];\n if (!Array.isArray(matchmaking.key) || matchmaking.key.length < 1 || matchmaking.key.length > 8) {\n errori.push(`modes[${indice}].matchmaking.key: must contain from 1 to 8 fields.`);\n valido = false;\n } else for (const [keyIndice, item] of matchmaking.key.entries()) {\n if (typeof item !== "string" || !CAMPO_MATCHMAKING.test(item)) {\n errori.push(`modes[${indice}].matchmaking.key[${keyIndice}]: must be 1-32 lowercase letters, digits, or hyphens and start with a letter or digit.`);\n valido = false;\n } else if (key.includes(item)) {\n errori.push(`modes[${indice}].matchmaking.key[${keyIndice}]: duplicate field ${item}.`);\n valido = false;\n } else key.push(item);\n }\n if (!interoTra(matchmaking.timeoutMs, 1e3, 3e5)) {\n errori.push(`modes[${indice}].matchmaking.timeoutMs: must be an integer from 1000 to 300000.`);\n valido = false;\n }\n let defaults;\n if (matchmaking.defaults !== void 0) {\n const values = oggetto(matchmaking.defaults);\n if (values === null || Object.keys(values).length !== key.length || key.some((field) => !Object.hasOwn(values, field))) {\n errori.push(`modes[${indice}].matchmaking.defaults: must contain exactly the declared key fields.`);\n } else {\n defaults = {};\n for (const [field, value2] of Object.entries(values)) {\n if (!(typeof value2 === "string" && value2.length >= 1 && value2.length <= 64 && /^[A-Za-z0-9_.:-]+$/.test(value2)) && !Number.isSafeInteger(value2)) {\n errori.push(`modes[${indice}].matchmaking.defaults.${field}: must be a string of 1-64 characters or a safe integer.`);\n } else Object.defineProperty(defaults, field, { value: value2, enumerable: true });\n }\n }\n }\n if (valido) modes.push({ ...modo, matchmaking: {\n ...defaults === void 0 ? {} : { defaults },\n key,\n timeoutMs: matchmaking.timeoutMs\n } });\n }\n }\n }\n if (overlay !== null && modes.length === 0) errori.push("modes: at least one explicit mode is required with the standard overlay.");\n if (errori.length > 0) return { ok: false, errori };\n return { ok: true, manifest: {\n manifest: 1,\n overlay,\n id,\n name,\n description,\n cover,\n card,\n icon,\n screenshots,\n tags,\n languages: languages2,\n language,\n platform,\n orientation,\n input,\n visibility,\n network,\n isolated,\n requires,\n players,\n lobby,\n persistent,\n spectators,\n boards,\n roles,\n teams,\n voice,\n modes\n } };\n}\n\n// ../contracts/src/overlay.ts\nvar OVERLAY_PANELS = ["home", "room", "invite", "friends", "voice", "boards"];\nfunction overlayConfiguration(manifest, coverUrl = null, invite = null, iconUrl = null) {\n const validated = validaManifest(manifest);\n if (!validated.ok) throw new Error("The overlay manifest is invalid.");\n return { manifest: validated.manifest, coverUrl, iconUrl, invite };\n}\nfunction record(value) {\n return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;\n}\nfunction validSafeArea(value) {\n const area = record(value);\n return area !== null && Object.keys(area).length === 4 && ["top", "right", "bottom", "left"].every((key) => typeof area[key] === "number" && Number.isFinite(area[key]) && Number(area[key]) >= 0 && Number(area[key]) <= 1e5);\n}\nfunction validOverlayView(value) {\n const data = record(value);\n return data !== null && Object.keys(data).every((key) => ["inputBlocked", "reservedRects", "safeArea", "shortcutEnabled"].includes(key)) && (data.safeArea === void 0 || validSafeArea(data.safeArea)) && (data.shortcutEnabled === void 0 || typeof data.shortcutEnabled === "boolean") && typeof data.inputBlocked === "boolean" && Array.isArray(data.reservedRects) && data.reservedRects.length <= 8 && data.reservedRects.every((value2) => {\n const rect = record(value2);\n return rect !== null && Object.keys(rect).length === 4 && ["x", "y", "width", "height"].every((key) => typeof rect[key] === "number" && Number.isFinite(rect[key]) && rect[key] >= 0 && rect[key] <= 1e5);\n });\n}\nfunction validOverlayRequest(value) {\n const message = record(value), args = record(message?.args);\n if (message?.type !== "caisual:overlay" || message.v !== 1 || typeof message.epoch !== "string" || message.epoch.length < 1 || message.epoch.length > 128 || typeof message.requestId !== "string" || !(/^[1-9][0-9]{0,15}$/.test(message.requestId) && Number.isSafeInteger(Number(message.requestId))) || args === null) return false;\n if (Object.keys(message).some((key) => !["type", "v", "epoch", "requestId", "sessionId", "op", "args"].includes(key)) || !(message.sessionId === void 0 || message.sessionId === null || typeof message.sessionId === "string" && /^[1-9][0-9]{0,15}$/.test(message.sessionId))) return false;\n const keys = (...allowed) => Object.keys(args).every((key) => allowed.includes(key));\n const text = (key) => typeof args[key] === "string" && args[key].length >= 1 && args[key].length <= 64;\n switch (message.op) {\n case "local.start":\n return keys("mode") && text("mode");\n case "room.create":\n return keys("mode") && (args.mode === null || text("mode"));\n case "room.join":\n return keys("code") && (args.code === void 0 || text("code"));\n case "room.watch":\n return keys("code") && text("code");\n case "room.match": {\n const key = record(args.key);\n return keys("mode", "key") && text("mode") && (args.key === void 0 || key !== null && Object.keys(key).length <= 8 && Object.values(key).every((v) => typeof v === "string" && v.length >= 1 && v.length <= 64 || typeof v === "number" && Number.isSafeInteger(v)));\n }\n case "room.ready":\n return keys("ready") && typeof args.ready === "boolean";\n case "room.role":\n case "room.requestRole":\n return keys("role") && text("role");\n case "room.team":\n return keys("team") && Number.isInteger(args.team) && args.team >= 1 && args.team <= 24;\n case "room.restart":\n case "room.start":\n case "session.cancel":\n case "session.leave":\n case "session.disconnect":\n case "session.resume":\n return keys();\n case "voice.join":\n case "voice.leave":\n return keys() && typeof message.sessionId === "string";\n case "voice.mute":\n return keys("muted") && typeof args.muted === "boolean" && typeof message.sessionId === "string";\n case "voice.setVolume":\n return keys("playerId", "volume") && typeof message.sessionId === "string" && typeof args.playerId === "string" && args.playerId.length > 0 && args.playerId.length <= 128 && typeof args.volume === "number" && Number.isFinite(args.volume) && args.volume >= 0 && args.volume <= 1;\n case "overlay.view":\n return validOverlayView(args);\n default:\n return false;\n }\n}\nfunction validBoardDay(value) {\n if (typeof value !== "string" || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false;\n const at = Date.parse(`${value}T00:00:00Z`);\n return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;\n}\nfunction validOverlaySessionState(value) {\n const data = record(value);\n const exact = (v, keys) => v !== null && Object.keys(v).length === keys.length && Object.keys(v).every((key) => keys.includes(key));\n const text = (v) => typeof v === "string" && v.length <= 128;\n const nullable = (v) => v === null || text(v);\n const finite = (v) => typeof v === "number" && Number.isFinite(v);\n if (!data || !exact(data, ["kind", "id", "mode", "localStatus", "ready", "capabilities", "room", "waiting", "resume", "resumeError", ..."voice" in data ? ["voice"] : []])) return false;\n if (data.voice !== void 0 && data.voice !== null && (data.kind !== "room" || !record(data.room) || !validOverlayVoice(data.voice))) return false;\n const capabilities = record(data.capabilities), room = record(data.room), waiting = record(data.waiting), resume = record(data.resume);\n if (!["boot", "home", "attaching", "matching", "local", "room", "watch"].includes(String(data.kind)) || !nullable(data.id) || !nullable(data.mode) || ![null, "playing", "ended"].includes(data.localStatus) || typeof data.ready !== "boolean" || typeof data.resumeError !== "boolean" || !exact(capabilities, ["local", "rooms", "overlay", "requestRole"]) || !Object.values(capabilities).every((v) => typeof v === "boolean")) return false;\n if (data.waiting !== null && (!exact(waiting, ["players", "min", "max"]) || !Object.values(waiting).every((v) => Number.isInteger(v) && Number(v) >= 0 && Number(v) <= 24))) return false;\n if (data.resume !== null && (!exact(resume, ["version", "code", "mode", "updatedAt"]) || resume.version !== 1 || !text(resume.code) || !nullable(resume.mode) || !finite(resume.updatedAt))) return false;\n if (data.room === null) return true;\n if (!exact(room, ["code", "mode", "status", "host", "you", "players", "countdownAt", "connection", "closedCode", "limits", "lobby", "persistent", "delayMs", "requestRole", ..."result" in (room ?? {}) ? ["result"] : [], ..."rematch" in (room ?? {}) ? ["rematch"] : []]) || !room) return false;\n const limits = record(room.limits), rematch = record(room.rematch);\n if (room.rematch !== void 0 && room.rematch !== null && (!exact(rematch, ["keepSetup", "autoStart"]) || typeof rematch.keepSetup !== "boolean" || typeof rematch.autoStart !== "boolean")) return false;\n return text(room.code) && nullable(room.mode) && nullable(room.host) && nullable(room.you) && ["lobby", "countdown", "playing", "finished", "ended"].includes(String(room.status)) && ["connecting", "connected", "reconnecting", "disconnected", "ended", "closed", "replaced"].includes(String(room.connection)) && ["countdownAt", "closedCode", "delayMs"].every((key) => room[key] === null || finite(room[key])) && ["lobby", "persistent", "requestRole"].every((key) => typeof room[key] === "boolean") && exact(limits, ["min", "max"]) && Object.values(limits).every((v) => Number.isInteger(v) && Number(v) >= 1 && Number(v) <= 24) && Array.isArray(room.players) && room.players.length <= 24 && room.players.every((value2) => {\n const player = record(value2);\n return exact(player, ["id", "name", "guest", "role", "team", "ready", "connected"]) && player !== null && text(player.id) && text(player.name) && nullable(player.role) && (player.team === null || Number.isInteger(player.team) && Number(player.team) >= 1 && Number(player.team) <= 24) && ["guest", "ready", "connected"].every((key) => typeof player[key] === "boolean");\n });\n}\nfunction validOverlayVoice(value) {\n const voice = record(value);\n if (!voice || Object.keys(voice).length !== 6 || !["mode", "state", "mic", "muted", "speaking", "peers"].every((key) => key in voice) || !["room", "team", "proximity"].includes(String(voice.mode)) || !["off", "joining", "on", "reconnecting"].includes(String(voice.state)) || !["mic", "muted", "speaking"].every((key) => typeof voice[key] === "boolean") || !Array.isArray(voice.peers) || voice.peers.length > 24) return false;\n const ids = /* @__PURE__ */ new Set();\n return voice.peers.every((value2) => {\n const peer = record(value2);\n if (!peer || Object.keys(peer).length !== 5 || !["id", "mic", "muted", "speaking", "volume"].every((key) => key in peer) || typeof peer.id !== "string" || !peer.id.length || peer.id.length > 128 || ids.has(peer.id) || !["mic", "muted", "speaking"].every((key) => typeof peer[key] === "boolean") || typeof peer.volume !== "number" || !Number.isFinite(peer.volume) || peer.volume < 0 || peer.volume > 1) return false;\n ids.add(peer.id);\n return true;\n });\n}\n\n// ../contracts/src/match-result.ts\nfunction readMatchResult(value, playerIds) {\n const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;\n const result = object(value);\n if (!result || !Array.isArray(result.standings)) return null;\n const known = new Set(playerIds), seen = /* @__PURE__ */ new Set();\n const standings = [];\n for (const item of result.standings) {\n const row = object(item);\n if (!row || typeof row.playerId !== "string" || !known.has(row.playerId) || seen.has(row.playerId)) continue;\n seen.add(row.playerId);\n standings.push({\n playerId: row.playerId,\n ...typeof row.score === "number" && Number.isFinite(row.score) ? { score: row.score } : {},\n ...typeof row.rank === "number" && Number.isSafeInteger(row.rank) && row.rank > 0 ? { rank: row.rank } : {}\n });\n }\n if (!standings.length) return null;\n return {\n standings,\n ...Array.isArray(result.winners) ? { winners: [...new Set(result.winners.filter((id) => typeof id === "string" && seen.has(id)))] } : {},\n ...typeof result.draw === "boolean" ? { draw: result.draw } : {},\n ...typeof result.unit === "string" ? { unit: result.unit } : {}\n };\n}\n\n// src/errors.ts\nfunction creaErrore(code, message, version = {}) {\n return Object.assign(new Error(message), { name: "CaisualError", code, ...version });\n}\n\n// src/overlay/host-bridge.ts\nfunction eMessaggioReady(value) {\n return record(value)?.type === "caisual:ready";\n}\nfunction eRichiestaBiglietto(value) {\n const data = record(value);\n return data?.type === "caisual:ticket" && (data.aud === void 0 || data.aud === "portal" || data.aud === "live");\n}\nfunction stanzaDaMessaggio(value) {\n const data = record(value);\n if (data?.type !== "caisual:room") return void 0;\n if (data.room === null) return null;\n const room = record(data.room);\n return typeof room?.code === "string" && /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(room.code) ? { code: room.code } : void 0;\n}\nfunction creaPonteOspite(input) {\n let port = null, epoch = null, instance = null;\n let disposed = false, legacyReady = true, sequence = 0, requestId = 0;\n let state = null, clockOffset = null;\n let polling = null, pollingEnd = null;\n const pending = /* @__PURE__ */ new Map();\n const states = /* @__PURE__ */ new Set();\n const shortcuts = /* @__PURE__ */ new Set();\n const opens = /* @__PURE__ */ new Set();\n const errors = /* @__PURE__ */ new Set();\n const scores = /* @__PURE__ */ new Set();\n const notify = (listeners, value) => {\n for (const listener of listeners) try {\n listener(value);\n } catch {\n }\n };\n const rejectPending = () => {\n for (const value of pending.values()) {\n input.finestra.clearTimeout(value.timer);\n value.reject(creaErrore("session_replaced", "The game document changed."));\n }\n pending.clear();\n };\n const stopPolling = () => {\n if (polling !== null) input.finestra.clearInterval(polling);\n if (pollingEnd !== null) input.finestra.clearTimeout(pollingEnd);\n polling = pollingEnd = null;\n };\n const askReady = () => {\n if (!disposed && input.frame.src !== "") input.frame.contentWindow?.postMessage({ type: "caisual:ready?" }, input.origineGioco);\n };\n const poll = () => {\n stopPolling();\n polling = input.finestra.setInterval(askReady, 500);\n pollingEnd = input.finestra.setTimeout(stopPolling, 1e4);\n askReady();\n };\n const loaded = () => {\n legacyReady = true;\n poll();\n };\n const listen = (event) => {\n if (disposed || event.origin !== input.origineGioco || event.source !== input.frame.contentWindow || !eMessaggioReady(event.data)) return;\n const data = record(event.data);\n const nextInstance = typeof data.instance === "string" && data.instance.length <= 128 ? data.instance : null;\n if (port && (nextInstance !== null ? nextInstance === instance : !legacyReady)) return;\n stopPolling();\n legacyReady = false;\n instance = nextInstance;\n rejectPending();\n port?.close();\n input.onRoom(null);\n epoch = input.epoch?.() ?? crypto.randomUUID();\n sequence = requestId = 0;\n state = null;\n clockOffset = null;\n notify(states, null);\n const channel = input.creaCanale?.() ?? new MessageChannel();\n const currentPort = channel.port1, currentEpoch = epoch;\n port = currentPort;\n const current = () => !disposed && port === currentPort && epoch === currentEpoch;\n currentPort.onmessage = (event2) => {\n if (!current()) return;\n const data2 = record(event2.data);\n if (eRichiestaBiglietto(data2)) {\n const aud = data2?.aud === "live" ? "live" : "portal";\n void input.rinnova(aud).then((ticket) => {\n if (current()) currentPort.postMessage({ type: "caisual:ticket", aud, ticket });\n }).catch(() => {\n if (current()) currentPort.postMessage({ type: "caisual:ticket", aud, error: "offline" });\n });\n return;\n }\n if (data2?.type === "caisual:reload") {\n const target = record(data2.target);\n const code = typeof target?.code === "string" && /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(target.code) ? target.code : void 0;\n const roomId = typeof target?.roomId === "string" && /^g[1-9][0-9]*-[1-9][0-9]*\\.[a-z0-9]{16}$/.test(target.roomId) ? target.roomId : void 0;\n input.reload?.(code || roomId ? { code, roomId, watch: target?.watch === true } : void 0);\n return;\n }\n const room = stanzaDaMessaggio(data2);\n if (room !== void 0) {\n input.onRoom(room);\n return;\n }\n if (data2?.v !== 1 || data2.epoch !== currentEpoch) return;\n if (data2.type === "caisual:overlay-response" && typeof data2.requestId === "string") {\n const item = pending.get(data2.requestId);\n if (!item) return;\n if (data2.ok !== true && (data2.ok !== false || typeof record(data2.error)?.code !== "string" || typeof record(data2.error)?.message !== "string")) return;\n pending.delete(data2.requestId);\n input.finestra.clearTimeout(item.timer);\n const response = data2;\n if (response.ok) item.resolve();\n else item.reject(creaErrore(response.error.code, response.error.message));\n } else if (data2.type === "caisual:overlay-state" && Number.isSafeInteger(data2.seq) && data2.seq > sequence) {\n if (!validOverlaySessionState(data2.state) || typeof data2.serverTime !== "number" || !Number.isFinite(data2.serverTime)) return;\n clockOffset = data2.serverTime - Date.now();\n sequence = data2.seq;\n state = structuredClone(data2.state);\n notify(states, state);\n } else if (data2.type === "caisual:overlay-error" && data2.sessionId === state?.id) {\n const error = record(data2.error);\n if (typeof error?.code === "string" && typeof error.message === "string") notify(errors, { sessionId: data2.sessionId, error: { code: error.code, message: error.message } });\n } else if (data2.type === "caisual:overlay-shortcut") {\n notify(shortcuts, void 0);\n } else if (data2.type === "caisual:overlay-open" && OVERLAY_PANELS.includes(data2.panel)) {\n notify(opens, data2.panel);\n } else if (data2.type === "caisual:overlay-score") {\n const score = record(data2.score);\n if (score && typeof score.board === "string" && typeof score.player === "string" && Number.isSafeInteger(score.score) && Number.isFinite(score.submittedAt) && (score.day === null || typeof score.day === "string")) {\n notify(scores, { board: score.board, player: score.player, score: score.score, day: score.day, submittedAt: score.submittedAt });\n }\n }\n };\n currentPort.start();\n input.frame.contentWindow?.postMessage({\n type: "caisual:hello",\n n: input.n,\n ticket: input.ticket,\n live: input.origineLive,\n invite: input.invite,\n // language resta disponibile ai kit pubblicati prima della separazione delle lingue.\n ...input.language ? { language: input.language, uiLanguage: input.language } : {},\n ...input.languagePreferences ? { languagePreferences: input.languagePreferences } : {},\n ...input.configuration ? { gameLanguages: manifestLanguages(input.configuration.manifest) } : {},\n ...input.configuration && data.overlayVersion === 1 ? { overlay: { v: 1, epoch, configuration: input.configuration } } : {}\n }, input.origineGioco, [channel.port2]);\n };\n input.finestra.addEventListener("message", listen);\n input.frame.addEventListener?.("load", loaded);\n poll();\n return {\n get watch() {\n return input.watch === true;\n },\n reload() {\n input.reload?.();\n },\n get epoch() {\n return epoch;\n },\n serverTime() {\n return clockOffset === null ? null : Date.now() + clockOffset;\n },\n get state() {\n return state === null ? null : structuredClone(state);\n },\n subscribe(listener) {\n states.add(listener);\n listener(state);\n return () => {\n states.delete(listener);\n };\n },\n onShortcut(listener) {\n shortcuts.add(listener);\n return () => {\n shortcuts.delete(listener);\n };\n },\n onOpen(listener) {\n opens.add(listener);\n return () => {\n opens.delete(listener);\n };\n },\n onError(listener) {\n errors.add(listener);\n return () => {\n errors.delete(listener);\n };\n },\n onScore(listener) {\n scores.add(listener);\n return () => {\n scores.delete(listener);\n };\n },\n request(op, args) {\n if (!port || !epoch || disposed) return Promise.reject(creaErrore("offline", "The game bridge is not connected."));\n if (pending.size >= 32) return Promise.reject(creaErrore("rate_limited", "Too many overlay requests."));\n const id = String(++requestId), request = {\n type: "caisual:overlay",\n v: 1,\n epoch,\n requestId: id,\n op,\n args,\n ...["room.ready", "room.role", "room.requestRole", "room.team", "room.start", "room.restart", "session.leave", "session.disconnect", "voice.join", "voice.mute", "voice.leave", "voice.setVolume"].includes(op) ? { sessionId: state?.id ?? null } : {}\n };\n if (!validOverlayRequest(request)) return Promise.reject(creaErrore("invalid_request", "The overlay request is invalid."));\n return new Promise((resolve, reject) => {\n const timeout = op === "room.match" ? 31e4 : input.requestTimeoutMs ?? 15e3;\n const timer = input.finestra.setTimeout(() => {\n pending.delete(id);\n reject(creaErrore("timeout", "The overlay request timed out."));\n }, timeout);\n pending.set(id, { resolve, reject, timer });\n try {\n port.postMessage(request);\n } catch (error) {\n input.finestra.clearTimeout(timer);\n pending.delete(id);\n reject(error);\n }\n });\n },\n dispose() {\n disposed = true;\n stopPolling();\n rejectPending();\n port?.close();\n port = null;\n input.finestra.removeEventListener("message", listen);\n input.frame.removeEventListener?.("load", loaded);\n states.clear();\n opens.clear();\n shortcuts.clear();\n scores.clear();\n errors.clear();\n }\n };\n}\nfunction avviaHandshake(input) {\n const bridge = creaPonteOspite(input);\n return () => bridge.dispose();\n}\nfunction gameViewport(frame) {\n const rect = frame.getBoundingClientRect();\n const zoomX = frame.offsetWidth ? rect.width / frame.offsetWidth : 1;\n const zoomY = frame.offsetHeight ? rect.height / frame.offsetHeight : 1;\n const left = rect.left + frame.clientLeft * zoomX, top = rect.top + frame.clientTop * zoomY;\n return {\n left,\n top,\n right: left + frame.clientWidth * zoomX,\n bottom: top + frame.clientHeight * zoomY,\n scaleX: zoomX ? 1 / zoomX : 1,\n scaleY: zoomY ? 1 / zoomY : 1\n };\n}\nfunction measureSafeArea(frame, probe) {\n const win = frame.ownerDocument.defaultView, css = win.getComputedStyle(probe), viewport = gameViewport(frame);\n const clamp = (value, max) => Math.max(0, Math.min(max, value));\n return {\n top: clamp(((parseFloat(css.paddingTop) || 0) - viewport.top) * viewport.scaleY, frame.clientHeight),\n right: clamp((viewport.right - (win.innerWidth - (parseFloat(css.paddingRight) || 0))) * viewport.scaleX, frame.clientWidth),\n bottom: clamp((viewport.bottom - (win.innerHeight - (parseFloat(css.paddingBottom) || 0))) * viewport.scaleY, frame.clientHeight),\n left: clamp(((parseFloat(css.paddingLeft) || 0) - viewport.left) * viewport.scaleX, frame.clientWidth)\n };\n}\n\n// src/overlay/boards.ts\nfunction createBoardController(input) {\n let disposed = false, generation = 0, timer;\n const seen = /* @__PURE__ */ new Set();\n let query = null, data = null, error = false, loading = false;\n let queued = null, saving = null, reads = 0;\n const later = input.later ?? setTimeout, clear = input.clear ?? clearTimeout;\n const cancel = () => {\n if (timer !== void 0) clear(timer);\n timer = void 0;\n };\n const notify = () => {\n if (!disposed) input.changed();\n };\n const matches = () => queued && query?.board === queued.board && query.period === (queued.day ? "daily" : "all-time") && (query.day ?? queued.day) === queued.day;\n const refresh = async () => {\n if (!query || disposed) return;\n cancel();\n const current = ++generation, selected = { ...query };\n loading = true;\n error = false;\n notify();\n try {\n const result = await input.read(selected);\n if (disposed || current !== generation) return;\n data = result;\n if (matches()) {\n const own = result.me;\n if (own?.verified && own.score >= queued.score) saving = own.score === queued.score ? "saved" : "bestAlready";\n }\n } catch {\n if (!disposed && current === generation) error = true;\n }\n if (disposed || current !== generation) return;\n loading = false;\n if (matches() && saving !== "saved" && saving !== "bestAlready") {\n reads++;\n if (reads < 2) {\n saving = "saving";\n timer = later(() => {\n void refresh();\n }, 1600);\n } else saving = "refreshHint";\n }\n notify();\n };\n return {\n get state() {\n return { query, data, loading, error, saving: matches() ? saving : null };\n },\n select(next) {\n if (JSON.stringify(next) === JSON.stringify(query)) return;\n cancel();\n generation++;\n query = { ...next };\n data = null;\n reads = 0;\n if (matches()) saving = "saving";\n void refresh();\n },\n queued(score) {\n const board = input.manifest.boards[score.board];\n if (score.player !== input.player || !board || !Number.isSafeInteger(score.score) || score.score < 0 || score.day !== null && !validBoardDay(score.day) || !(board.periods ?? ["all-time"]).includes(score.day ? "daily" : "all-time")) return;\n const signature = JSON.stringify(score);\n if (seen.has(signature)) return;\n seen.add(signature);\n if (seen.size > 64) seen.delete(seen.values().next().value);\n queued = score;\n saving = "saving";\n reads = 0;\n this.select({ board: score.board, period: score.day ? "daily" : "all-time", guests: query?.guests ?? input.guests ?? false, ...score.day ? { day: score.day } : {} });\n if (!loading) void refresh();\n notify();\n },\n refresh,\n reset() {\n seen.clear();\n cancel();\n generation++;\n query = null;\n data = null;\n queued = null;\n saving = null;\n loading = false;\n error = false;\n },\n dispose() {\n disposed = true;\n cancel();\n generation++;\n }\n };\n}\n\n// src/overlay/i18n.ts\nvar languages = ["en", "it", "es", "fr", "de", "pt"];\nvar words = {\n gameUpdated: ["This game was updated", "Questo gioco \\xE8 stato aggiornato", "Este juego se ha actualizado", "Ce jeu a \\xE9t\\xE9 mis \\xE0 jour", "Dieses Spiel wurde aktualisiert", "Este jogo foi atualizado"],\n reloadGame: ["Reload game", "Ricarica il gioco", "Recargar el juego", "Recharger le jeu", "Spiel neu laden", "Recarregar o jogo"],\n gameLanguages: ["Game languages", "Lingue del gioco", "Idiomas del juego", "Langues du jeu", "Spielsprachen", "Idiomas do jogo"],\n loading: ["Loading game...", "Caricamento...", "Cargando...", "Chargement...", "Spiel wird geladen...", "Carregando..."],\n loadingSlow: ["This game is taking longer than expected. You can wait a little longer or try again.", "Il gioco ci sta mettendo pi\\xF9 del previsto. Puoi aspettare ancora un po\\u2019 o riprovare.", "El juego est\\xE1 tardando m\\xE1s de lo esperado. Puedes esperar un poco m\\xE1s o volver a intentarlo.", "Le jeu met plus de temps que pr\\xE9vu. Vous pouvez patienter encore un peu ou r\\xE9essayer.", "Das Spiel braucht l\\xE4nger als erwartet. Du kannst noch etwas warten oder es erneut versuchen.", "O jogo est\\xE1 demorando mais do que o esperado. Voc\\xEA pode esperar mais um pouco ou tentar novamente."],\n home: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar"],\n homeMenu: ["Menu", "Menu", "Men\\xFA", "Menu", "Men\\xFC", "Menu"],\n mode: ["Mode", "Modalit\\xE0", "Modo", "Mode", "Modus", "Modo"],\n play: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar"],\n friendsPlay: ["Play with friends", "Gioca con amici", "Jugar con amigos", "Jouer entre amis", "Mit Freunden spielen", "Jogar com amigos"],\n find: ["Find players", "Trova giocatori", "Buscar jugadores", "Trouver des joueurs", "Spieler finden", "Buscar jogadores"],\n join: ["Join with code", "Entra con codice", "Entrar con c\\xF3digo", "Rejoindre avec un code", "Mit Code beitreten", "Entrar com c\\xF3digo"],\n joinInvite: ["Join this room", "Entra in questa stanza", "Entrar en est\\xE1 sala", "Rejoindre cette salle", "Diesem Raum beitreten", "Entrar nest\\xE1 sala"],\n watch: ["Watch a room", "Guarda una stanza", "Observar una sala", "Regarder une salle", "Raum ansehen", "Assistir a uma sala"],\n resume: ["Resume", "Riprendi", "Continuar", "Reprendre", "Fortsetzen", "Continuar"],\n room: ["Room", "Stanza", "Sala", "Salle", "Raum", "Sala"],\n code: ["Room code", "Codice stanza", "C\\xF3digo de sala", "Code de salle", "Raumcode", "C\\xF3digo da sala"],\n copy: ["Copy invite", "Copia invito", "Copiar invitaci\\xF3n", "Copier le lien", "Einladung kopieren", "Copiar convite"],\n copied: ["Invite copied", "Invito copiato", "Invitacion copiada", "Lien copi\\xE9", "Einladung kopiert", "Convite copiado"],\n copyFailed: ["Copy this link:", "Copia questo link:", "Copia este enlace:", "Copiez ce lien :", "Diesen Link kopieren:", "Copie este link:"],\n joining: ["Joining room...", "Ingresso nella stanza...", "Entrando en la sala...", "Connexion \\xE0 la salle...", "Raum wird betreten...", "Entrando na sala..."],\n matching: ["Finding your people...", "Ricerca giocatori...", "Buscando jugadores...", "Recherche de joueurs...", "Spieler werden gesucht...", "Buscando jogadores..."],\n queue: ["{n} / {max} players", "{n} / {max} giocatori", "{n} / {max} jugadores", "{n} / {max} joueurs", "{n} / {max} Spieler", "{n} / {max} jogadores"],\n cancel: ["Cancel", "Annulla", "Cancelar", "Annuler", "Abbrechen", "Cancelar"],\n close: ["Close", "Chiudi", "Cerrar", "Fermer", "Schlie\\xDFen", "Fechar"],\n back: ["Back", "Indietro", "Volver", "Retour", "Zur\\xFCck", "Voltar"],\n ready: ["Ready", "Pronto", "Listo", "Pr\\xEAt", "Bereit", "Pronto"],\n unready: ["Not ready", "Non pronto", "No listo", "Pas pr\\xEAt", "Nicht bereit", "N\\xE3o pronto"],\n start: ["Start", "Inizia", "Empezar", "Commencer", "Starten", "Come\\xE7ar"],\n role: ["Role", "Ruolo", "Rol", "R\\xF4le", "Rolle", "Fun\\xE7\\xE3o"],\n team: ["Team", "Squadra", "Equipo", "\\xC9quipe", "Team", "Equipe"],\n host: ["Host", "Host", "Anfitrion", "H\\xF4te", "Host", "Anfitri\\xE3o"],\n you: ["You", "Tu", "T\\xFA", "Vous", "Du", "Voc\\xEA"],\n away: ["Away", "Assente", "Ausente", "Absent", "Abwesend", "Ausente"],\n needPlayers: ["Waiting for more players", "In attesa di giocatori", "Esperando m\\xE1s jugadores", "En attente de joueurs", "Weitere Spieler fehlen", "Esperando mais jogadores"],\n needReady: ["Everyone needs to be ready", "Tutti devono essere pronti", "Todos deben estar listos", "Tout le monde doit \\xEAtre pr\\xEAt", "Alle m\\xFCssen bereit sein", "Todos precisam estar prontos"],\n needRoles: ["Fill the required roles", "Completa i ruoli richiesti", "Completa los roles", "Compl\\xE9tez les r\\xF4les", "Ben\\xF6tigte Rollen besetzen", "Complete as fun\\xE7\\xF5es"],\n needTeams: ["Choose the required teams", "Scegli le squadre richieste", "Elige los equipos", "Choisissez les \\xE9quipes", "Teams auswahlen", "Escolha as equipes"],\n waitHost: ["Waiting for the host", "In attesa dell\'host", "Esperando al anfitrion", "En attente de l\\u2019h\\xF4te", "Warten auf den Host", "Esperando o anfitri\\xE3o"],\n starting: ["Starting in", "Si inizia tra", "Empieza en", "D\\xE9but dans", "Start in", "Come\\xE7a em"],\n playing: ["Playing", "In partita", "Jugando", "En jeu", "Im Spiel", "Jogando"],\n ended: ["Game finished", "Partita conclusa", "Partida terminada", "Partie termin\\xE9e", "Spiel beendet", "Partida encerrada"],\n rematchReady: ["{n}/{max} ready", "{n}/{max} pronti", "{n}/{max} listos", "{n}/{max} pr\\xEAts", "{n}/{max} bereit", "{n}/{max} prontos"],\n rematchStart: ["Start rematch", "Avvia rivincita", "Iniciar revancha", "Lancer la revanche", "Revanche starten", "Iniciar revanche"],\n won: ["You won", "Hai vinto", "Has ganado", "Vous avez gagn\\xE9", "Du hast gewonnen", "Voc\\xEA venceu"],\n lost: ["You lost", "Hai perso", "Has perdido", "Vous avez perdu", "Du hast verloren", "Voc\\xEA perdeu"],\n draw: ["Draw", "Pareggio", "Empate", "\\xC9galit\\xE9", "Unentschieden", "Empate"],\n standings: ["Standings", "Piazzamenti", "Posiciones", "R\\xE9sultats", "Platzierungen", "Coloca\\xE7\\xF5es"],\n points: ["points", "punti", "puntos", "points", "Punkte", "pontos"],\n time: ["time", "tempo", "tiempo", "temps", "Zeit", "tempo"],\n distance: ["distance", "distanza", "distancia", "distance", "Distanz", "dist\\xE2ncia"],\n again: ["Play again", "Gioca ancora", "Jugar de nuevo", "Rejouer", "Erneut spielen", "Jogar novamente"],\n newRoom: ["New room. Share the new invite.", "Nuova stanza. Condividi il nuovo invito.", "Nueva sala. Comparte la invitaci\\xF3n.", "Nouvelle salle. Partagez le lien.", "Neuer Raum. Neue Einladung teilen.", "Nova sala. Compartilhe o convite."],\n watching: ["Watching", "In osservazione", "Observando", "Spectateur", "Zuschauen", "Assistindo"],\n delay: ["{n}s delay", "Ritardo {n}s", "Retraso de {n}s", "Retard de {n}s", "{n}s Verz\\xF6gerung", "Atraso de {n}s"],\n exit: ["Exit", "Esci", "Salir", "Quitter", "Verlassen", "Sair"],\n leaveNow: ["Leave for now", "Esci per ora", "Salir por ahora", "Quitter pour le moment", "Vor\\xFCbergehend verlassen", "Sair por enquanto"],\n leaveRoom: ["Leave room", "Lascia la stanza", "Abandonar sala", "Abandonner la salle", "Raum verlassen", "Deixar a sala"],\n leaveHint: ["Your room stays available for Resume.", "La stanza resta disponibile con Riprendi.", "Podr\\xE1s volver a est\\xE1 sala.", "Vous pourrez reprendre cette salle.", "Du kannst den Raum fortsetzen.", "Voc\\xEA pode voltar a est\\xE1 sala."],\n temporaryHint: ["The game continues. Rejoining may only be possible briefly.", "La partita continua. Il rientro pu\\xF2 essere disponibile solo per poco.", "La partida continua. Volver puede ser posible solo por poco tiempo.", "La partie continue. Le retour peut \\xEAtre limit\\xE9.", "Das Spiel l\\xE4uft weiter. R\\xFCckkehr nur kurz m\\xF6glich.", "A partida continua. O retorno pode ser limitado."],\n abandonHint: ["Leave room gives up your place.", "Lascia la stanza libera il tuo posto.", "Abandonar libera tu plaza.", "Abandonner lib\\xE8re votre place.", "Raum verlassen gibt deinen Platz frei.", "Deixar a sala libera sua vaga."],\n reconnecting: ["Reconnecting...", "Riconnessione...", "Reconectando...", "Reconnexion...", "Verbindung wird erneuert...", "Reconectando..."],\n replaced: ["Opened in another tab", "Aperta in un\\u2019altra scheda", "Abierta en otra pest\\xE1na", "Ouverte dans un autre onglet", "In anderem Tab ge\\xF6ffnet", "Aberta em outra aba"],\n error: ["Something went wrong. Try again.", "Qualcosa non va. Riprova.", "Algo sali\\xF3 mal. Reintenta.", "Une erreur est survenue. R\\xE9essayez.", "Etwas ist schiefgelaufen. Erneut versuchen.", "Algo deu errado. Tente novamente."],\n noRoom: ["This room is no longer available.", "Questa stanza non \\xE8 pi\\xF9 disponibile.", "Esta sala ya no est\\xE1 disponible.", "Cette salle n\'est plus disponible.", "Dieser Raum ist nicht mehr verf\\xFCgbar.", "Esta sala n\\xE3o est\\xE1 mais disponivel."],\n full: ["This room is full.", "La stanza \\xE8 piena.", "La sala est\\xE1 llena.", "Cette salle est pleine.", "Dieser Raum ist voll.", "Esta sala est\\xE1 cheia."],\n noMatch: ["No match this time. Try again.", "Nessun gruppo trovato. Riprova.", "No hay grupo. Reintenta.", "Aucun groupe trouv\\xE9. R\\xE9essayez.", "Keine Gruppe gefunden. Erneut versuchen.", "Nenhum grupo encontrado. Tente novamente."],\n invalidCode: ["Enter a six-character room code.", "Inserisci un codice di sei caratteri.", "Escribe un c\\xF3digo de seis caracteres.", "Entrez un code de six caracteres.", "Sechsstelligen Raumcode eingeben.", "Digite um c\\xF3digo de seis caracteres."],\n refused: ["The room did not accept that change.", "La stanza ha rifiutato la modifica.", "La sala rechaz\\xF3 el cambio.", "La salle a refus\\xE9 ce changement.", "Der Raum hat die \\xC4nderung abgelehnt.", "A sala recusou a altera\\xE7\\xE3o."],\n unavailable: ["Unavailable right now", "Non disponibile ora", "No disponible ahora", "Indisponible pour le moment", "Derzeit nicht verf\\xFCgbar", "Indisponivel agora"],\n offline: ["Connection unavailable. Try again.", "Connessione non disponibile. Riprova.", "Sin conexi\\xF3n. Reintenta.", "Connexion indisponible. R\\xE9essayez.", "Keine Verbindung. Erneut versuchen.", "Sem conex\\xE3o. Tente novamente."],\n saveFailed: ["Keep the room code. Resume could not be saved.", "Conserva il codice. Riprendi non \\xE8 stato salvato.", "Guarda el c\\xF3digo. No se pudo guardar el regreso.", "Gardez le code. La reprise ne peut pas \\xEAtre enregistr\\xE9e.", "Raumcode aufbewahren. Fortsetzen nicht gespeichert.", "Guarde o c\\xF3digo. O retorno n\\xE3o foi salvo."],\n boards: ["Leaderboard", "Classifica", "Clasificaci\\xF3n", "Classement", "Bestenliste", "Classifica\\xE7\\xE3o"],\n board: ["Board", "Classifica", "Tabla", "Classement", "Bestenliste", "Tabela"],\n daily: ["Daily", "Giornaliera", "Diaria", "Du jour", "T\\xE4glich", "Di\\xE1ria"],\n allTime: ["All time", "Di sempre", "Hist\\xF3rica", "Tous les temps", "Gesamt", "Geral"],\n accounts: ["Accounts", "Account", "Cuentas", "Comptes", "Konten", "Contas"],\n guests: ["Guests", "Ospiti", "Invitados", "Invit\\xE9s", "G\\xE4ste", "Visitantes"],\n category: ["Category", "Categoria", "Categoria", "Cat\\xE9gorie", "Kategorie", "Categoria"],\n period: ["Period", "Periodo", "Per\\xEDodo", "P\\xE9riode", "Zeitraum", "Per\\xEDodo"],\n rank: ["Rank", "Posizione", "Puesto", "Rang", "Platz", "Posicao"],\n score: ["Score", "Punteggio", "Puntos", "Score", "Punkte", "Pontos"],\n verified: ["Verified", "Verificato", "Verificado", "V\\xE9rifi\\xE9", "Verifiziert", "Verificado"],\n own: ["Your best", "Il tuo record", "Tu record", "Votre record", "Dein Rekord", "Seu recorde"],\n empty: ["No scores yet", "Nessun punteggio", "A\\xFAn no hay puntos", "Aucun score", "Noch keine Punkte", "Ainda sem pontos"],\n saving: ["Saving score...", "Salvataggio punteggio...", "Guardando puntos...", "Enregistrement du score...", "Punkte werden gespeichert...", "Salvando pontos..."],\n saved: ["Your best is on the board", "Il tuo record \\xE8 in classifica", "Tu record est\\xE1 en la tabla", "Votre record est au classement", "Dein Rekord ist eingetragen", "Seu recorde est\\xE1 na tabela"],\n bestAlready: ["Your best is already on the board", "Il tuo record era gi\\xE0 in classifica", "Tu record ya estaba en la tabla", "Votre record est d\\xE9j\\xE0 au classement", "Dein Rekord ist bereits eingetragen", "Seu recorde j\\xE1 est\\xE1 na tabela"],\n refresh: ["Refresh", "Aggiorna", "Actualizar", "Actualiser", "Aktualisieren", "Atualizar"],\n refreshHint: ["Score not visible yet. Refresh to check.", "Punteggio non ancora visibile. Aggiorna per controllare.", "Puntos a\\xFAn no visibles. Actualiza.", "Score pas encore visible. Actualisez.", "Punkte noch nicht sichtbar. Aktualisieren.", "Pontos ainda n\\xE3o visiveis. Atualize."],\n friends: ["Friends & party", "Amici e gruppo", "Amigos y grupo", "Amis et groupe", "Freunde & Gruppe", "Amigos e grupo"],\n localCrew: ["Friends and party are unavailable in local preview.", "Amici e gruppo non disponibili in anteprima locale.", "Amigos y grupo no disponibles en la vista local.", "Amis et groupe indisponibles en aper\\xE7u local.", "Freunde und Gruppe in lokaler Vorschau nicht verf\\xFCgbar.", "Amigos e grupo indispon\\xEDveis na pr\\xE9via local."],\n loginCrew: ["Sign in on Caisual to use friends and party.", "Accedi a Caisual per amici e gruppo.", "Inicia sesion para amigos y grupo.", "Connectez-vous pour utiliser amis et groupe.", "F\\xFCr Freunde und Gruppe bei Caisual anmelden.", "Entre no Caisual para amigos e grupo."],\n online: ["Online", "Online", "En linea", "En ligne", "Online", "Online"],\n noFriends: ["No friends online", "Nessun amico online", "Sin amigos en linea", "Aucun ami en ligne", "Keine Freunde online", "Nenhum amigo online"],\n createParty: ["Create party", "Crea gruppo", "Crear grupo", "Cr\\xE9er un groupe", "Gruppe erstellen", "Criar grupo"],\n inviteParty: ["Invite to party", "Invita nel gruppo", "Invitar al grupo", "Inviter au groupe", "In Gruppe einladen", "Convidar para o grupo"],\n leaveParty: ["Leave party", "Lascia gruppo", "Salir del grupo", "Quitter le groupe", "Gruppe verlassen", "Sair do grupo"],\n accept: ["Accept", "Accetta", "Aceptar", "Accepter", "Annehmen", "Aceitar"],\n decline: ["Decline", "Rifiuta", "Rechazar", "Refuser", "Ablehnen", "Recusar"],\n follow: ["Join them", "Raggiungi", "Unirse", "Rejoindre", "Beitreten", "Juntar-se"],\n voice: ["Voice", "Voce", "Voz", "Voix", "Sprache", "Voz"],\n voiceJoin: ["Join voice", "Entra in voce", "Unirse a voz", "Activer la voix", "Sprachchat beitreten", "Entrar na voz"],\n voiceLeave: ["Leave voice", "Esci dalla voce", "Salir de voz", "Quitter la voix", "Sprachchat verlassen", "Sair da voz"],\n voiceMute: ["Mute", "Disattiva microfono", "Silenciar", "Couper le micro", "Stummschalten", "Silenciar"],\n voiceUnmute: ["Unmute", "Attiva microfono", "Activar micr\\xF3fono", "Activer le micro", "Mikrofon aktivieren", "Ativar microfone"],\n voiceOff: ["Voice off", "Voce disattivata", "Voz desactivada", "Voix d\\xE9sactiv\\xE9e", "Sprachchat aus", "Voz desativada"],\n voiceJoining: ["Joining voice...", "Connessione voce...", "Conectando voz...", "Connexion vocale...", "Sprachchat verbindet...", "Conectando voz..."],\n voiceOn: ["Voice connected", "Voce connessa", "Voz conectada", "Voix connect\\xE9e", "Sprachchat verbunden", "Voz conectada"],\n voiceMuted: ["Muted", "Microfono disattivato", "Silenciado", "Micro coup\\xE9", "Stumm", "Silenciado"],\n voiceMic: ["Mic on", "Microfono attivo", "Micr\\xF3fono activo", "Micro actif", "Mikrofon an", "Microfone ativo"],\n voiceListening: ["Listening only", "Solo ascolto", "Solo escucha", "\\xC9coute seule", "Nur zuh\\xF6ren", "Somente ouvindo"],\n voiceSpeaking: ["Speaking", "Sta parlando", "Hablando", "Parle", "Spricht", "Falando"],\n voicePeers: ["Voice participants", "Partecipanti in voce", "Participantes de voz", "Participants vocaux", "Sprachteilnehmer", "Participantes de voz"],\n voiceEmpty: ["No one else in voice yet.", "Nessun altro in voce per ora.", "A\\xFAn no hay nadie m\\xE1s en voz.", "Personne d\\u2019autre en voix pour le moment.", "Noch niemand im Sprachchat.", "Ningu\\xE9m mais na voz ainda."],\n voiceVolume: ["Volume for {name}", "Volume di {name}", "Volumen de {name}", "Volume de {name}", "Lautst\\xE4rke f\\xFCr {name}", "Volume de {name}"],\n voiceUnavailable: ["Join a room with voice to use these controls.", "Entra in una stanza con voce per usare questi controlli.", "Entra en una sala con voz para usar estos controles.", "Rejoignez une salle vocale pour utiliser ces commandes.", "Diese Steuerung braucht einen Raum mit Sprachchat.", "Entre em uma sala com voz para usar estes controles."],\n voiceWatch: ["Voice is unavailable while watching.", "La voce non e\' disponibile in osservazione.", "La voz no est\\xE1 disponible al observar.", "La voix est indisponible en observation.", "Beim Zuschauen ist kein Sprachchat verf\\xFCgbar.", "A voz n\\xE3o est\\xE1 dispon\\xEDvel ao assistir."],\n voiceDenied: ["Microphone permission denied. Allow it in your browser, then try again.", "Permesso microfono negato. Consenti l\'accesso nel browser e riprova.", "Permiso de micr\\xF3fono denegado. Act\\xEDvalo en el navegador e int\\xE9ntalo de nuevo.", "Acc\\xE8s au micro refus\\xE9. Autorisez-le dans le navigateur, puis r\\xE9essayez.", "Mikrofonzugriff verweigert. Im Browser erlauben und erneut versuchen.", "Permiss\\xE3o do microfone negada. Permita no navegador e tente novamente."],\n voiceUnsupported: ["Voice is not supported in this browser.", "Questo browser non supporta la voce.", "Este navegador no admite voz.", "Ce navigateur ne prend pas en charge la voix.", "Dieser Browser unterst\\xFCtzt keinen Sprachchat.", "Este navegador n\\xE3o oferece suporte a voz."],\n voiceFailed: ["Voice could not connect. Try again.", "Connessione voce non riuscita. Riprova.", "No se pudo conectar la voz. Int\\xE9ntalo de nuevo.", "Connexion vocale impossible. R\\xE9essayez.", "Sprachverbindung fehlgeschlagen. Erneut versuchen.", "N\\xE3o foi poss\\xEDvel conectar a voz. Tente novamente."],\n voicePeerGone: ["This participant has left voice.", "Questo partecipante e\' uscito dalla voce.", "Este participante sali\\xF3 de voz.", "Ce participant a quitt\\xE9 la voix.", "Diese Person hat den Sprachchat verlassen.", "Este participante saiu da voz."],\n shortcut: ["Shift+Tab shortcut", "Scorciatoia Shift+Tab", "Atajo Shift+Tab", "Raccourci Maj+Tab", "Umschalt+Tab-Kurzbefehl", "Atalho Shift+Tab"],\n menu: ["Caisual menu", "Menu Caisual", "Menu Caisual", "Menu Caisual", "Caisual-Menu", "Menu Caisual"],\n retry: ["Retry", "Riprova", "Reintentar", "R\\xE9essayer", "Erneut versuchen", "Tentar novamente"]\n};\nvar column = (index) => Object.fromEntries(Object.entries(words).map(([key, row]) => [key, row[index]]));\nvar dictionaries = { en: column(0), it: column(1), es: column(2), fr: column(3), de: column(4), pt: column(5) };\nfunction overlayLanguage(raw) {\n const value = raw?.toLowerCase().split("-")[0];\n return languages.includes(value) ? value : "en";\n}\nfunction overlayLocale(raw) {\n const tag = normalizeLanguage(raw);\n return tag && languages.includes(tag.split("-")[0]) ? tag : "en";\n}\nfunction translator(language) {\n const dictionary = dictionaries[overlayLanguage(language)];\n return (key, values = {}) => dictionary[key].replace(/\\{(\\w+)\\}/g, (_all, name) => String(values[name] ?? ""));\n}\nfunction errorText(code) {\n if (code === "version_outdated") return "gameUpdated";\n if (code === "permission_denied") return "voiceDenied";\n if (code === "unsupported") return "voiceUnsupported";\n if (code === "voice_disabled") return "voiceUnavailable";\n if (code === "voice_error") return "voiceFailed";\n if (code === "voice_peer_missing") return "voicePeerGone";\n if (code === "not_publishing") return "voiceListening";\n if (["room_not_found", "room_ended", "version_closed", "no_resume"].includes(code)) return "noRoom";\n if (["room_full", "role_full"].includes(code)) return "full";\n if (code === "replaced") return "replaced";\n if (code === "no_match") return "noMatch";\n if (code === "invalid_code") return "invalidCode";\n if (["offline", "timeout"].includes(code)) return "offline";\n if (code.startsWith("role_") || ["not_in_lobby", "not_host", "session_replaced"].includes(code)) return "refused";\n if (code === "save_failed") return "saveFailed";\n return "error";\n}\n\n// src/overlay/ui-model.ts\nfunction phase(session) {\n if (!session || session.kind === "boot") return "boot";\n if (session.kind === "attaching" || session.kind === "matching") return session.kind;\n if (session.room && ["closed", "replaced"].includes(session.room.connection)) return "error";\n if (session.kind === "local") return session.localStatus === "ended" ? "ended" : "playing";\n if (session.room?.status === "ended" || session.room?.status === "finished") return "ended";\n if (session.kind === "watch") return "watching";\n if (session.kind === "room" && session.room) return session.room.status;\n return "home";\n}\nfunction initialUi(manifest) {\n return { session: null, panel: "auto", mode: manifest.modes[0]?.id ?? "", busy: false, error: null, notice: null, shortcutEnabled: true };\n}\nfunction reduceUi(model, action) {\n switch (action.type) {\n case "session": {\n const next = action.session, changed = next?.id !== model.session?.id || next === null;\n const pending = next?.kind === "attaching" || next?.kind === "matching";\n const nextPhase = phase(next), transition = phase(model.session) !== nextPhase;\n const automatic = transition && ["lobby", "countdown", "playing", "ended"].includes(nextPhase) && [null, "auto", "room", "invite", "home"].includes(model.panel);\n return {\n ...model,\n session: next,\n mode: next?.mode ?? model.mode,\n panel: changed || pending || automatic ? "auto" : model.panel,\n error: changed ? null : model.error,\n notice: changed ? null : model.notice\n };\n }\n case "panel":\n return { ...model, panel: action.panel, error: null, notice: null };\n case "mode":\n return { ...model, mode: action.mode, error: null };\n case "busy":\n return { ...model, busy: action.busy };\n case "error":\n return { ...model, error: action.code, busy: false };\n case "notice":\n return { ...model, notice: action.notice };\n case "shortcut":\n return { ...model, shortcutEnabled: action.enabled };\n }\n}\nfunction visiblePanel(model) {\n const current = phase(model.session);\n if (current === "boot" || current === "attaching" || current === "matching" || current === "error") return current;\n if (model.session?.room?.limits.max === 1 && ["room", "invite"].includes(model.panel ?? "")) return "home";\n if (model.panel !== "auto") return model.panel;\n if (model.session?.room?.limits.max === 1 && current === "lobby") return null;\n return current === "home" ? "home" : current === "lobby" ? "room" : current === "countdown" ? "countdown" : null;\n}\nfunction primaryAction(manifest, mode) {\n const selected = manifest.modes.find((item) => item.id === mode);\n if (!selected) return null;\n return {\n op: selected.execution === "local" ? "local.start" : "room.create",\n friends: selected.execution === "room" && risolviModalita(manifest, mode).players.max > 1\n };\n}\nfunction startReason(manifest, session) {\n const room = session?.room;\n if (!room || session.kind !== "room" || room.status !== "lobby" || room.connection !== "connected") return "unavailable";\n const connected = room.players.filter((p) => p.connected), active = connected.filter((p) => p.role !== "spectator");\n if (active.length < room.limits.min) return "needPlayers";\n if (connected.some((p) => !p.ready)) return "needReady";\n if (manifest.roles.some((role) => active.filter((p) => p.role === role.id).length < role.min)) return "needRoles";\n if (manifest.teams && (active.some((p) => p.team === null) || new Set(active.map((p) => p.team)).size < manifest.teams.min)) return "needTeams";\n return room.host !== room.you ? "waitHost" : null;\n}\nfunction canPlayAgain(session) {\n if (phase(session) !== "ended") return false;\n if (session?.kind === "local") return true;\n if (session?.room?.status === "finished") return session.kind === "room" && session.room.connection === "connected" && (session.room.limits.max === 1 || session.room.players.some((p) => p.id === session.room.you && p.connected && p.role !== "spectator" && !p.ready));\n return session?.kind === "room" && (session.room?.limits.max === 1 || !session.room?.lobby || session.room.host === session.room.you);\n}\nfunction normalizeInvite(code) {\n const value = code.toUpperCase().replace(/[\\s-]/g, "");\n return /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(value) ? value : null;\n}\nfunction matchPresentation(session) {\n const room = session?.room;\n if (!room || !["finished", "ended"].includes(room.status)) return null;\n const result = readMatchResult(room.result, room.players.map((p) => p.id));\n if (!result) return null;\n const own = session?.kind === "room" && room.players.some((p) => p.id === room.you && p.role !== "spectator") && result.standings.some((p) => p.playerId === room.you);\n const first = result.standings[0];\n const winners = result.winners ?? result.standings.filter((p, i) => i === 0 || first.rank !== void 0 && p.rank === first.rank).map((p) => p.playerId);\n return { result, outcome: !own ? "ended" : result.draw ? "draw" : winners.includes(room.you) ? "won" : "lost" };\n}\n\n// src/overlay/styles.ts\nvar styles = `\n.game-icon{width:28px;height:28px;aspect-ratio:1;object-fit:contain;border-radius:7px;flex:none;vertical-align:middle}.game-icon-title{width:56px;height:56px;margin-bottom:12px}\n.safe-area-probe{position:fixed;visibility:hidden;pointer-events:none;padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px)}\n:host{all:initial;position:fixed;inset:0;z-index:10000;pointer-events:none;font:15px/1.45 system-ui,sans-serif;color:#f4f4f1;color-scheme:dark;--accent:#a8efc5}\n[data-layout],[data-surface],.sr{pointer-events:none}*{box-sizing:border-box}button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}button,select,input{border:1px solid #ffffff30;background:#25292b;color:inherit;border-radius:12px;min-height:44px;padding:10px 14px}button{cursor:pointer}button:disabled{opacity:.45;cursor:default}button:hover:not(:disabled){background:#343b3a}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--accent);outline-offset:3px}a{color:var(--accent)}.primary{background:var(--accent);color:var(--accent-ink,#11221b);border-color:transparent;font-weight:700}.primary:hover:not(:disabled){filter:brightness(1.1);background:var(--accent)}.quiet{background:transparent}label{display:grid;gap:6px;text-align:left}select,input{width:100%;min-width:0}h1,h2,p{margin:0}h1{font-size:clamp(26px,5vw,42px);line-height:1.1;letter-spacing:-.035em}h2{font-size:20px}small,.muted{color:#bdc5c1}.stack{display:grid;gap:16px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row>*{flex:0 1 auto}.row .grow,.grow{flex:1}.split{display:grid;grid-template-columns:1fr 1fr;gap:10px}.pill{position:absolute;top:max(10px,env(safe-area-inset-top));right:max(10px,env(safe-area-inset-right));display:flex;height:44px;border:1px solid #ffffff35;border-radius:24px;background:#171e20eF;box-shadow:0 4px 20px #0004;pointer-events:auto;overflow:hidden}.pill button{border:0;border-radius:0;padding:8px 13px;background:transparent}.pill button:focus-visible{outline-offset:-4px}.pill small{margin-left:8px}.backdrop{position:absolute;inset:0;background:#0b151ce8;backdrop-filter:blur(10px);pointer-events:auto;display:grid;place-items:center;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));overflow:auto}.backdrop.home{background-color:#142127;background-size:contain;background-repeat:no-repeat;background-position:center}.dialog{position:relative;width:min(100%,540px);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#141b1df5;border:1px solid #ffffff25;border-radius:22px;padding:24px;box-shadow:0 20px 80px #0005}.dialog.wide{width:min(100%,700px)}.top{display:flex;align-items:center;gap:12px;margin-bottom:18px}.top h2{flex:1}.tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid #ffffff25;padding-bottom:12px}.tabs button{min-height:36px;padding:6px 10px}.tabs [aria-current=true]{border-color:var(--accent)}.roster{list-style:none;padding:0;margin:0;display:grid;gap:8px;max-height:32dvh;overflow:auto}.roster li{display:flex;align-items:center;gap:8px;padding:10px;background:#ffffff08;border-radius:10px}.roster .name{flex:1;overflow-wrap:anywhere}.badge{border:1px solid #ffffff30;border-radius:6px;padding:2px 6px;font-size:12px}.code{font-size:24px;letter-spacing:.13em;font-variant-numeric:tabular-nums}.notice,.error{border-radius:10px;padding:10px;background:#a8efc514;overflow-wrap:anywhere}.error{background:#ff8b7720;color:#ffd2c9}.countdown{font-size:88px;line-height:1;text-align:center;font-variant-numeric:tabular-nums}.ended{position:absolute;bottom:max(12px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);max-width:calc(100% - 24px);width:max-content;background:#171e20f5;pointer-events:auto;border:1px solid #ffffff30;border-radius:16px;padding:10px;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap}.ended [data-rematch-players]{max-width:100%;max-height:3.2em;overflow:auto;overflow-wrap:anywhere}.standings{list-style:none;margin:0;padding:0;flex-basis:100%;max-height:20dvh;overflow:auto;font-size:13px}.standings li{display:flex;justify-content:space-between;gap:16px;overflow-wrap:anywhere}.ended{max-height:calc(100dvh - 80px);overflow:auto}.ended strong{padding:0 8px}.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.table-wrap{overflow:auto;max-height:38dvh}table{width:100%;border-collapse:collapse;text-align:left}th,td{padding:9px 6px;border-bottom:1px solid #ffffff20}td:nth-child(3){text-align:right}td small{display:block}.self{background:#ffffff0a}.checkbox{display:flex;align-items:center;gap:8px;font-size:13px}.checkbox input{width:18px;min-height:18px}.full{width:100%}\n[hidden]{display:none!important}.voice-peers{list-style:none;margin:0;padding:0;display:grid;gap:10px}.voice-peers li{border:1px solid #ffffff25;border-radius:12px;padding:12px;display:grid;gap:8px}.voice-peers [data-speaking=true]{border-color:var(--accent)}.voice-peers input{width:100%;accent-color:var(--accent);padding:0}.voice-peers label{font-size:13px}.pill .voice-toggle{width:44px;padding:8px}.voice-toggle[data-voice-state=on][data-muted=false]{color:var(--accent)}\n.boot{position:absolute;inset:0;z-index:2;isolation:isolate;display:grid;place-items:center;overflow:auto;overscroll-behavior:contain;padding:max(100px,env(safe-area-inset-top)) max(24px,env(safe-area-inset-right)) max(48px,env(safe-area-inset-bottom)) max(24px,env(safe-area-inset-left));background:#0b151c;opacity:1;transition:opacity .4s ease;pointer-events:auto;outline:none}\n.boot::before,.boot::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:-1}.boot::before{background:radial-gradient(ellipse at 50% 38%,color-mix(in srgb,var(--accent),transparent 80%),transparent 65%)}.boot::after{background:radial-gradient(ellipse at 50% 38%,#0b151c20,#0b151cd9 85%),linear-gradient(#0b151c66,#0b151cbf)}\n.boot-cover{position:fixed;inset:0;z-index:-2;width:100%;height:100%;aspect-ratio:3 / 2;object-fit:contain;filter:blur(20px);opacity:.65;pointer-events:none}\n.boot-brand{position:absolute;top:max(28px,env(safe-area-inset-top));left:max(32px,env(safe-area-inset-left));display:flex;align-items:center;gap:10px;font-size:14px;font-weight:650;letter-spacing:.02em;color:#f4f4f1b3}.boot-brand span{display:grid;place-items:center;width:36px;height:36px;border:1px solid #ffffff25;border-radius:12px;background:#171e20af;box-shadow:0 4px 20px #0004;color:var(--boot-accent);font-size:20px;font-weight:800}\n.boot-content{width:min(100%,900px);text-align:center;display:grid;justify-items:center;gap:24px}.boot h1{max-width:16ch;font-size:clamp(44px,8vw,108px);font-weight:800;line-height:1.04;letter-spacing:-.05em;overflow-wrap:anywhere;text-wrap:balance;color:var(--boot-accent);text-shadow:0 20px 80px #0005}\n.boot-progress{width:112px;height:3px;border-radius:12px;background:#ffffff20;overflow:hidden;margin-top:12px}.boot-progress span{display:block;width:44%;height:100%;border-radius:inherit;background:var(--boot-accent);animation:boot-progress 1.8s ease-in-out infinite}.boot-status{max-width:42ch;min-height:3em;font-size:14px;line-height:1.5;color:#d3dad6;text-wrap:balance}.boot-recovery{min-height:44px}.boot-recovery .row{justify-content:center}.boot-leaving{opacity:0;pointer-events:none}\n@keyframes boot-progress{0%{transform:translateX(-110%)}100%{transform:translateX(340%)}}\n@media(max-width:480px){.dialog{padding:18px;border-radius:18px}.split{grid-template-columns:1fr 1fr;gap:8px}.tabs{gap:4px}.tabs button{font-size:13px;padding:6px 8px}.pill button:focus-visible{outline-offset:-4px}.pill small{display:none}.ended{gap:6px}.standings{list-style:none;margin:0;padding:0;flex-basis:100%;max-height:20dvh;overflow:auto;font-size:13px}.standings li{display:flex;justify-content:space-between;gap:16px;overflow-wrap:anywhere}.ended{max-height:calc(100dvh - 80px);overflow:auto}.ended strong{font-size:13px}.ended button{padding:8px 10px;font-size:13px}.roster{max-height:28dvh}}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}.boot{transition:none}.boot-progress span{animation:none;transform:translateX(65%)}}\n`;\n\n// src/overlay/voice-panel.ts\nfunction voiceEligible(manifest, session) {\n return manifest.voice !== "none" && session?.kind !== "watch" && session?.room?.players.find((player) => player.id === session.room?.you)?.role !== "spectator";\n}\nfunction voiceStatus(voice, t) {\n const key = voice.state === "joining" ? "voiceJoining" : voice.state === "reconnecting" ? "reconnecting" : voice.state === "off" ? "voiceOff" : "voiceOn";\n return t(key);\n}\nfunction updateVoicePanel(container, input) {\n const { session, t } = input, voice = session?.kind === "room" ? session.voice : null;\n if (!voiceEligible(input.manifest, session) || !voice) {\n container.replaceChildren();\n const note = container.ownerDocument.createElement("p");\n note.textContent = t(session?.kind === "watch" || session?.room?.players.find((p) => p.id === session.room?.you)?.role === "spectator" ? "voiceWatch" : "voiceUnavailable");\n container.append(note);\n return;\n }\n if (!container.querySelector("[data-voice-status]")) container.innerHTML = `<p role="status" aria-live="polite" data-voice-status></p><p data-voice-self></p>\n <div class="row"><button type="button" data-action="voice-join"></button><button type="button" data-action="voice-mute"></button><button type="button" data-action="voice-leave"></button></div>\n <p class="error" role="alert" data-voice-error hidden></p><h3 data-voice-heading></h3><ul class="voice-peers" data-voice-peers></ul><p class="muted" data-voice-empty></p>`;\n const get = (selector) => container.querySelector(selector);\n const status = get("[data-voice-status]");\n status.textContent = voiceStatus(voice, t);\n status.dataset.voiceState = voice.state;\n const mic = (value) => t(!value.mic ? "voiceListening" : value.muted ? "voiceMuted" : value.speaking ? "voiceSpeaking" : "voiceMic");\n get("[data-voice-self]").textContent = voice.state === "off" ? "" : `${t("you")}: ${mic(voice)}`;\n const join = get(\'[data-action="voice-join"]\'), mute = get(\'[data-action="voice-mute"]\'), leave = get(\'[data-action="voice-leave"]\');\n join.textContent = t("voiceJoin");\n join.hidden = voice.state !== "off";\n join.disabled = session?.room?.connection !== "connected" || input.pending === "voice.join";\n mute.textContent = t(voice.muted ? "voiceUnmute" : "voiceMute");\n mute.hidden = voice.state !== "on" || !voice.mic;\n mute.disabled = input.pending === "voice.mute";\n mute.setAttribute("aria-pressed", String(voice.muted));\n leave.textContent = t("voiceLeave");\n leave.hidden = voice.state === "off" && input.pending !== "voice.join";\n leave.disabled = input.pending === "voice.leave";\n const error = get("[data-voice-error]");\n error.hidden = !input.error;\n error.textContent = input.error ? t(errorText(input.error)) : "";\n get("[data-voice-heading]").textContent = t("voicePeers");\n get("[data-voice-empty]").textContent = t("voiceEmpty");\n get("[data-voice-empty]").hidden = voice.peers.length > 0;\n const list = get("[data-voice-peers]"), ids = new Set(voice.peers.map((peer) => peer.id));\n for (const row of list.querySelectorAll("[data-voice-peer]")) if (!ids.has(row.dataset.voicePeer)) row.remove();\n for (const peer of voice.peers) {\n let row = [...list.children].find((node) => node.dataset.voicePeer === peer.id);\n if (!row) {\n row = container.ownerDocument.createElement("li");\n row.dataset.voicePeer = peer.id;\n row.innerHTML = \'<div class="row"><strong data-peer-name></strong><small data-peer-status></small></div><label><span data-volume-label></span><input type="range" min="0" max="1" step="0.05" data-control="voice-volume"></label>\';\n row.querySelector("input").dataset.peer = peer.id;\n list.append(row);\n }\n const name = session?.room?.players.find((player) => player.id === peer.id)?.name ?? peer.id;\n row.dataset.mic = String(peer.mic);\n row.dataset.muted = String(peer.muted);\n row.dataset.speaking = String(peer.speaking);\n row.querySelector("[data-peer-name]").textContent = name;\n row.querySelector("[data-peer-status]").textContent = mic(peer);\n row.querySelector("[data-volume-label]").textContent = t("voiceVolume", { name });\n const range = row.querySelector("input");\n if (range.dataset.editing !== "true") range.value = String(peer.volume);\n range.setAttribute("aria-valuetext", `${Math.round(Number(range.value) * 100)}%`);\n range.disabled = voice.state !== "on";\n }\n}\n\n// src/overlay/ui.ts\nvar escape = (value) => String(value ?? "").replace(/[&<>"\']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", \'"\': "&quot;", "\'": "&#39;" })[c]);\nfunction mountOverlay(input) {\n const manifest = input.configuration.manifest;\n if (manifest.overlay?.version !== 1) return null;\n const document = input.container.ownerDocument, win = document.defaultView, t = translator(input.language);\n const host = document.createElement("div");\n host.dataset.caisualOverlay = "";\n host.lang = overlayLanguage(input.language);\n host.style.setProperty("pointer-events", "none", "important");\n const root = host.attachShadow({ mode: "open" });\n if (typeof win.CSSStyleSheet?.prototype.replaceSync === "function" && "adoptedStyleSheets" in root) {\n const sheet = new win.CSSStyleSheet();\n sheet.replaceSync(styles);\n root.adoptedStyleSheets = [sheet];\n } else {\n const sheet = document.createElement("link");\n sheet.rel = "stylesheet";\n sheet.href = "/__caisual/overlay/v1.css";\n root.append(sheet);\n }\n const elements = document.createElement("div");\n elements.dataset.layout = "";\n elements.style.pointerEvents = "none";\n elements.innerHTML = `<div data-surface></div><div class="sr" role="status" aria-live="polite" data-live></div>`;\n const safeProbe = document.createElement("div");\n safeProbe.className = "safe-area-probe";\n safeProbe.setAttribute("aria-hidden", "true");\n root.append(elements, safeProbe);\n const surface = root.querySelector("[data-surface]"), live = root.querySelector("[data-live]");\n surface.style.pointerEvents = "none";\n live.style.pointerEvents = "none";\n const accent = manifest.overlay.accent ?? "#a8efc5";\n host.style.setProperty("--accent", accent);\n const rgb = [1, 3, 5].map((i) => parseInt(accent.slice(i, i + 2), 16) / 255).map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\n const luminance = rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722;\n host.style.setProperty("--accent-ink", luminance > 0.179 ? "#000000" : "#ffffff");\n host.style.setProperty("--boot-accent", luminance > 0.179 ? accent : `color-mix(in srgb, ${accent}, #ffffff 70%)`);\n input.container.append(host);\n let model = initialUi(manifest), disposed = false, operation = 0, lastView = "", geometryFrame = 0;\n let inviteAfterCreate;\n let lastPhase = "", wasModal = false, copyFallback = null;\n let boot = null, bootTimer = 0, bootFadeTimer = 0;\n let voiceError = null, voicePending = null, voiceOperation = 0;\n let codeDraft = input.configuration.invite ?? "";\n const oldInert = Boolean(input.frame.inert), oldTabIndex = input.frame.getAttribute("tabindex");\n try {\n model.shortcutEnabled = win.localStorage.getItem("caisual-overlay-shortcut-v1") !== "off";\n } catch {\n }\n const boards = input.boards ? createBoardController({ manifest, player: input.player.id, guests: input.player.guest, read: input.boards, changed: () => render() }) : null;\n const stops = [];\n const selectedMode = () => manifest.modes.find((mode) => mode.id === model.mode);\n const disabled = () => model.busy ? " disabled" : "";\n const button = (action, key, extra = "", off = false) => `<button type="button" data-action="${action}"${extra}${off || model.busy ? " disabled" : ""}>${t(key)}</button>`;\n function resetBootWait() {\n win.clearTimeout(bootTimer);\n if (!boot || phase(model.session) !== "boot") return;\n boot.querySelector("[data-boot-message]").textContent = t("loading");\n boot.querySelector("[data-boot-recovery]").hidden = true;\n bootTimer = win.setTimeout(() => {\n bootTimer = 0;\n if (disposed || !boot) return;\n boot.querySelector("[data-boot-message]").textContent = t("loadingSlow");\n boot.querySelector("[data-boot-recovery]").hidden = false;\n }, 9e3);\n }\n function updateBoot(loading) {\n if (loading) {\n if (boot) {\n win.clearTimeout(bootFadeTimer);\n bootFadeTimer = 0;\n boot.inert = false;\n boot.removeAttribute("aria-hidden");\n boot.style.pointerEvents = "auto";\n boot.classList.remove("boot-leaving");\n return;\n }\n boot = document.createElement("section");\n boot.className = "boot";\n boot.tabIndex = -1;\n boot.setAttribute("aria-labelledby", "boot-title");\n boot.setAttribute("aria-describedby", "boot-status");\n boot.style.pointerEvents = "auto";\n boot.innerHTML = `<div class="boot-brand" aria-hidden="true"><span>C</span>Caisual</div>\n <div class="boot-content"><h1 id="boot-title">${escape(manifest.name)}</h1>\n <div class="boot-progress" aria-hidden="true"><span></span></div>\n <p class="boot-status" id="boot-status" role="status" aria-live="polite" aria-atomic="true"><span class="sr">${escape(manifest.name)}. </span><span data-boot-message></span></p>\n <div class="boot-recovery"><div class="row" data-boot-recovery hidden>${button("reload", "retry", \' class="primary"\')}${button("exit-now", "exit", \' class="quiet"\')}</div></div>\n </div>`;\n if (input.configuration.coverUrl) {\n const cover = document.createElement("img");\n cover.className = "boot-cover";\n cover.alt = "";\n cover.setAttribute("aria-hidden", "true");\n cover.addEventListener("error", () => {\n cover.hidden = true;\n }, { once: true });\n cover.src = input.configuration.coverUrl;\n boot.prepend(cover);\n }\n elements.append(boot);\n resetBootWait();\n } else if (boot && !boot.inert) {\n boot.inert = true;\n boot.setAttribute("aria-hidden", "true");\n boot.style.pointerEvents = "none";\n boot.classList.add("boot-leaving");\n const remove = () => {\n win.clearTimeout(bootTimer);\n bootTimer = 0;\n boot?.remove();\n boot = null;\n bootFadeTimer = 0;\n };\n if (win.matchMedia?.("(prefers-reduced-motion: reduce)").matches) remove();\n else bootFadeTimer = win.setTimeout(remove, 420);\n }\n }\n const dispatch = (action) => {\n if (disposed) return;\n model = reduceUi(model, action);\n render();\n };\n const announce = (text) => {\n if (live.textContent !== text) live.textContent = text;\n };\n const controls = () => [...root.querySelectorAll(\'button:not(:disabled),a[href],input:not(:disabled),select:not(:disabled),[tabindex="0"]\')].filter((el) => !el.closest("[hidden]"));\n const soloMode = (mode) => (mode === null || manifest.modes.some((item) => item.id === mode)) && risolviModalita(manifest, mode).players.max === 1;\n const solo = () => model.session?.room?.limits.max === 1;\n const roomCode = () => solo() ? null : model.session?.room?.code ?? null;\n const setPanel = (panel) => {\n if (panel === "boards" && boards && !boards.state.query) {\n const id = Object.keys(manifest.boards)[0];\n if (id) boards.select({ board: id, period: (manifest.boards[id].periods ?? ["all-time"])[0], guests: input.player.guest });\n }\n copyFallback = null;\n dispatch({ type: "panel", panel });\n };\n const close = () => {\n const current = phase(model.session), panel = visiblePanel(model);\n if (current === "boot") return;\n if (current === "home") setPanel("home");\n else if (current === "lobby" && panel !== "room") setPanel("room");\n else setPanel(null);\n };\n const toggle = () => {\n if (visiblePanel(model)) close();\n else setPanel(model.session?.room ? "room" : "home");\n };\n async function perform(op, args, after) {\n const token = ++operation;\n dispatch({ type: "error", code: null });\n dispatch({ type: "busy", busy: true });\n try {\n await input.bridge.request(op, args);\n if (token === operation && !disposed) await after?.();\n } catch (error) {\n if (token === operation && !disposed && error.code !== "cancelled") dispatch({ type: "error", code: error.code ?? "offline" });\n } finally {\n if (token === operation && !disposed) dispatch({ type: "busy", busy: false });\n }\n }\n function updateVoice() {\n const container = root.querySelector("[data-voice-panel]");\n if (container) updateVoicePanel(container, { manifest, session: model.session, t, error: voiceError, pending: voicePending });\n const toggle2 = root.querySelector("[data-voice-toggle]"), voice = model.session?.voice;\n if (toggle2) {\n toggle2.dataset.voiceState = voice?.state ?? "off";\n toggle2.dataset.muted = String(voice?.muted ?? false);\n toggle2.setAttribute("aria-label", `${t("voice")}: ${voice ? voiceStatus(voice, t) : t("voiceOff")}`);\n toggle2.textContent = voice?.state === "on" && !voice.muted ? "\\u25CF" : "\\u25CB";\n }\n }\n async function performVoice(op, args) {\n const sessionId = model.session?.id, epoch = input.bridge.epoch, volume = op === "voice.setVolume";\n const token = volume ? voiceOperation : ++voiceOperation;\n const current = () => !disposed && model.session?.id === sessionId && input.bridge.epoch === epoch && token === voiceOperation;\n voiceError = null;\n if (!volume) voicePending = op;\n updateVoice();\n try {\n await input.bridge.request(op, args);\n } catch (error) {\n if (current()) voiceError = error.code ?? "voice_error";\n } finally {\n if (current()) {\n if (!volume) voicePending = null;\n updateVoice();\n }\n }\n }\n async function copyInvite() {\n const code = roomCode();\n if (!code) return;\n const url = input.inviteUrl(code);\n try {\n await win.navigator.clipboard.writeText(url);\n dispatch({ type: "notice", notice: t("copied") });\n } catch {\n copyFallback = url;\n render();\n root.querySelector(\'input[data-control="invite-link"]\')?.select();\n }\n }\n function invitation() {\n if (solo()) return "";\n const code = roomCode();\n if (!code) return `<p>${t("noRoom")}</p>`;\n return `<div class="row"><div class="grow"><small>${t("code")}</small><div class="code" data-room-code>${escape(code)}</div></div>${button("copy", "copy")}</div>${copyFallback ? `<label>${t("copyFailed")}<input data-control="invite-link" readonly value="${escape(copyFallback)}"></label>` : ""}`;\n }\n function navigation(panel) {\n const items = [];\n if (model.session?.room && !solo()) items.push(["room", "room"], ["invite", "copy"]);\n items.push(["friends", "friends"]);\n if (Object.keys(manifest.boards).length) items.push(["boards", "boards"]);\n if (voiceEligible(manifest, model.session)) items.push(["voice", "voice"]);\n return `<nav class="tabs" aria-label="Caisual">${items.map(([id, key]) => button(`panel:${id}`, key, ` aria-current="${id === panel}"`)).join("")}</nav>`;\n }\n function home() {\n const selected = selectedMode(), action = primaryAction(manifest, model.mode), session = model.session;\n const description = resolveText(manifest.description, input.language, manifestLanguages(manifest)[0]);\n const hasRooms = manifest.modes.some((mode) => mode.execution === "room" && risolviModalita(manifest, mode.id).players.max > 1);\n return `${input.configuration.iconUrl ? `<img class="game-icon game-icon-title" src="${escape(input.configuration.iconUrl)}" alt="" />` : ""}<h1>${escape(manifest.name)}</h1>${description ? `<p class="muted" data-game-description>${escape(description)}</p>` : ""}<p class="muted" data-game-languages>${t("gameLanguages")}: ${escape(manifestLanguages(manifest).join(" \\xB7 "))}</p><label>${t("mode")}<select data-control="mode"${disabled()}>${manifest.modes.map((mode) => `<option value="${escape(mode.id)}"${mode.id === model.mode ? " selected" : ""}>${escape(risolviPresentazione(manifest, mode.id, input.language).label)}</option>`).join("")}</select></label>\n ${selected?.instructions ? `<p class="muted">${escape(resolveText(selected.instructions, input.language, manifestLanguages(manifest)[0]))}</p>` : ""}\n ${input.configuration.invite && phase(session) === "home" ? button("join-invite", input.bridge.watch ? "watch" : "joinInvite", \' class="primary"\', !session?.ready) : ""}\n ${action ? button("play", action.friends ? "friendsPlay" : "play", \' class="primary"\', !session?.ready) : ""}\n ${selected?.matchmaking ? button("match", "find", "", !selected.matchmaking.defaults || !session?.ready) : ""}\n ${session?.resume ? button("resume", "resume", "", !session.ready) + `<small>${escape(solo() || soloMode(session.resume.mode) ? "" : session.resume.code)}</small>` : ""}\n ${hasRooms && !solo() ? `<div class="split">${button("panel:join", "join", "", !session?.ready)}${manifest.spectators ? button("panel:watch", "watch", "", !session?.ready) : ""}</div>` : ""}\n ${solo() ? button("panel:exit", "exit", \' class="quiet"\') : ""}${navigation("home")}<label class="checkbox"><input type="checkbox" data-control="shortcut"${model.shortcutEnabled ? " checked" : ""}>${t("shortcut")}</label>`;\n }\n function room() {\n const session = model.session, room2 = session?.room;\n if (!room2) return `<p>${t("noRoom")}</p>`;\n const own = room2.players.find((player) => player.id === room2.you), lobby = room2.status === "lobby" && session?.kind === "room";\n const canRole = session?.kind === "room" && (lobby || room2.status === "playing" && room2.requestRole);\n const reason = startReason(manifest, session);\n return `${invitation()}<ul class="roster" aria-label="${t("room")}">${room2.players.map((p) => `<li data-player-id="${escape(p.id)}"><span class="name">${escape(p.name)} ${p.id === room2.you ? `<small>(${t("you")})</small>` : ""}</span>${p.id === room2.host ? `<span class="badge">${t("host")}</span>` : ""}${p.role ? `<small>${escape(resolveText(manifest.roles.find((r) => r.id === p.role)?.label, input.language, manifestLanguages(manifest)[0], p.role))}</small>` : ""}${p.team ? `<small>${t("team")} ${p.team}</small>` : ""}<small>${!p.connected ? t("away") : lobby ? t(p.ready ? "ready" : "unready") : ""}</small></li>`).join("")}</ul>\n ${canRole && manifest.roles.length ? `<label>${t("role")}<select data-control="role"${disabled()}><option value="" disabled${!own?.role ? " selected" : ""}>${t("role")}</option>${manifest.roles.map((role) => `<option value="${escape(role.id)}"${role.id === own?.role ? " selected" : ""}>${escape(resolveText(role.label, input.language, manifestLanguages(manifest)[0], role.id))}</option>`).join("")}</select></label>` : ""}\n ${lobby && manifest.teams ? `<label>${t("team")}<select data-control="team"${disabled()}><option value="" disabled${!own?.team ? " selected" : ""}>${t("team")}</option>${Array.from({ length: manifest.teams.max }, (_, i) => `<option value="${i + 1}"${own?.team === i + 1 ? " selected" : ""}>${t("team")} ${i + 1}</option>`).join("")}</select></label>` : ""}\n ${lobby ? `<div class="row">${button("ready", own?.ready ? "unready" : "ready", \' class="primary"\', room2.connection !== "connected")}${room2.host === room2.you ? button("start", "start", "", reason !== null) : ""}</div>${reason ? `<p class="muted" data-start-reason>${t(reason)}</p>` : ""}` : ""}\n ${session?.kind === "watch" ? `<p>${t("watching")} \\xB7 ${t("delay", { n: (room2.delayMs ?? 0) / 1e3 })}</p>` : ""}\n ${navigation("room")}<label class="checkbox"><input type="checkbox" data-control="shortcut"${model.shortcutEnabled ? " checked" : ""}>${t("shortcut")}</label>${button("panel:exit", "exit", \' class="quiet"\')}`;\n }\n function crew() {\n const provider = input.crew, state = provider?.getSnapshot();\n if (!provider || provider.unavailable || !state?.you) return `<p>${t(provider?.unavailable === "local" ? "localCrew" : "loginCrew")}</p>`;\n const online = state.friends.filter((friend) => friend.online), party = state.party;\n const person = (p) => `<li>${p.game ? `<img class="game-icon" src="${escape(p.game.iconUrl)}" alt="" />` : ""}<span class="name">${escape(p.name)}<small>${p.game ? ` \\xB7 ${escape(p.game.name)}` : ""}</small></span>${p.room && p.game ? button("follow", "follow", ` data-code="${escape(p.room.code)}" data-game="${escape(p.game.slug)}"`) : ""}${party?.leader === state.you.id && !party.members.some((member) => member.id === p.id) ? button("party-invite", "inviteParty", ` data-player="${escape(p.id)}"`) : ""}</li>`;\n return `${!state.connected ? `<p>${t("reconnecting")}</p>` : ""}${party ? `<ul class="roster">${party.members.map(person).join("")}</ul>${button("party-leave", "leaveParty")}` : button("party-create", "createParty")}\n ${state.invites.map((invite) => `<div class="row"><span class="grow">${escape(invite.from.name)}</span>${button("party-accept", "accept", ` data-party="${escape(invite.party)}"`)}${button("party-decline", "decline", ` data-party="${escape(invite.party)}"`)}</div>`).join("")}\n ${state.follow ? `<div class="row"><span class="grow"><img class="game-icon" src="${escape(state.follow.game.iconUrl)}" alt="" /> ${escape(state.follow.from.name)} \\xB7 ${escape(state.follow.game.name)}</span>${button("follow", "follow", ` data-code="${escape(state.follow.code)}" data-game="${escape(state.follow.game.slug)}"`)}</div>` : ""}\n <h2>${t("online")}</h2>${online.length ? `<ul class="roster">${online.map(person).join("")}</ul>` : `<p class="muted">${t("noFriends")}</p>`}`;\n }\n function leaderboard() {\n if (!boards || !boards.state.query) return `<p>${t("unavailable")}</p>`;\n const { query, data, loading, error, saving } = boards.state;\n const board = manifest.boards[query.board];\n return `<label>${t("board")}<select data-control="board">${Object.entries(manifest.boards).map(([id, value]) => `<option value="${escape(id)}"${query.board === id ? " selected" : ""}>${escape(resolveText(value.label, input.language, manifestLanguages(manifest)[0], id))}</option>`).join("")}</select></label>\n <div class="split"><label>${t("period")}<select data-control="period">${(board.periods ?? ["all-time"]).map((period) => `<option value="${period}"${query.period === period ? " selected" : ""}>${t(period === "daily" ? "daily" : "allTime")}</option>`).join("")}</select></label><label>${t("category")}<select data-control="category"><option value="accounts"${!query.guests ? " selected" : ""}>${t("accounts")}</option><option value="guests"${query.guests ? " selected" : ""}>${t("guests")}</option></select></label></div>\n ${query.period === "daily" ? `<small data-board-day>${escape(data?.day ?? query.day ?? new Date(input.bridge.serverTime() ?? Date.now()).toISOString().slice(0, 10))}</small>` : ""}\n ${saving ? `<p role="status" data-saving>${t(saving)}</p>` : ""}${error ? `<p role="alert">${t("offline")}</p>` : ""}\n ${data ? `<div class="table-wrap"><table><thead><tr><th>${t("rank")}</th><th>${t(query.guests ? "guests" : "accounts")}</th><th>${t("score")}</th></tr></thead><tbody>${data.entries.map((entry) => `<tr${entry.me ? \' class="self"\' : ""}><td>${entry.rank}</td><td>${escape(entry.name)}${entry.verified ? `<small>${t("verified")}</small>` : ""}</td><td>${entry.score}</td></tr>`).join("")}</tbody></table>${data.entries.length ? "" : `<p>${t("empty")}</p>`}</div><p data-own-score>${t("own")} (${t(data.ownGuest ? "guests" : "accounts")}): ${data.me ? `#${data.me.rank} \\xB7 ${data.me.score}${data.me.verified ? ` \\xB7 ${t("verified")}` : ""}` : t("empty")}</p>` : `<p>${t(loading ? "loading" : "empty")}</p>`}\n ${button("refresh", "refresh", "", loading)}`;\n }\n function content(panel) {\n switch (panel) {\n case "home":\n return home();\n case "room":\n return room();\n case "invite":\n return invitation();\n case "friends":\n return crew();\n case "voice":\n return \'<div class="stack" data-voice-panel></div>\';\n case "boards":\n return leaderboard();\n case "join":\n case "watch":\n return `<form class="stack" data-form="${panel}"><label>${t("code")}<input data-control="code" name="code" autocomplete="off" autocapitalize="characters" spellcheck="false" maxlength="16" value="${escape(codeDraft)}" required></label><button class="primary" type="submit"${disabled()}>${t(panel === "join" ? "join" : "watch")}</button></form>`;\n case "attaching":\n case "matching":\n return `<p role="status">${t(panel === "matching" ? "matching" : "joining")}</p>${model.session?.waiting ? `<p>${t("queue", { n: model.session.waiting.players, max: model.session.waiting.max })}</p>` : ""}<button type="button" data-action="cancel">${t("cancel")}</button>`;\n case "countdown":\n return `<p>${t("starting")}</p><div class="countdown" data-countdown></div>`;\n case "boot":\n return "";\n case "error":\n return `<p role="alert">${t(model.session?.room?.connection === "replaced" ? "replaced" : "noRoom")}</p>${button("leave", "home")}${button("exit-now", "exit")}`;\n case "exit":\n return model.session?.kind === "room" && phase(model.session) !== "ended" ? `<p>${t(model.session.room?.persistent ? "leaveHint" : "temporaryHint")}</p>${invitation()}${button("disconnect-exit", "leaveNow", \' class="primary"\')}<p class="muted">${t("abandonHint")}</p>${button("leave-exit", "leaveRoom")}` : button("leave-exit", "exit", \' class="primary"\');\n }\n }\n function title(panel) {\n const keys = { boot: "loading", home: "home", room: "room", invite: "copy", friends: "friends", voice: "voice", boards: "boards", join: "join", watch: "watch", attaching: "joining", matching: "matching", countdown: "starting", error: "error", exit: "exit" };\n return t(keys[panel]);\n }\n function updateCountdown() {\n const at = model.session?.room?.countdownAt, now = input.bridge.serverTime();\n const value = at === null || at === void 0 || now === null ? "..." : String(Math.max(0, Math.round((at - now) / 1e3)));\n const node = root.querySelector("[data-countdown]");\n if (node && node.textContent !== value) {\n node.textContent = value;\n announce(`${t("starting")} ${value}`);\n }\n }\n function geometry() {\n geometryFrame = 0;\n if (disposed || !input.bridge.epoch || !model.session) return;\n const frame = gameViewport(input.frame), { scaleX, scaleY } = frame;\n const reservedRects = [...root.querySelectorAll("[data-reserve]")].map((el) => {\n const rect = el.getBoundingClientRect(), left = Math.max(frame.left, rect.left), top = Math.max(frame.top, rect.top), right = Math.min(frame.right, rect.right), bottom = Math.min(frame.bottom, rect.bottom);\n return { x: Math.max(0, Math.round((left - frame.left) * scaleX)), y: Math.max(0, Math.round((top - frame.top) * scaleY)), width: Math.max(0, Math.round((right - left) * scaleX)), height: Math.max(0, Math.round((bottom - top) * scaleY)) };\n }).filter((rect) => rect.width && rect.height).slice(0, 8);\n const view = { inputBlocked: phase(model.session) === "boot" || !!visiblePanel(model), reservedRects, safeArea: measureSafeArea(input.frame, safeProbe), shortcutEnabled: model.shortcutEnabled };\n const serialized = `${input.bridge.epoch}:${JSON.stringify(view)}`;\n if (lastView === serialized) return;\n lastView = serialized;\n void input.bridge.request("overlay.view", view).catch(async (error) => {\n if (error?.code === "invalid_request" && lastView === serialized) {\n const { safeArea, ...legacy } = view;\n try {\n await input.bridge.request("overlay.view", legacy);\n return;\n } catch {\n }\n }\n if (lastView === serialized) lastView = "";\n });\n }\n function resize() {\n if (!geometryFrame) geometryFrame = win.requestAnimationFrame(geometry);\n }\n function resultBar() {\n const presentation = matchPresentation(model.session);\n if (!presentation) return `<strong>${t("ended")}</strong>`;\n const room2 = model.session.room, { result } = presentation;\n const unit = result.unit && ["points", "time", "distance"].includes(result.unit) ? t(result.unit) : result.unit;\n const rows = result.standings.map((p, i) => {\n const name = room2.players.find((player) => player.id === p.playerId).name;\n const score = p.score === void 0 ? "" : `<span>${escape(String(p.score))}${unit ? ` ${escape(unit)}` : ""}</span>`;\n return `<li><span>${p.rank ?? i + 1}. ${escape(name)}</span>${score}</li>`;\n }).join("");\n return `<strong data-outcome>${t(presentation.outcome)}</strong><ol class="standings" aria-label="${t("standings")}">${rows}</ol>`;\n }\n function rematchBar() {\n const session = model.session, room2 = session?.room;\n if (!room2 || room2.status !== "finished" || solo()) return "";\n const active = room2.players.filter((p) => p.connected && p.role !== "spectator");\n const ready = active.filter((p) => p.ready), host2 = session.kind === "room" && room2.host === room2.you;\n const canStart = room2.connection === "connected" && active.length >= room2.limits.min && ready.length === active.length;\n return `<small role="status" data-rematch-ready>${t("rematchReady", { n: ready.length, max: active.length })}</small>\n ${ready.length ? `<small data-rematch-players>${ready.map((p) => escape(p.name)).join(", ")}</small>` : ""}\n ${room2.rematch?.autoStart ? "" : host2 ? button("restart", "rematchStart", \' class="primary"\', !canStart) : `<small>${t("waitHost")}</small>`}`;\n }\n function render() {\n if (disposed) return;\n const panel = visiblePanel(model), current = phase(model.session), room2 = model.session?.room;\n const focused = root.activeElement;\n const focusPeer = focused?.dataset.peer;\n const focusKey = focused?.dataset.control ? ["control", focused.dataset.control] : focused?.dataset.action ? ["action", focused.dataset.action] : null;\n const previousScroll = root.querySelector(".dialog")?.scrollTop ?? 0;\n const selection = focused?.tagName === "INPUT" ? { start: focused.selectionStart, end: focused.selectionEnd } : null;\n const crewState = input.crew?.getSnapshot(), invitations = (crewState?.invites.length ?? 0) + (crewState?.follow ? 1 : 0);\n const label = current === "watching" ? t("watching") : room2?.connection === "reconnecting" ? t("reconnecting") : solo() ? "Caisual" : room2?.code ?? "Caisual";\n surface.innerHTML = current === "boot" ? "" : `<div class="pill" data-reserve><button type="button" data-action="menu" aria-label="${t("menu")}" aria-expanded="${!!panel}">C<span aria-hidden="true"><small>${escape(label)}</small></span></button>${voiceEligible(manifest, model.session) && model.session?.kind === "room" ? `<button type="button" class="voice-toggle" data-voice-toggle data-action="panel:voice"></button>` : ""}${invitations ? `<button type="button" data-action="panel:friends" aria-label="${t("friends")} (${invitations})">${invitations}</button>` : ""}</div>\n ${current === "ended" && !panel ? `<div class="ended" data-reserve role="region" aria-label="${t("ended")}">${resultBar()}${boards?.state.saving ? `<small role="status" data-saving>${t(boards.state.saving)}</small>` : ""}${canPlayAgain(model.session) ? button("again", "again", \' class="primary"\') : model.session?.kind === "room" && room2?.status !== "finished" && !solo() ? `<small>${t("waitHost")}</small>` : ""}${rematchBar()}${Object.keys(manifest.boards).length ? button("panel:boards", "boards") : ""}${button("panel:home", "homeMenu")}</div>` : ""}\n ${panel ? `<div class="backdrop${panel === "home" ? " home" : ""}"><section class="dialog${panel === "boards" || panel === "friends" ? " wide" : ""}" role="dialog" aria-modal="true" aria-labelledby="panel-title" tabindex="-1"><div class="top"><h2 id="panel-title">${title(panel)}</h2><button type="button" data-action="close" aria-label="${t("close")}">\\xD7</button></div><div class="stack">${content(panel)}${model.error ? `<p class="error" role="alert" data-error>${t(errorText(model.error))}</p>${model.error === "version_outdated" ? button("reload-game", "reloadGame", \' class="primary"\') : ""}` : ""}${model.session?.resumeError ? `<p class="error" role="alert">${t("saveFailed")}</p>` : ""}${model.notice ? `<p class="notice" role="status">${escape(model.notice)}</p>` : ""}</div></section></div>` : ""}`;\n for (const element of surface.querySelectorAll(".pill,.backdrop,.ended")) element.style.pointerEvents = "auto";\n const backdrop = root.querySelector(".backdrop.home");\n if (backdrop && input.configuration.coverUrl) backdrop.style.backgroundImage = `linear-gradient(#0b151c99,#0b151cee),url(${JSON.stringify(input.configuration.coverUrl)})`;\n host.dataset.phase = current;\n host.dataset.panel = panel ?? "";\n input.frame.inert = !!panel || oldInert;\n if (panel) input.frame.tabIndex = -1;\n else if (oldTabIndex === null) input.frame.removeAttribute("tabindex");\n else input.frame.setAttribute("tabindex", oldTabIndex);\n updateVoice();\n updateBoot(current === "boot");\n const dialog = root.querySelector(".dialog");\n if (dialog) dialog.scrollTop = previousScroll;\n const focusPanel = current === "boot" ? boot : dialog;\n const matched = focusKey ? [...root.querySelectorAll(`[data-${focusKey[0]}]`)].find((el) => el.getAttribute(`data-${focusKey[0]}`) === focusKey[1] && el.dataset.peer === focusPeer) : null;\n if (!panel && wasModal && !input.frame.inert && input.frame.isConnected) {\n input.frame.focus({ preventScroll: true });\n input.frame.contentWindow?.focus();\n } else if (matched && (!panel || focusPanel?.contains(matched)) && !matched.hasAttribute("disabled")) {\n matched.focus({ preventScroll: true });\n if (matched.tagName === "INPUT" && selection?.start !== null && selection?.end !== null && selection) matched.setSelectionRange(selection.start, selection.end);\n } else if (panel && (!wasModal || focused)) (current === "boot" ? boot : dialog?.querySelector(\'select,input,button:not([data-action="close"]):not(:disabled)\') ?? dialog)?.focus({ preventScroll: true });\n wasModal = !!panel;\n if (lastPhase !== current) {\n lastPhase = current;\n announce(current === "boot" ? "" : t({ home: "home", attaching: "joining", matching: "matching", lobby: "room", countdown: "starting", playing: "playing", ended: "ended", watching: "watching", error: "error" }[current]));\n }\n updateCountdown();\n resize();\n }\n const click = (event) => {\n const target = event.target.closest("button[data-action]");\n if (!target || target.disabled) return;\n const action = target.dataset.action;\n event.stopPropagation();\n if (action.startsWith("panel:")) {\n setPanel(action.slice(6));\n return;\n }\n switch (action) {\n case "menu":\n toggle();\n break;\n case "close":\n close();\n break;\n case "play": {\n const selected = primaryAction(manifest, model.mode);\n if (selected) void perform(selected.op, { mode: model.mode });\n break;\n }\n case "match":\n void perform("room.match", { mode: model.mode });\n break;\n case "join-invite":\n if (input.configuration.invite) void perform(input.bridge.watch ? "room.watch" : "room.join", { code: input.configuration.invite });\n break;\n case "resume":\n void perform("session.resume", {});\n break;\n case "cancel":\n void perform("session.cancel", {});\n break;\n case "ready":\n void perform("room.ready", { ready: !model.session?.room?.players.find((p) => p.id === model.session?.room?.you)?.ready });\n break;\n case "start":\n void perform("room.start", {});\n break;\n case "restart":\n void perform("room.restart", {});\n break;\n case "copy":\n void copyInvite();\n break;\n case "voice-join":\n void performVoice("voice.join", {});\n break;\n case "voice-mute":\n void performVoice("voice.mute", { muted: !model.session?.voice?.muted });\n break;\n case "voice-leave":\n void performVoice("voice.leave", {});\n break;\n case "again": {\n if (!canPlayAgain(model.session)) break;\n if (model.session?.kind === "local") void perform("local.start", { mode: model.session.mode ?? model.mode });\n else if (model.session?.room?.status === "finished") void perform("room.restart", {});\n else if (solo()) void perform("room.create", { mode: model.session?.room?.mode ?? null });\n else {\n inviteAfterCreate = roomCode();\n void perform("room.create", { mode: model.session?.room?.mode ?? null }).then(() => {\n if (model.error) inviteAfterCreate = void 0;\n });\n }\n break;\n }\n case "disconnect-exit":\n void perform("session.disconnect", {}, input.exit);\n break;\n case "leave-exit":\n void perform("session.leave", {}, input.exit);\n break;\n case "leave":\n void perform("session.leave", {}, () => setPanel("home"));\n break;\n case "exit-now":\n input.exit();\n break;\n case "reload-game":\n input.bridge.reload();\n break;\n case "reload":\n boot?.focus({ preventScroll: true });\n resetBootWait();\n input.frame.src = input.frame.src;\n break;\n case "refresh":\n void boards?.refresh();\n break;\n case "party-create":\n input.crew?.party.create();\n break;\n case "party-leave":\n input.crew?.party.leave();\n break;\n case "party-invite":\n input.crew?.party.invite(target.dataset.player);\n break;\n case "party-accept":\n input.crew?.party.accept(target.dataset.party);\n break;\n case "party-decline":\n input.crew?.party.decline(target.dataset.party);\n break;\n case "follow":\n input.crew?.follow(target.dataset.game, target.dataset.code);\n break;\n }\n };\n const change = (event) => {\n const target = event.target, field = target.dataset.control;\n if (field === "voice-volume") {\n target.dataset.editing = "true";\n void performVoice("voice.setVolume", { playerId: target.dataset.peer, volume: Number(target.value) }).finally(() => {\n delete target.dataset.editing;\n updateVoice();\n });\n return;\n }\n if (field === "mode") dispatch({ type: "mode", mode: target.value });\n if (field === "role") void perform(model.session?.room?.status === "lobby" ? "room.role" : "room.requestRole", { role: target.value });\n if (field === "team") void perform("room.team", { team: Number(target.value) });\n if (field === "shortcut") {\n const enabled = target.checked;\n try {\n win.localStorage.setItem("caisual-overlay-shortcut-v1", enabled ? "on" : "off");\n } catch {\n }\n dispatch({ type: "shortcut", enabled });\n }\n const query = boards?.state.query;\n if (query && ["board", "period", "category"].includes(field ?? "")) {\n const next = { ...query };\n if (field === "board") {\n next.board = target.value;\n next.period = (manifest.boards[next.board].periods ?? ["all-time"])[0];\n delete next.day;\n }\n if (field === "period") {\n next.period = target.value;\n delete next.day;\n }\n if (field === "category") next.guests = target.value === "guests";\n boards.select(next);\n }\n };\n const submit = (event) => {\n const form = event.target;\n if (!form.dataset.form) return;\n event.preventDefault();\n const code = normalizeInvite(form.querySelector(\'input[data-control="code"]\').value);\n if (!code) {\n dispatch({ type: "error", code: "invalid_code" });\n return;\n }\n void perform(form.dataset.form === "watch" ? "room.watch" : "room.join", { code });\n };\n const keydown = (event) => {\n const panel = visiblePanel(model);\n if (panel && event.key === "Escape") {\n event.preventDefault();\n event.stopImmediatePropagation();\n close();\n return;\n }\n if (panel && event.key === "Tab") {\n const items = controls().filter((el) => el.closest(panel === "boot" ? ".boot" : ".dialog")), first = items[0], last = items.at(-1);\n if (!first) {\n event.preventDefault();\n return;\n }\n if (event.shiftKey && (root.activeElement === first || !items.includes(root.activeElement))) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && (root.activeElement === last || !items.includes(root.activeElement))) {\n event.preventDefault();\n first.focus();\n }\n } else if (!panel && model.shortcutEnabled && event.key === "Tab" && event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {\n event.preventDefault();\n toggle();\n }\n };\n root.addEventListener("input", (event) => {\n const node = event.target;\n if (node.dataset.control === "code") codeDraft = node.value;\n if (node.dataset.control === "voice-volume") node.dataset.editing = "true";\n });\n root.addEventListener("click", click);\n root.addEventListener("change", change);\n root.addEventListener("submit", submit);\n win.addEventListener("keydown", keydown, true);\n win.addEventListener("resize", resize);\n win.addEventListener("scroll", resize, true);\n win.visualViewport?.addEventListener("resize", resize);\n win.visualViewport?.addEventListener("scroll", resize);\n const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null;\n observer?.observe(input.frame);\n const countdownTimer = win.setInterval(updateCountdown, 250);\n stops.push(input.bridge.subscribe((session) => {\n const previous = model.session;\n if (!session || session.id !== previous?.id) {\n voiceOperation++;\n voicePending = null;\n voiceError = null;\n }\n if (!session) {\n lastView = "";\n boards?.reset();\n operation++;\n model.busy = false;\n }\n if (previous && session && JSON.stringify({ ...previous, voice: null }) === JSON.stringify({ ...session, voice: null })) {\n model = reduceUi(model, { type: "session", session });\n updateVoice();\n } else dispatch({ type: "session", session });\n const created = roomCode();\n if (inviteAfterCreate !== void 0 && created && created !== inviteAfterCreate) {\n inviteAfterCreate = void 0;\n setPanel("invite");\n dispatch({ type: "notice", notice: t("newRoom") });\n void copyInvite();\n }\n }));\n stops.push(input.bridge.onOpen((panel) => setPanel(panel)), input.bridge.onShortcut(toggle), input.bridge.onError(({ error }) => {\n if (!visiblePanel(model)) setPanel("room");\n dispatch({ type: "error", code: error.code });\n }));\n stops.push(input.bridge.onScore((score) => boards?.queued(score)));\n if (input.crew) stops.push(input.crew.subscribe(() => {\n const state = input.crew.getSnapshot();\n if (state.follow || state.invites.length) announce(t("friends"));\n render();\n }));\n render();\n return { element: host, root, dispose() {\n disposed = true;\n operation++;\n stops.forEach((stop) => stop());\n boards?.dispose();\n observer?.disconnect();\n win.clearInterval(countdownTimer);\n win.cancelAnimationFrame(geometryFrame);\n win.clearTimeout(bootTimer);\n win.clearTimeout(bootFadeTimer);\n win.removeEventListener("keydown", keydown, true);\n win.removeEventListener("resize", resize);\n win.removeEventListener("scroll", resize, true);\n win.visualViewport?.removeEventListener("resize", resize);\n win.visualViewport?.removeEventListener("scroll", resize);\n input.frame.inert = oldInert;\n if (oldTabIndex === null) input.frame.removeAttribute("tabindex");\n else input.frame.setAttribute("tabindex", oldTabIndex);\n void input.bridge.request("overlay.view", { inputBlocked: false, reservedRects: [], shortcutEnabled: false }).catch(() => {\n });\n host.remove();\n } };\n}\nexport {\n avviaHandshake,\n creaPonteOspite,\n eMessaggioReady,\n eRichiestaBiglietto,\n mountOverlay,\n overlayConfiguration,\n overlayLanguage,\n overlayLocale,\n styles as overlayStyles,\n stanzaDaMessaggio\n};\n');
5326
+ response.end(request.method === "HEAD" ? void 0 : '// ../contracts/src/slug.ts\nvar NOMI_RISERVATI = [\n "www",\n "api",\n "app",\n "play",\n "live",\n "multi",\n "cdn",\n "assets",\n "static",\n "mail",\n "mx",\n "ns1",\n "ns2",\n "autodiscover",\n "_dmarc",\n "admin",\n "login",\n "account",\n "auth",\n "pay",\n "secure",\n "support",\n "help",\n "blog",\n "status",\n "dev",\n "staging",\n "test",\n "caisual",\n "shipz"\n];\nvar RISERVATI = new Set(NOMI_RISERVATI);\nvar SLUG_NUOVO = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nvar SLUG_STORICO = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;\nfunction isValidSlug(value) {\n return value.length >= 3 && value.length <= 32 && SLUG_NUOVO.test(value) || SLUG_STORICO.test(value);\n}\nfunction isReservedSlug(value) {\n return RISERVATI.has(value);\n}\n\n// ../contracts/src/i18n.ts\nfunction normalizeLanguage(value) {\n if (typeof value !== "string" || value.length > 128) return null;\n try {\n return Intl.getCanonicalLocales(value)[0] ?? null;\n } catch {\n return null;\n }\n}\nfunction manifestLanguages(manifest) {\n return manifest.languages?.length ? [...manifest.languages] : [manifest.language ?? "en"];\n}\nfunction languageFallbacks(language, defaultLanguage = "en") {\n const result = [];\n let tag = normalizeLanguage(language);\n while (tag) {\n result.push(tag);\n const parts = tag.split("-");\n parts.pop();\n if (parts.at(-1)?.length === 1) parts.pop();\n tag = parts.join("-");\n }\n result.push(normalizeLanguage(defaultLanguage) ?? defaultLanguage);\n return [...new Set(result)];\n}\nfunction resolveText(value, language, defaultLanguage = "en", key = "") {\n if (typeof value === "string") return value;\n if (value) {\n for (const tag of languageFallbacks(language, defaultLanguage)) {\n const name = Object.keys(value).find((name2) => name2.toLowerCase() === tag.toLowerCase());\n if (name !== void 0 && typeof value[name] === "string") return value[name];\n }\n }\n return key;\n}\n\n// ../contracts/src/manifest.ts\nfunction risolviModalita(manifest, mode) {\n const scelta = mode === null ? void 0 : manifest.modes.find((voce) => voce.id === mode);\n if (mode !== null && scelta === void 0) throw new Error("The selected game mode does not exist.");\n return { players: { ...scelta?.players ?? manifest.players }, lobby: scelta?.lobby ?? manifest.lobby };\n}\nfunction risolviPresentazione(manifest, mode, language = manifestLanguages(manifest)[0]) {\n risolviModalita(manifest, mode);\n const scelta = manifest.modes.find((voce) => voce.id === mode);\n return {\n execution: scelta?.execution ?? null,\n label: resolveText(scelta?.label, language, manifestLanguages(manifest)[0], scelta?.id ?? manifest.name ?? "Play"),\n instructions: resolveText(scelta?.instructions, language, manifestLanguages(manifest)[0]) || null\n };\n}\nvar TETTO_GIOCATORI = 24;\nvar RITARDO_SPETTATORI_MS = 3e3;\nvar MASSIMO_CLASSIFICHE = 32;\nvar CAMPI = /* @__PURE__ */ new Set([\n "overlay",\n "manifest",\n "id",\n "name",\n "description",\n "cover",\n "card",\n "icon",\n "screenshots",\n "tags",\n "languages",\n "language",\n "platform",\n "orientation",\n "input",\n "visibility",\n "network",\n "isolated",\n "requires",\n "players",\n "lobby",\n "persistent",\n "replays",\n "spectators",\n "boards",\n "roles",\n "teams",\n "voice",\n "modes"\n]);\nvar INPUT = /* @__PURE__ */ new Set(["keyboard", "mouse", "touch", "gamepad"]);\nvar PLATFORM = /* @__PURE__ */ new Set(["desktop", "mobile", "both"]);\nvar ORIENTATION = /* @__PURE__ */ new Set(["landscape", "portrait"]);\nvar VISIBILITY = /* @__PURE__ */ new Set(["public", "unlisted"]);\nvar VOICE = /* @__PURE__ */ new Set(["none", "room", "team", "proximity"]);\nvar PERFORMANCE = /* @__PURE__ */ new Set(["light", "medium", "heavy"]);\nvar TAG = /^[a-z0-9-]+$/;\nvar ID_INTERNO = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nvar CAMPO_MATCHMAKING = /^[a-z0-9][a-z0-9-]{0,31}$/;\nvar ID_CLASSIFICA = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nfunction oggetto(value) {\n if (typeof value !== "object" || value === null || Array.isArray(value)) return null;\n return value;\n}\nfunction percorsoRelativo(value) {\n if (value === "" || value.startsWith("/") || value.includes("\\\\") || value.includes("\\0")) return false;\n if (value.includes("?") || value.includes("#")) return false;\n const parti = value.split("/");\n if (parti.some((parte) => parte === "" || parte === "." || parte === "..")) return false;\n try {\n const decoded = parti.map((parte) => decodeURIComponent(parte));\n return !decoded.some((parte) => parte === "" || parte === "." || parte === ".." || parte.includes("/"));\n } catch {\n return false;\n }\n}\nfunction hostValido(value) {\n if (value.length === 0 || value.length > 253) return false;\n if (value.includes("://") || /[/:?#@]/.test(value)) return false;\n const parti = value.split(".");\n return parti.every(\n (parte) => parte.length >= 1 && parte.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(parte)\n );\n}\nfunction interoTra(value, min, max) {\n return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;\n}\nfunction stringaDefault(dati, campo, valoreDefault, errori) {\n const value = dati[campo];\n if (value === void 0) return valoreDefault;\n if (typeof value !== "string") {\n errori.push(`${campo}: must be a string.`);\n return valoreDefault;\n }\n return value;\n}\nfunction testoFacoltativo(value, key, max, path, errors) {\n if (value[key] === void 0) return void 0;\n const check = (text2, field2) => {\n if (typeof text2 !== "string" || text2.trim().length === 0 || text2.trim().length > max || /[\\r\\n\\u0000-\\u001f]/.test(text2)) {\n errors.push(`${field2}: must contain 1-${max} characters on one line.`);\n return void 0;\n }\n return text2.trim();\n };\n const text = value[key], field = path ? `${path}.${key}` : key;\n if (typeof text === "string") return check(text, field);\n const translations = oggetto(text);\n if (!translations || Object.keys(translations).length === 0) {\n errors.push(`${field}: must be a string or a non-empty language-to-text object.`);\n return void 0;\n }\n const result = {};\n for (const [raw, text2] of Object.entries(translations)) {\n const tag = normalizeLanguage(raw);\n if (!tag) {\n errors.push(`${field}.${raw}: must be a BCP 47 language tag.`);\n continue;\n }\n if (Object.hasOwn(result, tag)) errors.push(`${field}.${raw}: duplicate language.`);\n const checked = check(text2, `${field}.${raw}`);\n if (checked !== void 0) result[tag] = checked;\n }\n return result;\n}\nfunction validaManifest(valore) {\n const errori = [];\n const dati = oggetto(valore);\n if (dati === null) return { ok: false, errori: ["manifest: must be a JSON object."] };\n for (const campo of Object.keys(dati)) {\n if (!CAMPI.has(campo)) errori.push(`${campo}: unknown field.`);\n }\n if (dati.manifest === void 0) errori.push("manifest: is required and must be 1.");\n else if (dati.manifest !== 1) errori.push("manifest: must be exactly 1.");\n const id = stringaDefault(dati, "id", "", errori);\n if (dati.id === void 0) errori.push("id: is required.");\n else if (typeof dati.id === "string") {\n if (!isValidSlug(id)) {\n errori.push("id: must be 3-32 lowercase ASCII letters or digits with internal hyphens; historical UUID v4 slugs may be 36 characters.");\n } else if (isReservedSlug(id)) errori.push("id: this slug is reserved.");\n }\n const name = stringaDefault(dati, "name", "", errori);\n if (dati.name === void 0) errori.push("name: is required.");\n else if (typeof dati.name === "string" && (name.trim() === "" || name.length > 60)) {\n errori.push("name: must contain 1-60 characters.");\n }\n const description = dati.description === "" ? "" : testoFacoltativo(dati, "description", 500, "", errori) ?? "";\n const immagini = { cover: "", card: "", icon: "" };\n const usati = /* @__PURE__ */ new Set();\n for (const campo of ["cover", "card", "icon"]) {\n const path = dati[campo];\n if (path === void 0 || path === null) errori.push(`${campo}: is required.`);\n else if (typeof path !== "string" || !percorsoRelativo(path)) errori.push(`${campo}: must be a relative file path inside client/ without query, fragment, or parent segments.`);\n else {\n if (!/\\.(png|jpe?g|webp)$/i.test(path)) errori.push(`${campo}: must be a PNG, JPEG or WebP file.`);\n const canonical = decodeURIComponent(path);\n if (usati.has(canonical)) errori.push(`${campo}: each image must use a different file; cover, card and icon cannot share a path.`);\n usati.add(canonical);\n immagini[campo] = path;\n }\n }\n const { cover, card, icon } = immagini;\n const screenshots = [];\n if (dati.screenshots !== void 0) {\n if (!Array.isArray(dati.screenshots)) errori.push("screenshots: must be an array of relative file paths.");\n else {\n if (dati.screenshots.length > 8) errori.push("screenshots: must contain at most 8 paths.");\n for (const [indice, value] of dati.screenshots.entries()) {\n if (typeof value !== "string" || !percorsoRelativo(value)) {\n errori.push(`screenshots[${indice}]: must be a relative file path without query, fragment, or parent segments.`);\n } else screenshots.push(value);\n }\n }\n }\n const tags = [];\n if (dati.tags !== void 0) {\n if (!Array.isArray(dati.tags)) errori.push("tags: must be an array.");\n else {\n if (dati.tags.length > 10) errori.push("tags: must contain at most 10 tags.");\n for (const [indice, value] of dati.tags.entries()) {\n if (typeof value !== "string" || value.length > 24 || !TAG.test(value)) {\n errori.push(`tags[${indice}]: must be 1-24 lowercase letters, digits, or hyphens.`);\n } else tags.push(value);\n }\n }\n }\n const legacyLanguage = stringaDefault(dati, "language", "en", errori);\n if (!/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(legacyLanguage)) {\n errori.push("language: must be a BCP 47 language tag such as en, it, or pt-BR.");\n }\n const languages2 = [];\n if (!Array.isArray(dati.languages) || dati.languages.length === 0) {\n errori.push("languages: must be a non-empty array of BCP 47 language tags.");\n } else for (const [index, raw] of dati.languages.entries()) {\n const tag = normalizeLanguage(raw);\n if (!tag) errori.push(`languages[${index}]: must be a BCP 47 language tag.`);\n else if (languages2.includes(tag)) errori.push(`languages[${index}]: duplicate language ${tag}.`);\n else languages2.push(tag);\n }\n if (!languages2.includes("en")) errori.push("languages: English is always required alongside the game\'s own languages.");\n const language = languages2[0] ?? legacyLanguage;\n if (typeof description === "object") {\n for (const tag of Object.keys(description)) {\n if (!languages2.includes(tag)) errori.push(`description.${tag}: language must be declared in languages.`);\n }\n }\n if (dati.language !== void 0 && dati.languages !== void 0 && legacyLanguage.toLowerCase() !== language.toLowerCase()) {\n errori.push("language: must match the first entry in languages when both are present.");\n }\n let platform = "both";\n if (dati.platform === void 0) errori.push("platform: is required.");\n else if (typeof dati.platform !== "string" || !PLATFORM.has(dati.platform)) {\n errori.push("platform: must be desktop, mobile, or both.");\n } else platform = dati.platform;\n let orientation = "landscape";\n if (dati.orientation !== void 0) {\n if (typeof dati.orientation !== "string" || !ORIENTATION.has(dati.orientation)) {\n errori.push("orientation: must be landscape or portrait.");\n } else orientation = dati.orientation;\n }\n const input = [];\n if (dati.input !== void 0) {\n if (!Array.isArray(dati.input)) errori.push("input: must be an array.");\n else for (const [indice, value] of dati.input.entries()) {\n if (typeof value !== "string" || !INPUT.has(value)) {\n errori.push(`input[${indice}]: must be keyboard, mouse, touch, or gamepad.`);\n } else if (input.includes(value)) errori.push(`input[${indice}]: duplicate value ${value}.`);\n else input.push(value);\n }\n }\n let visibility = "public";\n if (dati.visibility !== void 0) {\n if (typeof dati.visibility !== "string" || !VISIBILITY.has(dati.visibility)) {\n errori.push("visibility: must be public or unlisted.");\n } else visibility = dati.visibility;\n }\n const network = [];\n if (dati.network !== void 0) {\n if (!Array.isArray(dati.network)) errori.push("network: must be an array of host names.");\n else for (const [indice, value] of dati.network.entries()) {\n if (typeof value !== "string" || !hostValido(value)) {\n errori.push(`network[${indice}]: must be a host name without scheme, port, path, query, or fragment.`);\n } else if (network.includes(value)) errori.push(`network[${indice}]: duplicate host ${value}.`);\n else network.push(value);\n }\n }\n if (dati.isolated !== void 0) {\n if (typeof dati.isolated !== "boolean") errori.push("isolated: must be a boolean.");\n }\n const requires = {\n webgl2: false,\n webgpu: false,\n wasm: false,\n threads: false,\n memoryMb: null,\n performance: "light"\n };\n if (dati.requires !== void 0) {\n const value = oggetto(dati.requires);\n if (value === null) errori.push("requires: must be an object.");\n else {\n for (const campo of Object.keys(value)) {\n if (!["webgl2", "webgpu", "wasm", "threads", "memoryMb", "performance"].includes(campo)) {\n errori.push(`requires.${campo}: unknown field.`);\n }\n }\n for (const campo of ["webgl2", "webgpu", "wasm", "threads"]) {\n if (value[campo] === void 0) continue;\n if (typeof value[campo] !== "boolean") errori.push(`requires.${campo}: must be a boolean.`);\n else requires[campo] = value[campo];\n }\n if (value.memoryMb !== void 0) {\n if (value.memoryMb !== null && (!interoTra(value.memoryMb, 512, 32768) || value.memoryMb % 256 !== 0)) {\n errori.push("requires.memoryMb: must be an integer from 512 to 32768 in steps of 256, or null.");\n } else requires.memoryMb = value.memoryMb;\n }\n if (value.performance !== void 0) {\n if (typeof value.performance !== "string" || !PERFORMANCE.has(value.performance)) {\n errori.push("requires.performance: must be light, medium, or heavy.");\n } else requires.performance = value.performance;\n }\n }\n }\n let players = { min: 1, max: 1 };\n if (dati.players !== void 0) {\n const value = oggetto(dati.players);\n if (value === null) errori.push("players: must be an object with min and max.");\n else {\n for (const campo of Object.keys(value)) {\n if (campo !== "min" && campo !== "max") errori.push(`players.${campo}: unknown field.`);\n }\n if (!interoTra(value.min, 1, TETTO_GIOCATORI)) errori.push(`players.min: must be an integer from 1 to ${TETTO_GIOCATORI}.`);\n if (!interoTra(value.max, 1, TETTO_GIOCATORI)) errori.push(`players.max: must be an integer from 1 to ${TETTO_GIOCATORI} in manifest version 1.`);\n if (interoTra(value.min, 1, TETTO_GIOCATORI) && interoTra(value.max, 1, TETTO_GIOCATORI)) {\n if (value.min > value.max) errori.push("players.max: must be greater than or equal to players.min.");\n else players = { min: value.min, max: value.max };\n }\n }\n }\n let lobby = false;\n if (dati.lobby !== void 0) {\n if (typeof dati.lobby !== "boolean") errori.push("lobby: must be a boolean.");\n else lobby = dati.lobby;\n }\n let persistent = false;\n if (dati.persistent !== void 0) {\n if (typeof dati.persistent !== "boolean") errori.push("persistent: must be a boolean.");\n else persistent = dati.persistent;\n }\n const replays = dati.replays === true;\n if (dati.replays !== void 0 && typeof dati.replays !== "boolean") errori.push("replays: must be a boolean.");\n let spectators = { delayMs: RITARDO_SPETTATORI_MS };\n if (dati.spectators === false || dati.spectators === null) spectators = null;\n else if (dati.spectators !== void 0 && dati.spectators !== true) {\n const value = oggetto(dati.spectators);\n if (value === null) {\n errori.push("spectators: must be a boolean or an object with delayMs.");\n } else {\n for (const campo of Object.keys(value)) {\n if (campo !== "delayMs") errori.push(`spectators.${campo}: unknown field.`);\n }\n if (!interoTra(value.delayMs, 0, 3e4)) {\n errori.push("spectators.delayMs: must be an integer from 0 to 30000.");\n } else spectators = { delayMs: value.delayMs };\n }\n }\n let overlay = null;\n if (dati.overlay !== void 0 && dati.overlay !== null) {\n const value = oggetto(dati.overlay);\n if (value === null) errori.push("overlay: must be an object or null.");\n else {\n for (const key of Object.keys(value)) if (!["version", "accent"].includes(key)) errori.push(`overlay.${key}: unknown field.`);\n if (value.version !== 1) errori.push("overlay.version: must be exactly 1.");\n if (value.accent !== void 0 && (typeof value.accent !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value.accent))) {\n errori.push("overlay.accent: must be a six-digit hexadecimal color, such as #336699.");\n }\n overlay = { version: 1, ...typeof value.accent === "string" ? { accent: value.accent } : {} };\n }\n }\n const boards = {};\n if (dati.boards !== void 0) {\n const value = oggetto(dati.boards);\n if (value === null) errori.push("boards: must be an object of board ids.");\n else {\n if (Object.keys(value).length > MASSIMO_CLASSIFICHE) {\n errori.push(`boards: at most ${MASSIMO_CLASSIFICHE} boards.`);\n }\n for (const [id2, raw] of Object.entries(value)) {\n let valido = true;\n if (!ID_CLASSIFICA.test(id2)) {\n errori.push(`boards.${id2}: invalid board id.`);\n valido = false;\n }\n const board = oggetto(raw);\n if (board === null) {\n errori.push(`boards.${id2}.source: must be "client" or "server".`);\n continue;\n }\n for (const campo of Object.keys(board)) {\n if (!["source", "label", "periods", "day"].includes(campo)) errori.push(`boards.${id2}.${campo}: unknown field.`);\n }\n if (board.source !== "client" && board.source !== "server") {\n errori.push(`boards.${id2}.source: must be "client" or "server".`);\n valido = false;\n }\n if (board.day !== void 0 && board.day !== "submit" && board.day !== "start") errori.push(`boards.${id2}.day: must be "submit" or "start".`);\n if (board.day === "start" && board.source !== "server") errori.push(`boards.${id2}.day: start requires source "server".`);\n const label = testoFacoltativo(board, "label", 48, `boards.${id2}`, errori);\n let periods = ["all-time"];\n if (board.periods !== void 0) {\n if (!Array.isArray(board.periods) || board.periods.length < 1 || board.periods.length > 2 || board.periods.some((period) => period !== "daily" && period !== "all-time") || new Set(board.periods).size !== board.periods.length) {\n errori.push(`boards.${id2}.periods: must contain daily, all-time, or both without duplicates.`);\n } else periods = [...board.periods];\n }\n if (valido) Object.defineProperty(boards, id2, { value: {\n source: board.source,\n periods,\n ...board.day === void 0 ? {} : { day: board.day },\n ...label === void 0 ? {} : { label }\n }, enumerable: true, configurable: true, writable: true });\n }\n }\n }\n const roles = [];\n if (dati.roles !== void 0) {\n if (!Array.isArray(dati.roles)) errori.push("roles: must be an array.");\n else {\n const ids = /* @__PURE__ */ new Set();\n for (const [indice, raw] of dati.roles.entries()) {\n const value = oggetto(raw);\n if (value === null) {\n errori.push(`roles[${indice}]: must be an object.`);\n continue;\n }\n for (const campo of Object.keys(value)) {\n if (!["id", "min", "max", "label"].includes(campo)) errori.push(`roles[${indice}].${campo}: unknown field.`);\n }\n const idRuolo = value.id;\n const min = value.min;\n const max = value.max;\n let valido = true;\n if (typeof idRuolo !== "string" || idRuolo.length > 32 || !ID_INTERNO.test(idRuolo)) {\n errori.push(`roles[${indice}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`);\n valido = false;\n } else if (ids.has(idRuolo)) {\n errori.push(`roles[${indice}].id: duplicate role ${idRuolo}.`);\n valido = false;\n } else ids.add(idRuolo);\n if (!interoTra(min, 0, TETTO_GIOCATORI)) {\n errori.push(`roles[${indice}].min: must be an integer from 0 to ${TETTO_GIOCATORI}.`);\n valido = false;\n }\n if (max !== void 0 && !interoTra(max, 0, TETTO_GIOCATORI)) {\n errori.push(`roles[${indice}].max: must be an integer from 0 to ${TETTO_GIOCATORI} when present.`);\n valido = false;\n }\n if (typeof min === "number" && typeof max === "number" && min > max) {\n errori.push(`roles[${indice}].max: must be greater than or equal to min.`);\n valido = false;\n }\n const label = testoFacoltativo(value, "label", 32, `roles[${indice}]`, errori);\n if (valido) roles.push({\n id: idRuolo,\n min,\n ...max === void 0 ? {} : { max },\n ...label === void 0 ? {} : { label }\n });\n }\n }\n }\n let teams = null;\n if (dati.teams !== void 0 && dati.teams !== null) {\n const value = oggetto(dati.teams);\n if (value === null) errori.push("teams: must be null or an object with min and max.");\n else {\n for (const campo of Object.keys(value)) {\n if (campo !== "min" && campo !== "max") errori.push(`teams.${campo}: unknown field.`);\n }\n if (!interoTra(value.min, 2, TETTO_GIOCATORI)) errori.push(`teams.min: must be an integer from 2 to ${TETTO_GIOCATORI}.`);\n if (!interoTra(value.max, 2, TETTO_GIOCATORI)) errori.push(`teams.max: must be an integer from 2 to ${TETTO_GIOCATORI}.`);\n if (interoTra(value.min, 2, TETTO_GIOCATORI) && interoTra(value.max, 2, TETTO_GIOCATORI)) {\n if (value.min > value.max) errori.push("teams.max: must be greater than or equal to teams.min.");\n else teams = { min: value.min, max: value.max };\n }\n }\n }\n let voice = "none";\n if (dati.voice !== void 0) {\n if (typeof dati.voice !== "string" || !VOICE.has(dati.voice)) {\n errori.push("voice: must be none, room, team, or proximity.");\n } else voice = dati.voice;\n }\n const modes = [];\n if (dati.modes !== void 0) {\n if (!Array.isArray(dati.modes)) errori.push("modes: must be an array.");\n else {\n const ids = /* @__PURE__ */ new Set();\n for (const [indice, raw] of dati.modes.entries()) {\n const value = oggetto(raw);\n if (value === null) {\n errori.push(`modes[${indice}]: must be an object.`);\n continue;\n }\n for (const campo of Object.keys(value)) {\n if (!["id", "players", "lobby", "matchmaking", "execution", "label", "instructions"].includes(campo)) errori.push(`modes[${indice}].${campo}: unknown field.`);\n }\n if (typeof value.id !== "string" || value.id.length > 32 || !ID_INTERNO.test(value.id)) {\n errori.push(`modes[${indice}].id: must be 1-32 lowercase letters, digits, or internal hyphens.`);\n continue;\n }\n if (ids.has(value.id)) {\n errori.push(`modes[${indice}].id: duplicate mode ${value.id}.`);\n continue;\n }\n ids.add(value.id);\n const modo = { id: value.id };\n for (const [key2, max] of [["label", 48], ["instructions", 160]]) {\n const text = testoFacoltativo(value, key2, max, `modes[${indice}]`, errori);\n if (text !== void 0) modo[key2] = text;\n }\n if (value.execution !== void 0) {\n if (value.execution !== "local" && value.execution !== "room") errori.push(`modes[${indice}].execution: must be local or room.`);\n else modo.execution = value.execution;\n }\n if (overlay !== null && modo.execution === void 0) errori.push(`modes[${indice}].execution: is required with the standard overlay.`);\n if (value.players !== void 0) {\n const campo = `modes[${indice}].players`;\n const range = oggetto(value.players);\n if (range === null) errori.push(`${campo}: must be an object with min and max.`);\n else {\n for (const key2 of Object.keys(range)) {\n if (key2 !== "min" && key2 !== "max") errori.push(`${campo}.${key2}: unknown field.`);\n }\n if (!interoTra(range.min, 1, TETTO_GIOCATORI)) errori.push(`${campo}.min: must be an integer from 1 to ${TETTO_GIOCATORI}.`);\n if (!interoTra(range.max, 1, TETTO_GIOCATORI)) errori.push(`${campo}.max: must be an integer from 1 to ${TETTO_GIOCATORI}.`);\n if (interoTra(range.min, 1, TETTO_GIOCATORI) && interoTra(range.max, 1, TETTO_GIOCATORI)) {\n if (range.min > range.max) errori.push(`${campo}.max: must be greater than or equal to min.`);\n else modo.players = { min: range.min, max: range.max };\n }\n }\n }\n if (value.lobby !== void 0) {\n if (typeof value.lobby !== "boolean") errori.push(`modes[${indice}].lobby: must be a boolean.`);\n else modo.lobby = value.lobby;\n }\n if (modo.execution === "local") {\n const range = modo.players ?? players;\n if (range.min !== 1 || range.max !== 1) errori.push(`modes[${indice}].players: local execution requires min and max to be 1.`);\n if (modo.lobby ?? lobby) errori.push(`modes[${indice}].lobby: local execution requires false.`);\n if (value.matchmaking !== void 0) errori.push(`modes[${indice}].matchmaking: local execution cannot use matchmaking.`);\n }\n if (value.matchmaking === void 0) {\n modes.push(modo);\n continue;\n }\n const matchmaking = oggetto(value.matchmaking);\n if (matchmaking === null) {\n errori.push(`modes[${indice}].matchmaking: must be an object.`);\n continue;\n }\n for (const campo of Object.keys(matchmaking)) {\n if (!["key", "timeoutMs", "defaults"].includes(campo)) {\n errori.push(`modes[${indice}].matchmaking.${campo}: unknown field.`);\n }\n }\n let valido = true;\n const key = [];\n if (!Array.isArray(matchmaking.key) || matchmaking.key.length < 1 || matchmaking.key.length > 8) {\n errori.push(`modes[${indice}].matchmaking.key: must contain from 1 to 8 fields.`);\n valido = false;\n } else for (const [keyIndice, item] of matchmaking.key.entries()) {\n if (typeof item !== "string" || !CAMPO_MATCHMAKING.test(item)) {\n errori.push(`modes[${indice}].matchmaking.key[${keyIndice}]: must be 1-32 lowercase letters, digits, or hyphens and start with a letter or digit.`);\n valido = false;\n } else if (key.includes(item)) {\n errori.push(`modes[${indice}].matchmaking.key[${keyIndice}]: duplicate field ${item}.`);\n valido = false;\n } else key.push(item);\n }\n if (!interoTra(matchmaking.timeoutMs, 1e3, 3e5)) {\n errori.push(`modes[${indice}].matchmaking.timeoutMs: must be an integer from 1000 to 300000.`);\n valido = false;\n }\n let defaults;\n if (matchmaking.defaults !== void 0) {\n const values = oggetto(matchmaking.defaults);\n if (values === null || Object.keys(values).length !== key.length || key.some((field) => !Object.hasOwn(values, field))) {\n errori.push(`modes[${indice}].matchmaking.defaults: must contain exactly the declared key fields.`);\n } else {\n defaults = {};\n for (const [field, value2] of Object.entries(values)) {\n if (!(typeof value2 === "string" && value2.length >= 1 && value2.length <= 64 && /^[A-Za-z0-9_.:-]+$/.test(value2)) && !Number.isSafeInteger(value2)) {\n errori.push(`modes[${indice}].matchmaking.defaults.${field}: must be a string of 1-64 characters or a safe integer.`);\n } else Object.defineProperty(defaults, field, { value: value2, enumerable: true });\n }\n }\n }\n if (valido) modes.push({ ...modo, matchmaking: {\n ...defaults === void 0 ? {} : { defaults },\n key,\n timeoutMs: matchmaking.timeoutMs\n } });\n }\n }\n }\n if (overlay !== null && modes.length === 0) errori.push("modes: at least one explicit mode is required with the standard overlay.");\n if (errori.length > 0) return { ok: false, errori };\n return { ok: true, manifest: {\n manifest: 1,\n overlay,\n id,\n name,\n description,\n cover,\n card,\n icon,\n screenshots,\n tags,\n languages: languages2,\n language,\n platform,\n orientation,\n input,\n visibility,\n network,\n requires,\n players,\n lobby,\n persistent,\n replays,\n spectators,\n boards,\n roles,\n teams,\n voice,\n modes\n } };\n}\n\n// ../contracts/src/replay.ts\nvar REPLAY_MAX_BYTES = 10 * 1024 * 1024;\nvar REPLAY_MAX_DURATION_MS = 30 * 60 * 1e3;\nvar REPLAY_CHUNK_BYTES = 512 * 1024;\nvar REPLAY_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;\nvar REPLAY_ID = /^[A-Za-z0-9_-]{22}$/;\n\n// ../contracts/src/overlay.ts\nvar OVERLAY_PANELS = ["home", "room", "invite", "friends", "voice", "boards"];\nfunction overlayConfiguration(manifest, coverUrl = null, invite = null, iconUrl = null) {\n const validated = validaManifest(manifest);\n if (!validated.ok) throw new Error("The overlay manifest is invalid.");\n return { manifest: validated.manifest, coverUrl, iconUrl, invite };\n}\nfunction record(value) {\n return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;\n}\nfunction validSafeArea(value) {\n const area = record(value);\n return area !== null && Object.keys(area).length === 4 && ["top", "right", "bottom", "left"].every((key) => typeof area[key] === "number" && Number.isFinite(area[key]) && Number(area[key]) >= 0 && Number(area[key]) <= 1e5);\n}\nfunction validOverlayView(value) {\n const data = record(value);\n return data !== null && Object.keys(data).every((key) => ["inputBlocked", "reservedRects", "safeArea", "shortcutEnabled"].includes(key)) && (data.safeArea === void 0 || validSafeArea(data.safeArea)) && (data.shortcutEnabled === void 0 || typeof data.shortcutEnabled === "boolean") && typeof data.inputBlocked === "boolean" && Array.isArray(data.reservedRects) && data.reservedRects.length <= 8 && data.reservedRects.every((value2) => {\n const rect = record(value2);\n return rect !== null && Object.keys(rect).length === 4 && ["x", "y", "width", "height"].every((key) => typeof rect[key] === "number" && Number.isFinite(rect[key]) && rect[key] >= 0 && rect[key] <= 1e5);\n });\n}\nfunction validOverlayRequest(value) {\n const message = record(value), args = record(message?.args);\n if (message?.type !== "caisual:overlay" || message.v !== 1 || typeof message.epoch !== "string" || message.epoch.length < 1 || message.epoch.length > 128 || typeof message.requestId !== "string" || !(/^[1-9][0-9]{0,15}$/.test(message.requestId) && Number.isSafeInteger(Number(message.requestId))) || args === null) return false;\n if (Object.keys(message).some((key) => !["type", "v", "epoch", "requestId", "sessionId", "op", "args"].includes(key)) || !(message.sessionId === void 0 || message.sessionId === null || typeof message.sessionId === "string" && /^[1-9][0-9]{0,15}$/.test(message.sessionId))) return false;\n const keys = (...allowed) => Object.keys(args).every((key) => allowed.includes(key));\n const text = (key) => typeof args[key] === "string" && args[key].length >= 1 && args[key].length <= 64;\n switch (message.op) {\n case "replay.play":\n case "replay.pause":\n return keys() && typeof message.sessionId === "string";\n case "replay.seek":\n return keys("positionMs") && typeof message.sessionId === "string" && typeof args.positionMs === "number" && Number.isFinite(args.positionMs) && args.positionMs >= 0 && args.positionMs <= REPLAY_MAX_DURATION_MS;\n case "replay.speed":\n return keys("speed") && typeof message.sessionId === "string" && [0.5, 1, 2, 4].includes(Number(args.speed)) && typeof args.speed === "number";\n case "local.start":\n return keys("mode") && text("mode");\n case "room.create":\n return keys("mode") && (args.mode === null || text("mode"));\n case "room.join":\n return keys("code") && (args.code === void 0 || text("code"));\n case "room.watch":\n return keys("code") && text("code");\n case "room.match": {\n const key = record(args.key);\n return keys("mode", "key") && text("mode") && (args.key === void 0 || key !== null && Object.keys(key).length <= 8 && Object.values(key).every((v) => typeof v === "string" && v.length >= 1 && v.length <= 64 || typeof v === "number" && Number.isSafeInteger(v)));\n }\n case "room.ready":\n return keys("ready") && typeof args.ready === "boolean";\n case "room.role":\n case "room.requestRole":\n return keys("role") && text("role");\n case "room.team":\n return keys("team") && Number.isInteger(args.team) && args.team >= 1 && args.team <= 24;\n case "room.restart":\n case "room.start":\n case "session.cancel":\n case "session.leave":\n case "session.disconnect":\n case "session.resume":\n return keys();\n case "voice.join":\n case "voice.leave":\n return keys() && typeof message.sessionId === "string";\n case "voice.mute":\n return keys("muted") && typeof args.muted === "boolean" && typeof message.sessionId === "string";\n case "voice.setVolume":\n return keys("playerId", "volume") && typeof message.sessionId === "string" && typeof args.playerId === "string" && args.playerId.length > 0 && args.playerId.length <= 128 && typeof args.volume === "number" && Number.isFinite(args.volume) && args.volume >= 0 && args.volume <= 1;\n case "overlay.view":\n return validOverlayView(args);\n default:\n return false;\n }\n}\nfunction validBoardDay(value) {\n if (typeof value !== "string" || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false;\n const at = Date.parse(`${value}T00:00:00Z`);\n return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;\n}\nfunction validOverlaySessionState(value) {\n const data = record(value);\n const exact = (v, keys) => v !== null && Object.keys(v).length === keys.length && Object.keys(v).every((key) => keys.includes(key));\n const text = (v) => typeof v === "string" && v.length <= 128;\n const nullable = (v) => v === null || text(v);\n const finite = (v) => typeof v === "number" && Number.isFinite(v);\n if (!data || !exact(data, ["kind", "id", "mode", "localStatus", "ready", "capabilities", "room", "waiting", "resume", "resumeError", ..."voice" in data ? ["voice"] : []])) return false;\n if (data.voice !== void 0 && data.voice !== null && (data.kind !== "room" || !record(data.room) || !validOverlayVoice(data.voice))) return false;\n const capabilities = record(data.capabilities), room = record(data.room), waiting = record(data.waiting), resume = record(data.resume);\n if (!["boot", "home", "attaching", "matching", "local", "room", "watch"].includes(String(data.kind)) || !nullable(data.id) || !nullable(data.mode) || ![null, "playing", "ended"].includes(data.localStatus) || typeof data.ready !== "boolean" || typeof data.resumeError !== "boolean" || !exact(capabilities, ["local", "rooms", "overlay", "requestRole"]) || !Object.values(capabilities).every((v) => typeof v === "boolean")) return false;\n if (data.waiting !== null && (!exact(waiting, ["players", "min", "max"]) || !Object.values(waiting).every((v) => Number.isInteger(v) && Number(v) >= 0 && Number(v) <= 24))) return false;\n if (data.resume !== null && (!exact(resume, ["version", "code", "mode", "updatedAt"]) || resume.version !== 1 || !text(resume.code) || !nullable(resume.mode) || !finite(resume.updatedAt))) return false;\n if (data.room === null) return true;\n if (!exact(room, ["code", "mode", "status", "host", "you", "players", "countdownAt", "connection", "closedCode", "limits", "lobby", "persistent", "delayMs", "requestRole", ..."replay" in (room ?? {}) ? ["replay"] : [], ..."replayId" in (room ?? {}) ? ["replayId"] : [], ..."result" in (room ?? {}) ? ["result"] : [], ..."rematch" in (room ?? {}) ? ["rematch"] : []]) || !room) return false;\n if (room.replayId !== void 0 && (typeof room.replayId !== "string" || !REPLAY_ID.test(room.replayId))) return false;\n if (room.replay !== void 0) {\n const playback = record(room.replay);\n if (data.kind !== "watch" || !exact(playback, ["positionMs", "durationMs", "paused", "speed", "truncated"]) || !playback || !finite(playback.positionMs) || !finite(playback.durationMs) || Number(playback.positionMs) < 0 || Number(playback.positionMs) > Number(playback.durationMs) || Number(playback.durationMs) > REPLAY_MAX_DURATION_MS || ![0.5, 1, 2, 4].includes(Number(playback.speed)) || typeof playback.speed !== "number" || typeof playback.paused !== "boolean" || typeof playback.truncated !== "boolean") return false;\n }\n const limits = record(room.limits), rematch = record(room.rematch);\n if (room.rematch !== void 0 && room.rematch !== null && (!exact(rematch, ["keepSetup", "autoStart"]) || typeof rematch.keepSetup !== "boolean" || typeof rematch.autoStart !== "boolean")) return false;\n return text(room.code) && nullable(room.mode) && nullable(room.host) && nullable(room.you) && ["lobby", "countdown", "playing", "finished", "ended"].includes(String(room.status)) && ["connecting", "connected", "reconnecting", "disconnected", "ended", "closed", "replaced"].includes(String(room.connection)) && ["countdownAt", "closedCode", "delayMs"].every((key) => room[key] === null || finite(room[key])) && ["lobby", "persistent", "requestRole"].every((key) => typeof room[key] === "boolean") && exact(limits, ["min", "max"]) && Object.values(limits).every((v) => Number.isInteger(v) && Number(v) >= 1 && Number(v) <= 24) && Array.isArray(room.players) && room.players.length <= 24 && room.players.every((value2) => {\n const player = record(value2);\n return exact(player, ["id", "name", "guest", "role", "team", "ready", "connected"]) && player !== null && text(player.id) && text(player.name) && nullable(player.role) && (player.team === null || Number.isInteger(player.team) && Number(player.team) >= 1 && Number(player.team) <= 24) && ["guest", "ready", "connected"].every((key) => typeof player[key] === "boolean");\n });\n}\nfunction validOverlayVoice(value) {\n const voice = record(value);\n if (!voice || Object.keys(voice).length !== 6 || !["mode", "state", "mic", "muted", "speaking", "peers"].every((key) => key in voice) || !["room", "team", "proximity"].includes(String(voice.mode)) || !["off", "joining", "on", "reconnecting"].includes(String(voice.state)) || !["mic", "muted", "speaking"].every((key) => typeof voice[key] === "boolean") || !Array.isArray(voice.peers) || voice.peers.length > 24) return false;\n const ids = /* @__PURE__ */ new Set();\n return voice.peers.every((value2) => {\n const peer = record(value2);\n if (!peer || Object.keys(peer).length !== 5 || !["id", "mic", "muted", "speaking", "volume"].every((key) => key in peer) || typeof peer.id !== "string" || !peer.id.length || peer.id.length > 128 || ids.has(peer.id) || !["mic", "muted", "speaking"].every((key) => typeof peer[key] === "boolean") || typeof peer.volume !== "number" || !Number.isFinite(peer.volume) || peer.volume < 0 || peer.volume > 1) return false;\n ids.add(peer.id);\n return true;\n });\n}\n\n// ../contracts/src/room-limits.ts\nvar MASSIMO_BYTE_FRAME_STANZA = 64 * 1024;\n\n// ../contracts/src/match-result.ts\nfunction readMatchResult(value, playerIds) {\n const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;\n const result = object(value);\n if (!result || !Array.isArray(result.standings)) return null;\n const known = new Set(playerIds), seen = /* @__PURE__ */ new Set();\n const standings = [];\n for (const item of result.standings) {\n const row = object(item);\n if (!row || typeof row.playerId !== "string" || !known.has(row.playerId) || seen.has(row.playerId)) continue;\n seen.add(row.playerId);\n standings.push({\n playerId: row.playerId,\n ...typeof row.score === "number" && Number.isFinite(row.score) ? { score: row.score } : {},\n ...typeof row.rank === "number" && Number.isSafeInteger(row.rank) && row.rank > 0 ? { rank: row.rank } : {}\n });\n }\n if (!standings.length) return null;\n return {\n standings,\n ...Array.isArray(result.winners) ? { winners: [...new Set(result.winners.filter((id) => typeof id === "string" && seen.has(id)))] } : {},\n ...typeof result.draw === "boolean" ? { draw: result.draw } : {},\n ...typeof result.unit === "string" ? { unit: result.unit } : {}\n };\n}\n\n// src/errors.ts\nfunction creaErrore(code, message, version = {}) {\n return Object.assign(new Error(message), { name: "CaisualError", code, ...version });\n}\n\n// src/overlay/host-bridge.ts\nfunction eMessaggioReady(value) {\n return record(value)?.type === "caisual:ready";\n}\nfunction eRichiestaBiglietto(value) {\n const data = record(value);\n return data?.type === "caisual:ticket" && (data.aud === void 0 || data.aud === "portal" || data.aud === "live");\n}\nfunction stanzaDaMessaggio(value) {\n const data = record(value);\n if (data?.type !== "caisual:room") return void 0;\n if (data.room === null) return null;\n const room = record(data.room);\n return typeof room?.code === "string" && /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(room.code) ? { code: room.code } : void 0;\n}\nfunction creaPonteOspite(input) {\n let port = null, epoch = null, instance = null;\n let disposed = false, legacyReady = true, sequence = 0, requestId = 0;\n let state = null, clockOffset = null;\n let polling = null, pollingEnd = null;\n const pending = /* @__PURE__ */ new Map();\n const states = /* @__PURE__ */ new Set();\n const shortcuts = /* @__PURE__ */ new Set();\n const opens = /* @__PURE__ */ new Set();\n const errors = /* @__PURE__ */ new Set();\n const scores = /* @__PURE__ */ new Set();\n const notify = (listeners, value) => {\n for (const listener of listeners) try {\n listener(value);\n } catch {\n }\n };\n const rejectPending = () => {\n for (const value of pending.values()) {\n input.finestra.clearTimeout(value.timer);\n value.reject(creaErrore("session_replaced", "The game document changed."));\n }\n pending.clear();\n };\n const stopPolling = () => {\n if (polling !== null) input.finestra.clearInterval(polling);\n if (pollingEnd !== null) input.finestra.clearTimeout(pollingEnd);\n polling = pollingEnd = null;\n };\n const askReady = () => {\n if (!disposed && input.frame.src !== "") input.frame.contentWindow?.postMessage({ type: "caisual:ready?" }, input.origineGioco);\n };\n const poll = () => {\n stopPolling();\n polling = input.finestra.setInterval(askReady, 500);\n pollingEnd = input.finestra.setTimeout(stopPolling, 1e4);\n askReady();\n };\n const loaded = () => {\n legacyReady = true;\n poll();\n };\n const listen = (event) => {\n if (disposed || event.origin !== input.origineGioco || event.source !== input.frame.contentWindow || !eMessaggioReady(event.data)) return;\n const data = record(event.data);\n const nextInstance = typeof data.instance === "string" && data.instance.length <= 128 ? data.instance : null;\n if (port && (nextInstance !== null ? nextInstance === instance : !legacyReady)) return;\n stopPolling();\n legacyReady = false;\n instance = nextInstance;\n rejectPending();\n port?.close();\n input.onRoom(null);\n epoch = input.epoch?.() ?? crypto.randomUUID();\n sequence = requestId = 0;\n state = null;\n clockOffset = null;\n notify(states, null);\n const channel = input.creaCanale?.() ?? new MessageChannel();\n const currentPort = channel.port1, currentEpoch = epoch;\n port = currentPort;\n const current = () => !disposed && port === currentPort && epoch === currentEpoch;\n currentPort.onmessage = (event2) => {\n if (!current()) return;\n const data2 = record(event2.data);\n if (eRichiestaBiglietto(data2)) {\n const aud = data2?.aud === "live" ? "live" : "portal";\n void input.rinnova(aud).then((ticket) => {\n if (current()) currentPort.postMessage({ type: "caisual:ticket", aud, ticket });\n }).catch(() => {\n if (current()) currentPort.postMessage({ type: "caisual:ticket", aud, error: "offline" });\n });\n return;\n }\n if (data2?.type === "caisual:reload") {\n const target = record(data2.target);\n const code = typeof target?.code === "string" && /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(target.code) ? target.code : void 0;\n const roomId = typeof target?.roomId === "string" && /^g[1-9][0-9]*-[1-9][0-9]*\\.[a-z0-9]{16}$/.test(target.roomId) ? target.roomId : void 0;\n input.reload?.(code || roomId ? { code, roomId, watch: target?.watch === true } : void 0);\n return;\n }\n const room = stanzaDaMessaggio(data2);\n if (room !== void 0) {\n input.onRoom(room);\n return;\n }\n if (data2?.v !== 1 || data2.epoch !== currentEpoch) return;\n if (data2.type === "caisual:overlay-response" && typeof data2.requestId === "string") {\n const item = pending.get(data2.requestId);\n if (!item) return;\n if (data2.ok !== true && (data2.ok !== false || typeof record(data2.error)?.code !== "string" || typeof record(data2.error)?.message !== "string")) return;\n pending.delete(data2.requestId);\n input.finestra.clearTimeout(item.timer);\n const response = data2;\n if (response.ok) item.resolve();\n else item.reject(creaErrore(response.error.code, response.error.message));\n } else if (data2.type === "caisual:overlay-state" && Number.isSafeInteger(data2.seq) && data2.seq > sequence) {\n if (!validOverlaySessionState(data2.state) || typeof data2.serverTime !== "number" || !Number.isFinite(data2.serverTime)) return;\n clockOffset = data2.serverTime - Date.now();\n sequence = data2.seq;\n state = structuredClone(data2.state);\n notify(states, state);\n } else if (data2.type === "caisual:overlay-error" && data2.sessionId === state?.id) {\n const error = record(data2.error);\n if (typeof error?.code === "string" && typeof error.message === "string") notify(errors, { sessionId: data2.sessionId, error: { code: error.code, message: error.message } });\n } else if (data2.type === "caisual:overlay-shortcut") {\n notify(shortcuts, void 0);\n } else if (data2.type === "caisual:overlay-open" && OVERLAY_PANELS.includes(data2.panel)) {\n notify(opens, data2.panel);\n } else if (data2.type === "caisual:overlay-score") {\n const score = record(data2.score);\n if (score && typeof score.board === "string" && typeof score.player === "string" && Number.isSafeInteger(score.score) && Number.isFinite(score.submittedAt) && (score.day === null || typeof score.day === "string")) {\n notify(scores, { board: score.board, player: score.player, score: score.score, day: score.day, submittedAt: score.submittedAt });\n }\n }\n };\n currentPort.start();\n input.frame.contentWindow?.postMessage({\n type: "caisual:hello",\n replay: input.replay ?? null,\n n: input.n,\n ticket: input.ticket,\n live: input.origineLive,\n invite: input.invite,\n // language resta disponibile ai kit pubblicati prima della separazione delle lingue.\n ...input.language ? { language: input.language, uiLanguage: input.language } : {},\n ...input.languagePreferences ? { languagePreferences: input.languagePreferences } : {},\n ...input.configuration ? { gameLanguages: manifestLanguages(input.configuration.manifest) } : {},\n ...input.configuration && data.overlayVersion === 1 ? { overlay: { v: 1, epoch, configuration: input.configuration } } : {}\n }, input.origineGioco, [channel.port2]);\n };\n input.finestra.addEventListener("message", listen);\n input.frame.addEventListener?.("load", loaded);\n poll();\n return {\n get replay() {\n return input.replay ?? null;\n },\n get watch() {\n return input.watch === true;\n },\n reload() {\n input.reload?.();\n },\n get epoch() {\n return epoch;\n },\n serverTime() {\n return clockOffset === null ? null : Date.now() + clockOffset;\n },\n get state() {\n return state === null ? null : structuredClone(state);\n },\n subscribe(listener) {\n states.add(listener);\n listener(state);\n return () => {\n states.delete(listener);\n };\n },\n onShortcut(listener) {\n shortcuts.add(listener);\n return () => {\n shortcuts.delete(listener);\n };\n },\n onOpen(listener) {\n opens.add(listener);\n return () => {\n opens.delete(listener);\n };\n },\n onError(listener) {\n errors.add(listener);\n return () => {\n errors.delete(listener);\n };\n },\n onScore(listener) {\n scores.add(listener);\n return () => {\n scores.delete(listener);\n };\n },\n request(op, args) {\n if (!port || !epoch || disposed) return Promise.reject(creaErrore("offline", "The game bridge is not connected."));\n if (pending.size >= 32) return Promise.reject(creaErrore("rate_limited", "Too many overlay requests."));\n const id = String(++requestId), request = {\n type: "caisual:overlay",\n v: 1,\n epoch,\n requestId: id,\n op,\n args,\n ...["replay.play", "replay.pause", "replay.seek", "replay.speed", "room.ready", "room.role", "room.requestRole", "room.team", "room.start", "room.restart", "session.leave", "session.disconnect", "voice.join", "voice.mute", "voice.leave", "voice.setVolume"].includes(op) ? { sessionId: state?.id ?? null } : {}\n };\n if (!validOverlayRequest(request)) return Promise.reject(creaErrore("invalid_request", "The overlay request is invalid."));\n return new Promise((resolve, reject) => {\n const timeout = op === "room.match" ? 31e4 : input.requestTimeoutMs ?? 15e3;\n const timer = input.finestra.setTimeout(() => {\n pending.delete(id);\n reject(creaErrore("timeout", "The overlay request timed out."));\n }, timeout);\n pending.set(id, { resolve, reject, timer });\n try {\n port.postMessage(request);\n } catch (error) {\n input.finestra.clearTimeout(timer);\n pending.delete(id);\n reject(error);\n }\n });\n },\n dispose() {\n disposed = true;\n stopPolling();\n rejectPending();\n port?.close();\n port = null;\n input.finestra.removeEventListener("message", listen);\n input.frame.removeEventListener?.("load", loaded);\n states.clear();\n opens.clear();\n shortcuts.clear();\n scores.clear();\n errors.clear();\n }\n };\n}\nfunction avviaHandshake(input) {\n const bridge = creaPonteOspite(input);\n return () => bridge.dispose();\n}\nfunction gameViewport(frame) {\n const rect = frame.getBoundingClientRect();\n const zoomX = frame.offsetWidth ? rect.width / frame.offsetWidth : 1;\n const zoomY = frame.offsetHeight ? rect.height / frame.offsetHeight : 1;\n const left = rect.left + frame.clientLeft * zoomX, top = rect.top + frame.clientTop * zoomY;\n return {\n left,\n top,\n right: left + frame.clientWidth * zoomX,\n bottom: top + frame.clientHeight * zoomY,\n scaleX: zoomX ? 1 / zoomX : 1,\n scaleY: zoomY ? 1 / zoomY : 1\n };\n}\nfunction measureSafeArea(frame, probe) {\n const win = frame.ownerDocument.defaultView, css = win.getComputedStyle(probe), viewport = gameViewport(frame);\n const clamp = (value, max) => Math.max(0, Math.min(max, value));\n return {\n top: clamp(((parseFloat(css.paddingTop) || 0) - viewport.top) * viewport.scaleY, frame.clientHeight),\n right: clamp((viewport.right - (win.innerWidth - (parseFloat(css.paddingRight) || 0))) * viewport.scaleX, frame.clientWidth),\n bottom: clamp((viewport.bottom - (win.innerHeight - (parseFloat(css.paddingBottom) || 0))) * viewport.scaleY, frame.clientHeight),\n left: clamp(((parseFloat(css.paddingLeft) || 0) - viewport.left) * viewport.scaleX, frame.clientWidth)\n };\n}\n\n// src/overlay/boards.ts\nfunction createBoardController(input) {\n let disposed = false, generation = 0, timer;\n const seen = /* @__PURE__ */ new Set();\n let query = null, data = null, error = false, loading = false;\n let queued = null, saving = null, reads = 0;\n const later = input.later ?? setTimeout, clear = input.clear ?? clearTimeout;\n const cancel = () => {\n if (timer !== void 0) clear(timer);\n timer = void 0;\n };\n const notify = () => {\n if (!disposed) input.changed();\n };\n const matches = () => queued && query?.board === queued.board && query.period === (queued.day ? "daily" : "all-time") && (query.day ?? queued.day) === queued.day;\n const refresh = async () => {\n if (!query || disposed) return;\n cancel();\n const current = ++generation, selected = { ...query };\n loading = true;\n error = false;\n notify();\n try {\n const result = await input.read(selected);\n if (disposed || current !== generation) return;\n data = result;\n if (matches()) {\n const own = result.me;\n if (own?.verified && own.score >= queued.score) saving = own.score === queued.score ? "saved" : "bestAlready";\n }\n } catch {\n if (!disposed && current === generation) error = true;\n }\n if (disposed || current !== generation) return;\n loading = false;\n if (matches() && saving !== "saved" && saving !== "bestAlready") {\n reads++;\n if (reads < 2) {\n saving = "saving";\n timer = later(() => {\n void refresh();\n }, 1600);\n } else saving = "refreshHint";\n }\n notify();\n };\n return {\n get state() {\n return { query, data, loading, error, saving: matches() ? saving : null };\n },\n select(next) {\n if (JSON.stringify(next) === JSON.stringify(query)) return;\n cancel();\n generation++;\n query = { ...next };\n data = null;\n reads = 0;\n if (matches()) saving = "saving";\n void refresh();\n },\n queued(score) {\n const board = input.manifest.boards[score.board];\n if (score.player !== input.player || !board || !Number.isSafeInteger(score.score) || score.score < 0 || score.day !== null && !validBoardDay(score.day) || !(board.periods ?? ["all-time"]).includes(score.day ? "daily" : "all-time")) return;\n const signature = JSON.stringify(score);\n if (seen.has(signature)) return;\n seen.add(signature);\n if (seen.size > 64) seen.delete(seen.values().next().value);\n queued = score;\n saving = "saving";\n reads = 0;\n this.select({ board: score.board, period: score.day ? "daily" : "all-time", guests: query?.guests ?? input.guests ?? false, ...score.day ? { day: score.day } : {} });\n if (!loading) void refresh();\n notify();\n },\n refresh,\n reset() {\n seen.clear();\n cancel();\n generation++;\n query = null;\n data = null;\n queued = null;\n saving = null;\n loading = false;\n error = false;\n },\n dispose() {\n disposed = true;\n cancel();\n generation++;\n }\n };\n}\n\n// src/overlay/locale.ts\nvar languages = ["en", "it", "es", "fr", "de", "pt", "ja"];\nfunction overlayLanguage(raw) {\n const value = raw?.toLowerCase().split("-")[0];\n return languages.includes(value) ? value : "en";\n}\nfunction overlayLocale(raw) {\n const tag = normalizeLanguage(raw);\n return tag && languages.includes(tag.split("-")[0]) ? tag : "en";\n}\n\n// src/overlay/i18n.ts\nvar words = {\n watchReplay: ["Watch replay", "Guarda replay", "Ver repetici\\xF3n", "Voir le replay", "Wiederholung ansehen", "Assistir \\xE0 repeti\\xE7\\xE3o", "\\u30EA\\u30D7\\u30EC\\u30A4\\u3092\\u898B\\u308B"],\n copyReplay: ["Copy link", "Copia link", "Copiar enlace", "Copier le lien", "Link kopieren", "Copiar link", "\\u30EA\\u30F3\\u30AF\\u3092\\u30B3\\u30D4\\u30FC"],\n replayCopied: ["Link copied", "Link copiato", "Enlace copiado", "Lien copi\\xE9", "Link kopiert", "Link copiado", "\\u30EA\\u30F3\\u30AF\\u3092\\u30B3\\u30D4\\u30FC\\u3057\\u307E\\u3057\\u305F"],\n replay: ["Replay", "Replay", "Repetici\\xF3n", "Replay", "Wiederholung", "Repeti\\xE7\\xE3o", "\\u30EA\\u30D7\\u30EC\\u30A4"],\n replayPlay: ["Play", "Riproduci", "Reproducir", "Lire", "Abspielen", "Reproduzir", "\\u518D\\u751F"],\n replayPause: ["Pause", "Pausa", "Pausar", "Pause", "Pause", "Pausar", "\\u4E00\\u6642\\u505C\\u6B62"],\n replaySeek: ["Position", "Posizione", "Posici\\xF3n", "Position", "Position", "Posi\\xE7\\xE3o", "\\u518D\\u751F\\u4F4D\\u7F6E"],\n replaySpeed: ["Speed", "Velocit\\xE0", "Velocidad", "Vitesse", "Geschwindigkeit", "Velocidade", "\\u518D\\u751F\\u901F\\u5EA6"],\n replayTruncated: ["Partial recording", "Registrazione parziale", "Grabaci\\xF3n parcial", "Enregistrement partiel", "Teilweise Aufzeichnung", "Grava\\xE7\\xE3o parcial", "\\u4E00\\u90E8\\u306E\\u307F\\u306E\\u9332\\u753B"],\n gameUpdated: ["This game was updated", "Questo gioco \\xE8 stato aggiornato", "Este juego se ha actualizado", "Ce jeu a \\xE9t\\xE9 mis \\xE0 jour", "Dieses Spiel wurde aktualisiert", "Este jogo foi atualizado", "\\u30B2\\u30FC\\u30E0\\u304C\\u66F4\\u65B0\\u3055\\u308C\\u307E\\u3057\\u305F"],\n reloadGame: ["Reload game", "Ricarica il gioco", "Recargar el juego", "Recharger le jeu", "Spiel neu laden", "Recarregar o jogo", "\\u30B2\\u30FC\\u30E0\\u3092\\u518D\\u8AAD\\u307F\\u8FBC\\u307F"],\n gameLanguages: ["Game languages", "Lingue del gioco", "Idiomas del juego", "Langues du jeu", "Spielsprachen", "Idiomas do jogo", "\\u30B2\\u30FC\\u30E0\\u306E\\u8A00\\u8A9E"],\n loading: ["Loading game...", "Caricamento...", "Cargando...", "Chargement...", "Spiel wird geladen...", "Carregando...", "\\u8AAD\\u307F\\u8FBC\\u307F\\u4E2D..."],\n loadingSlow: ["This game is taking longer than expected. You can wait a little longer or try again.", "Il gioco ci sta mettendo pi\\xF9 del previsto. Puoi aspettare ancora un po\\u2019 o riprovare.", "El juego est\\xE1 tardando m\\xE1s de lo esperado. Puedes esperar un poco m\\xE1s o volver a intentarlo.", "Le jeu met plus de temps que pr\\xE9vu. Vous pouvez patienter encore un peu ou r\\xE9essayer.", "Das Spiel braucht l\\xE4nger als erwartet. Du kannst noch etwas warten oder es erneut versuchen.", "O jogo est\\xE1 demorando mais do que o esperado. Voc\\xEA pode esperar mais um pouco ou tentar novamente.", "\\u8AAD\\u307F\\u8FBC\\u307F\\u306B\\u6642\\u9593\\u304C\\u304B\\u304B\\u3063\\u3066\\u3044\\u307E\\u3059\\u3002\\u3057\\u3070\\u3089\\u304F\\u5F85\\u3064\\u304B\\u3001\\u3082\\u3046\\u4E00\\u5EA6\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002"],\n home: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar", "\\u30D7\\u30EC\\u30A4"],\n homeMenu: ["Menu", "Menu", "Men\\xFA", "Menu", "Men\\xFC", "Menu", "\\u30E1\\u30CB\\u30E5\\u30FC"],\n mode: ["Mode", "Modalit\\xE0", "Modo", "Mode", "Modus", "Modo", "\\u30E2\\u30FC\\u30C9"],\n singlePlayer: ["Single player", "Giocatore singolo", "Un jugador", "Un joueur", "Einzelspieler", "Um jogador", "\\u30B7\\u30F3\\u30B0\\u30EB\\u30D7\\u30EC\\u30A4"],\n multiplayer: ["Multiplayer", "Multigiocatore", "Multijugador", "Multijoueur", "Mehrspieler", "Multijogador", "\\u30DE\\u30EB\\u30C1\\u30D7\\u30EC\\u30A4"],\n createRoom: ["Create a room", "Crea una stanza", "Crear una sala", "Cr\\xE9er une salle", "Raum erstellen", "Criar uma sala", "\\u30EB\\u30FC\\u30E0\\u3092\\u4F5C\\u6210"],\n play: ["Play", "Gioca", "Jugar", "Jouer", "Spielen", "Jogar", "\\u30D7\\u30EC\\u30A4"],\n friendsPlay: ["Play with friends", "Gioca con amici", "Jugar con amigos", "Jouer entre amis", "Mit Freunden spielen", "Jogar com amigos", "\\u53CB\\u9054\\u3068\\u30D7\\u30EC\\u30A4"],\n find: ["Find a match", "Trova una partita", "Buscar partida", "Trouver une partie", "Partie finden", "Encontrar partida", "\\u5BFE\\u6226\\u3092\\u63A2\\u3059"],\n join: ["Join with code", "Entra con codice", "Entrar con c\\xF3digo", "Rejoindre avec un code", "Mit Code beitreten", "Entrar com c\\xF3digo", "\\u30B3\\u30FC\\u30C9\\u3067\\u53C2\\u52A0"],\n joinInvite: ["Join this room", "Entra in questa stanza", "Entrar en est\\xE1 sala", "Rejoindre cette salle", "Diesem Raum beitreten", "Entrar nest\\xE1 sala", "\\u3053\\u306E\\u30EB\\u30FC\\u30E0\\u306B\\u53C2\\u52A0"],\n watch: ["Watch a room", "Guarda una stanza", "Observar una sala", "Regarder une salle", "Raum ansehen", "Assistir a uma sala", "\\u30EB\\u30FC\\u30E0\\u3092\\u89B3\\u6226"],\n resume: ["Resume", "Riprendi", "Continuar", "Reprendre", "Fortsetzen", "Continuar", "\\u518D\\u958B"],\n room: ["Room", "Stanza", "Sala", "Salle", "Raum", "Sala", "\\u30EB\\u30FC\\u30E0"],\n code: ["Room code", "Codice stanza", "C\\xF3digo de sala", "Code de salle", "Raumcode", "C\\xF3digo da sala", "\\u30EB\\u30FC\\u30E0\\u30B3\\u30FC\\u30C9"],\n copy: ["Copy invite", "Copia invito", "Copiar invitaci\\xF3n", "Copier le lien", "Einladung kopieren", "Copiar convite", "\\u62DB\\u5F85\\u3092\\u30B3\\u30D4\\u30FC"],\n copied: ["Invite copied", "Invito copiato", "Invitacion copiada", "Lien copi\\xE9", "Einladung kopiert", "Convite copiado", "\\u62DB\\u5F85\\u3092\\u30B3\\u30D4\\u30FC\\u3057\\u307E\\u3057\\u305F"],\n copyFailed: ["Copy this link:", "Copia questo link:", "Copia este enlace:", "Copiez ce lien :", "Diesen Link kopieren:", "Copie este link:", "\\u3053\\u306E\\u30EA\\u30F3\\u30AF\\u3092\\u30B3\\u30D4\\u30FC\\u3057\\u3066\\u304F\\u3060\\u3055\\u3044\\uFF1A"],\n joining: ["Joining room...", "Ingresso nella stanza...", "Entrando en la sala...", "Connexion \\xE0 la salle...", "Raum wird betreten...", "Entrando na sala...", "\\u30EB\\u30FC\\u30E0\\u306B\\u53C2\\u52A0\\u4E2D..."],\n matching: ["Finding your people...", "Ricerca giocatori...", "Buscando jugadores...", "Recherche de joueurs...", "Spieler werden gesucht...", "Buscando jogadores...", "\\u30D7\\u30EC\\u30A4\\u30E4\\u30FC\\u3092\\u691C\\u7D22\\u4E2D..."],\n queue: ["{n} / {max} players", "{n} / {max} giocatori", "{n} / {max} jugadores", "{n} / {max} joueurs", "{n} / {max} Spieler", "{n} / {max} jogadores", "{n} / {max} \\u4EBA"],\n cancel: ["Cancel", "Annulla", "Cancelar", "Annuler", "Abbrechen", "Cancelar", "\\u30AD\\u30E3\\u30F3\\u30BB\\u30EB"],\n close: ["Close", "Chiudi", "Cerrar", "Fermer", "Schlie\\xDFen", "Fechar", "\\u9589\\u3058\\u308B"],\n back: ["Back", "Indietro", "Volver", "Retour", "Zur\\xFCck", "Voltar", "\\u623B\\u308B"],\n ready: ["Ready", "Pronto", "Listo", "Pr\\xEAt", "Bereit", "Pronto", "\\u6E96\\u5099\\u5B8C\\u4E86"],\n unready: ["Not ready", "Non pronto", "No listo", "Pas pr\\xEAt", "Nicht bereit", "N\\xE3o pronto", "\\u6E96\\u5099\\u3092\\u89E3\\u9664"],\n start: ["Start", "Inizia", "Empezar", "Commencer", "Starten", "Come\\xE7ar", "\\u958B\\u59CB"],\n role: ["Role", "Ruolo", "Rol", "R\\xF4le", "Rolle", "Fun\\xE7\\xE3o", "\\u5F79\\u5272"],\n team: ["Team", "Squadra", "Equipo", "\\xC9quipe", "Team", "Equipe", "\\u30C1\\u30FC\\u30E0"],\n host: ["Host", "Host", "Anfitrion", "H\\xF4te", "Host", "Anfitri\\xE3o", "\\u30DB\\u30B9\\u30C8"],\n you: ["You", "Tu", "T\\xFA", "Vous", "Du", "Voc\\xEA", "\\u3042\\u306A\\u305F"],\n away: ["Away", "Assente", "Ausente", "Absent", "Abwesend", "Ausente", "\\u96E2\\u5E2D\\u4E2D"],\n needPlayers: ["Waiting for more players", "In attesa di giocatori", "Esperando m\\xE1s jugadores", "En attente de joueurs", "Weitere Spieler fehlen", "Esperando mais jogadores", "\\u30D7\\u30EC\\u30A4\\u30E4\\u30FC\\u3092\\u5F85\\u3063\\u3066\\u3044\\u307E\\u3059"],\n needReady: ["Everyone needs to be ready", "Tutti devono essere pronti", "Todos deben estar listos", "Tout le monde doit \\xEAtre pr\\xEAt", "Alle m\\xFCssen bereit sein", "Todos precisam estar prontos", "\\u5168\\u54E1\\u306E\\u6E96\\u5099\\u5B8C\\u4E86\\u3092\\u5F85\\u3063\\u3066\\u3044\\u307E\\u3059"],\n needRoles: ["Fill the required roles", "Completa i ruoli richiesti", "Completa los roles", "Compl\\xE9tez les r\\xF4les", "Ben\\xF6tigte Rollen besetzen", "Complete as fun\\xE7\\xF5es", "\\u5FC5\\u8981\\u306A\\u5F79\\u5272\\u3092\\u9078\\u3093\\u3067\\u304F\\u3060\\u3055\\u3044"],\n needTeams: ["Choose the required teams", "Scegli le squadre richieste", "Elige los equipos", "Choisissez les \\xE9quipes", "Teams auswahlen", "Escolha as equipes", "\\u5FC5\\u8981\\u306A\\u30C1\\u30FC\\u30E0\\u3092\\u9078\\u3093\\u3067\\u304F\\u3060\\u3055\\u3044"],\n waitHost: ["Waiting for the host", "In attesa dell\'host", "Esperando al anfitrion", "En attente de l\\u2019h\\xF4te", "Warten auf den Host", "Esperando o anfitri\\xE3o", "\\u30DB\\u30B9\\u30C8\\u3092\\u5F85\\u3063\\u3066\\u3044\\u307E\\u3059"],\n starting: ["Starting in", "Si inizia tra", "Empieza en", "D\\xE9but dans", "Start in", "Come\\xE7a em", "\\u958B\\u59CB\\u307E\\u3067"],\n playing: ["Playing", "In partita", "Jugando", "En jeu", "Im Spiel", "Jogando", "\\u30D7\\u30EC\\u30A4\\u4E2D"],\n ended: ["Game finished", "Partita conclusa", "Partida terminada", "Partie termin\\xE9e", "Spiel beendet", "Partida encerrada", "\\u8A66\\u5408\\u7D42\\u4E86"],\n rematchReady: ["{n}/{max} ready", "{n}/{max} pronti", "{n}/{max} listos", "{n}/{max} pr\\xEAts", "{n}/{max} bereit", "{n}/{max} prontos", "{n}/{max} \\u4EBA\\u304C\\u6E96\\u5099\\u5B8C\\u4E86"],\n rematchStart: ["Start rematch", "Avvia rivincita", "Iniciar revancha", "Lancer la revanche", "Revanche starten", "Iniciar revanche", "\\u518D\\u6226\\u3092\\u958B\\u59CB"],\n won: ["You won", "Hai vinto", "Has ganado", "Vous avez gagn\\xE9", "Du hast gewonnen", "Voc\\xEA venceu", "\\u52DD\\u5229"],\n lost: ["You lost", "Hai perso", "Has perdido", "Vous avez perdu", "Du hast verloren", "Voc\\xEA perdeu", "\\u6557\\u5317"],\n draw: ["Draw", "Pareggio", "Empate", "\\xC9galit\\xE9", "Unentschieden", "Empate", "\\u5F15\\u304D\\u5206\\u3051"],\n standings: ["Standings", "Piazzamenti", "Posiciones", "R\\xE9sultats", "Platzierungen", "Coloca\\xE7\\xF5es", "\\u9806\\u4F4D"],\n points: ["points", "punti", "puntos", "points", "Punkte", "pontos", "\\u30DD\\u30A4\\u30F3\\u30C8"],\n time: ["time", "tempo", "tiempo", "temps", "Zeit", "tempo", "\\u6642\\u9593"],\n distance: ["distance", "distanza", "distancia", "distance", "Distanz", "dist\\xE2ncia", "\\u8DDD\\u96E2"],\n again: ["Play again", "Gioca ancora", "Jugar de nuevo", "Rejouer", "Erneut spielen", "Jogar novamente", "\\u3082\\u3046\\u4E00\\u5EA6\\u30D7\\u30EC\\u30A4"],\n newRoom: ["New room. Share the new invite.", "Nuova stanza. Condividi il nuovo invito.", "Nueva sala. Comparte la invitaci\\xF3n.", "Nouvelle salle. Partagez le lien.", "Neuer Raum. Neue Einladung teilen.", "Nova sala. Compartilhe o convite.", "\\u65B0\\u3057\\u3044\\u30EB\\u30FC\\u30E0\\u3067\\u3059\\u3002\\u65B0\\u3057\\u3044\\u62DB\\u5F85\\u3092\\u5171\\u6709\\u3057\\u3066\\u304F\\u3060\\u3055\\u3044\\u3002"],\n watching: ["Watching", "In osservazione", "Observando", "Spectateur", "Zuschauen", "Assistindo", "\\u89B3\\u6226\\u4E2D"],\n delay: ["{n}s delay", "Ritardo {n}s", "Retraso de {n}s", "Retard de {n}s", "{n}s Verz\\xF6gerung", "Atraso de {n}s", "{n}\\u79D2\\u306E\\u9045\\u5EF6"],\n exit: ["Exit", "Esci", "Salir", "Quitter", "Verlassen", "Sair", "\\u7D42\\u4E86"],\n leaveNow: ["Leave for now", "Esci per ora", "Salir por ahora", "Quitter pour le moment", "Vor\\xFCbergehend verlassen", "Sair por enquanto", "\\u4E00\\u6642\\u9000\\u51FA"],\n leaveRoom: ["Leave room", "Lascia la stanza", "Abandonar sala", "Abandonner la salle", "Raum verlassen", "Deixar a sala", "\\u30EB\\u30FC\\u30E0\\u3092\\u9000\\u51FA"],\n leaveHint: ["Your room stays available for Resume.", "La stanza resta disponibile con Riprendi.", "Podr\\xE1s volver a est\\xE1 sala.", "Vous pourrez reprendre cette salle.", "Du kannst den Raum fortsetzen.", "Voc\\xEA pode voltar a est\\xE1 sala.", "\\u30EB\\u30FC\\u30E0\\u306F\\u5F8C\\u304B\\u3089\\u518D\\u958B\\u3067\\u304D\\u307E\\u3059\\u3002"],\n temporaryHint: ["The game continues. Rejoining may only be possible briefly.", "La partita continua. Il rientro pu\\xF2 essere disponibile solo per poco.", "La partida continua. Volver puede ser posible solo por poco tiempo.", "La partie continue. Le retour peut \\xEAtre limit\\xE9.", "Das Spiel l\\xE4uft weiter. R\\xFCckkehr nur kurz m\\xF6glich.", "A partida continua. O retorno pode ser limitado.", "\\u30B2\\u30FC\\u30E0\\u306F\\u7D9A\\u304D\\u307E\\u3059\\u3002\\u518D\\u53C2\\u52A0\\u3067\\u304D\\u308B\\u6642\\u9593\\u306F\\u9650\\u3089\\u308C\\u308B\\u5834\\u5408\\u304C\\u3042\\u308A\\u307E\\u3059\\u3002"],\n abandonHint: ["Leave room gives up your place.", "Lascia la stanza libera il tuo posto.", "Abandonar libera tu plaza.", "Abandonner lib\\xE8re votre place.", "Raum verlassen gibt deinen Platz frei.", "Deixar a sala libera sua vaga.", "\\u30EB\\u30FC\\u30E0\\u3092\\u9000\\u51FA\\u3059\\u308B\\u3068\\u53C2\\u52A0\\u67A0\\u3092\\u624B\\u653E\\u3057\\u307E\\u3059\\u3002"],\n reconnecting: ["Reconnecting...", "Riconnessione...", "Reconectando...", "Reconnexion...", "Verbindung wird erneuert...", "Reconectando...", "\\u518D\\u63A5\\u7D9A\\u4E2D..."],\n replaced: ["Opened in another tab", "Aperta in un\\u2019altra scheda", "Abierta en otra pest\\xE1na", "Ouverte dans un autre onglet", "In anderem Tab ge\\xF6ffnet", "Aberta em outra aba", "\\u5225\\u306E\\u30BF\\u30D6\\u3067\\u958B\\u304B\\u308C\\u307E\\u3057\\u305F"],\n error: ["Something went wrong. Try again.", "Qualcosa non va. Riprova.", "Algo sali\\xF3 mal. Reintenta.", "Une erreur est survenue. R\\xE9essayez.", "Etwas ist schiefgelaufen. Erneut versuchen.", "Algo deu errado. Tente novamente.", "\\u554F\\u984C\\u304C\\u767A\\u751F\\u3057\\u307E\\u3057\\u305F\\u3002\\u3082\\u3046\\u4E00\\u5EA6\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002"],\n noRoom: ["This room is no longer available.", "Questa stanza non \\xE8 pi\\xF9 disponibile.", "Esta sala ya no est\\xE1 disponible.", "Cette salle n\'est plus disponible.", "Dieser Raum ist nicht mehr verf\\xFCgbar.", "Esta sala n\\xE3o est\\xE1 mais disponivel.", "\\u3053\\u306E\\u30EB\\u30FC\\u30E0\\u306F\\u5229\\u7528\\u3067\\u304D\\u307E\\u305B\\u3093\\u3002"],\n full: ["This room is full.", "La stanza \\xE8 piena.", "La sala est\\xE1 llena.", "Cette salle est pleine.", "Dieser Raum ist voll.", "Esta sala est\\xE1 cheia.", "\\u30EB\\u30FC\\u30E0\\u306F\\u6E80\\u54E1\\u3067\\u3059\\u3002"],\n noMatch: ["No match this time. Try again.", "Nessun gruppo trovato. Riprova.", "No hay grupo. Reintenta.", "Aucun groupe trouv\\xE9. R\\xE9essayez.", "Keine Gruppe gefunden. Erneut versuchen.", "Nenhum grupo encontrado. Tente novamente.", "\\u76F8\\u624B\\u304C\\u898B\\u3064\\u304B\\u308A\\u307E\\u305B\\u3093\\u3067\\u3057\\u305F\\u3002\\u3082\\u3046\\u4E00\\u5EA6\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002"],\n invalidCode: ["Enter a six-character room code.", "Inserisci un codice di sei caratteri.", "Escribe un c\\xF3digo de seis caracteres.", "Entrez un code de six caracteres.", "Sechsstelligen Raumcode eingeben.", "Digite um c\\xF3digo de seis caracteres.", "6\\u6587\\u5B57\\u306E\\u30EB\\u30FC\\u30E0\\u30B3\\u30FC\\u30C9\\u3092\\u5165\\u529B\\u3057\\u3066\\u304F\\u3060\\u3055\\u3044\\u3002"],\n refused: ["The room did not accept that change.", "La stanza ha rifiutato la modifica.", "La sala rechaz\\xF3 el cambio.", "La salle a refus\\xE9 ce changement.", "Der Raum hat die \\xC4nderung abgelehnt.", "A sala recusou a altera\\xE7\\xE3o.", "\\u30EB\\u30FC\\u30E0\\u306F\\u5909\\u66F4\\u3092\\u53D7\\u3051\\u4ED8\\u3051\\u307E\\u305B\\u3093\\u3067\\u3057\\u305F\\u3002"],\n unavailable: ["Unavailable right now", "Non disponibile ora", "No disponible ahora", "Indisponible pour le moment", "Derzeit nicht verf\\xFCgbar", "Indisponivel agora", "\\u73FE\\u5728\\u5229\\u7528\\u3067\\u304D\\u307E\\u305B\\u3093"],\n offline: ["Connection unavailable. Try again.", "Connessione non disponibile. Riprova.", "Sin conexi\\xF3n. Reintenta.", "Connexion indisponible. R\\xE9essayez.", "Keine Verbindung. Erneut versuchen.", "Sem conex\\xE3o. Tente novamente.", "\\u63A5\\u7D9A\\u3067\\u304D\\u307E\\u305B\\u3093\\u3002\\u3082\\u3046\\u4E00\\u5EA6\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002"],\n saveFailed: ["Keep the room code. Resume could not be saved.", "Conserva il codice. Riprendi non \\xE8 stato salvato.", "Guarda el c\\xF3digo. No se pudo guardar el regreso.", "Gardez le code. La reprise ne peut pas \\xEAtre enregistr\\xE9e.", "Raumcode aufbewahren. Fortsetzen nicht gespeichert.", "Guarde o c\\xF3digo. O retorno n\\xE3o foi salvo.", "\\u30EB\\u30FC\\u30E0\\u30B3\\u30FC\\u30C9\\u3092\\u63A7\\u3048\\u3066\\u304F\\u3060\\u3055\\u3044\\u3002\\u518D\\u958B\\u60C5\\u5831\\u3092\\u4FDD\\u5B58\\u3067\\u304D\\u307E\\u305B\\u3093\\u3067\\u3057\\u305F\\u3002"],\n boards: ["Leaderboard", "Classifica", "Clasificaci\\xF3n", "Classement", "Bestenliste", "Classifica\\xE7\\xE3o", "\\u30E9\\u30F3\\u30AD\\u30F3\\u30B0"],\n board: ["Board", "Classifica", "Tabla", "Classement", "Bestenliste", "Tabela", "\\u30E9\\u30F3\\u30AD\\u30F3\\u30B0"],\n daily: ["Daily", "Giornaliera", "Diaria", "Du jour", "T\\xE4glich", "Di\\xE1ria", "\\u65E5\\u5225"],\n allTime: ["All time", "Di sempre", "Hist\\xF3rica", "Tous les temps", "Gesamt", "Geral", "\\u5168\\u671F\\u9593"],\n accounts: ["Accounts", "Account", "Cuentas", "Comptes", "Konten", "Contas", "\\u30A2\\u30AB\\u30A6\\u30F3\\u30C8"],\n guests: ["Guests", "Ospiti", "Invitados", "Invit\\xE9s", "G\\xE4ste", "Visitantes", "\\u30B2\\u30B9\\u30C8"],\n category: ["Category", "Categoria", "Categoria", "Cat\\xE9gorie", "Kategorie", "Categoria", "\\u533A\\u5206"],\n period: ["Period", "Periodo", "Per\\xEDodo", "P\\xE9riode", "Zeitraum", "Per\\xEDodo", "\\u671F\\u9593"],\n rank: ["Rank", "Posizione", "Puesto", "Rang", "Platz", "Posicao", "\\u9806\\u4F4D"],\n score: ["Score", "Punteggio", "Puntos", "Score", "Punkte", "Pontos", "\\u30B9\\u30B3\\u30A2"],\n verified: ["Verified", "Verificato", "Verificado", "V\\xE9rifi\\xE9", "Verifiziert", "Verificado", "\\u78BA\\u8A8D\\u6E08\\u307F"],\n own: ["Your best", "Il tuo record", "Tu record", "Votre record", "Dein Rekord", "Seu recorde", "\\u81EA\\u5DF1\\u30D9\\u30B9\\u30C8"],\n empty: ["No scores yet", "Nessun punteggio", "A\\xFAn no hay puntos", "Aucun score", "Noch keine Punkte", "Ainda sem pontos", "\\u307E\\u3060\\u30B9\\u30B3\\u30A2\\u304C\\u3042\\u308A\\u307E\\u305B\\u3093"],\n saving: ["Saving score...", "Salvataggio punteggio...", "Guardando puntos...", "Enregistrement du score...", "Punkte werden gespeichert...", "Salvando pontos...", "\\u30B9\\u30B3\\u30A2\\u3092\\u4FDD\\u5B58\\u4E2D..."],\n saved: ["Your best is on the board", "Il tuo record \\xE8 in classifica", "Tu record est\\xE1 en la tabla", "Votre record est au classement", "Dein Rekord ist eingetragen", "Seu recorde est\\xE1 na tabela", "\\u81EA\\u5DF1\\u30D9\\u30B9\\u30C8\\u304C\\u30E9\\u30F3\\u30AD\\u30F3\\u30B0\\u306B\\u53CD\\u6620\\u3055\\u308C\\u307E\\u3057\\u305F"],\n bestAlready: ["Your best is already on the board", "Il tuo record era gi\\xE0 in classifica", "Tu record ya estaba en la tabla", "Votre record est d\\xE9j\\xE0 au classement", "Dein Rekord ist bereits eingetragen", "Seu recorde j\\xE1 est\\xE1 na tabela", "\\u81EA\\u5DF1\\u30D9\\u30B9\\u30C8\\u306F\\u53CD\\u6620\\u6E08\\u307F\\u3067\\u3059"],\n refresh: ["Refresh", "Aggiorna", "Actualizar", "Actualiser", "Aktualisieren", "Atualizar", "\\u66F4\\u65B0"],\n refreshHint: ["Score not visible yet. Refresh to check.", "Punteggio non ancora visibile. Aggiorna per controllare.", "Puntos a\\xFAn no visibles. Actualiza.", "Score pas encore visible. Actualisez.", "Punkte noch nicht sichtbar. Aktualisieren.", "Pontos ainda n\\xE3o visiveis. Atualize.", "\\u30B9\\u30B3\\u30A2\\u304C\\u307E\\u3060\\u8868\\u793A\\u3055\\u308C\\u307E\\u305B\\u3093\\u3002\\u66F4\\u65B0\\u3057\\u3066\\u78BA\\u8A8D\\u3057\\u3066\\u304F\\u3060\\u3055\\u3044\\u3002"],\n friends: ["Friends & party", "Amici e gruppo", "Amigos y grupo", "Amis et groupe", "Freunde & Gruppe", "Amigos e grupo", "\\u53CB\\u9054\\u3068\\u30D1\\u30FC\\u30C6\\u30A3\\u30FC"],\n localCrew: ["Friends and party are unavailable in local preview.", "Amici e gruppo non disponibili in anteprima locale.", "Amigos y grupo no disponibles en la vista local.", "Amis et groupe indisponibles en aper\\xE7u local.", "Freunde und Gruppe in lokaler Vorschau nicht verf\\xFCgbar.", "Amigos e grupo indispon\\xEDveis na pr\\xE9via local.", "\\u30ED\\u30FC\\u30AB\\u30EB\\u30D7\\u30EC\\u30D3\\u30E5\\u30FC\\u3067\\u306F\\u53CB\\u9054\\u3068\\u30D1\\u30FC\\u30C6\\u30A3\\u30FC\\u306F\\u5229\\u7528\\u3067\\u304D\\u307E\\u305B\\u3093\\u3002"],\n loginCrew: ["Sign in on Caisual to use friends and party.", "Accedi a Caisual per amici e gruppo.", "Inicia sesion para amigos y grupo.", "Connectez-vous pour utiliser amis et groupe.", "F\\xFCr Freunde und Gruppe bei Caisual anmelden.", "Entre no Caisual para amigos e grupo.", "\\u53CB\\u9054\\u3068\\u30D1\\u30FC\\u30C6\\u30A3\\u30FC\\u3092\\u5229\\u7528\\u3059\\u308B\\u306B\\u306FCaisual\\u306B\\u30ED\\u30B0\\u30A4\\u30F3\\u3057\\u3066\\u304F\\u3060\\u3055\\u3044\\u3002"],\n online: ["Online", "Online", "En linea", "En ligne", "Online", "Online", "\\u30AA\\u30F3\\u30E9\\u30A4\\u30F3"],\n noFriends: ["No friends online", "Nessun amico online", "Sin amigos en linea", "Aucun ami en ligne", "Keine Freunde online", "Nenhum amigo online", "\\u30AA\\u30F3\\u30E9\\u30A4\\u30F3\\u306E\\u53CB\\u9054\\u306F\\u3044\\u307E\\u305B\\u3093"],\n createParty: ["Create party", "Crea gruppo", "Crear grupo", "Cr\\xE9er un groupe", "Gruppe erstellen", "Criar grupo", "\\u30D1\\u30FC\\u30C6\\u30A3\\u30FC\\u3092\\u4F5C\\u6210"],\n inviteParty: ["Invite to party", "Invita nel gruppo", "Invitar al grupo", "Inviter au groupe", "In Gruppe einladen", "Convidar para o grupo", "\\u30D1\\u30FC\\u30C6\\u30A3\\u30FC\\u306B\\u62DB\\u5F85"],\n leaveParty: ["Leave party", "Lascia gruppo", "Salir del grupo", "Quitter le groupe", "Gruppe verlassen", "Sair do grupo", "\\u30D1\\u30FC\\u30C6\\u30A3\\u30FC\\u3092\\u9000\\u51FA"],\n accept: ["Accept", "Accetta", "Aceptar", "Accepter", "Annehmen", "Aceitar", "\\u627F\\u8AFE"],\n decline: ["Decline", "Rifiuta", "Rechazar", "Refuser", "Ablehnen", "Recusar", "\\u8F9E\\u9000"],\n follow: ["Join them", "Raggiungi", "Unirse", "Rejoindre", "Beitreten", "Juntar-se", "\\u4E00\\u7DD2\\u306B\\u53C2\\u52A0"],\n voice: ["Voice", "Voce", "Voz", "Voix", "Sprache", "Voz", "\\u30DC\\u30A4\\u30B9"],\n voiceJoin: ["Join voice", "Entra in voce", "Unirse a voz", "Activer la voix", "Sprachchat beitreten", "Entrar na voz", "\\u30DC\\u30A4\\u30B9\\u306B\\u53C2\\u52A0"],\n voiceLeave: ["Leave voice", "Esci dalla voce", "Salir de voz", "Quitter la voix", "Sprachchat verlassen", "Sair da voz", "\\u30DC\\u30A4\\u30B9\\u3092\\u9000\\u51FA"],\n voiceMute: ["Mute", "Disattiva microfono", "Silenciar", "Couper le micro", "Stummschalten", "Silenciar", "\\u30DF\\u30E5\\u30FC\\u30C8"],\n voiceUnmute: ["Unmute", "Attiva microfono", "Activar micr\\xF3fono", "Activer le micro", "Mikrofon aktivieren", "Ativar microfone", "\\u30DF\\u30E5\\u30FC\\u30C8\\u89E3\\u9664"],\n voiceOff: ["Voice off", "Voce disattivata", "Voz desactivada", "Voix d\\xE9sactiv\\xE9e", "Sprachchat aus", "Voz desativada", "\\u30DC\\u30A4\\u30B9\\u30AA\\u30D5"],\n voiceJoining: ["Joining voice...", "Connessione voce...", "Conectando voz...", "Connexion vocale...", "Sprachchat verbindet...", "Conectando voz...", "\\u30DC\\u30A4\\u30B9\\u306B\\u63A5\\u7D9A\\u4E2D..."],\n voiceOn: ["Voice connected", "Voce connessa", "Voz conectada", "Voix connect\\xE9e", "Sprachchat verbunden", "Voz conectada", "\\u30DC\\u30A4\\u30B9\\u63A5\\u7D9A\\u6E08\\u307F"],\n voiceMuted: ["Muted", "Microfono disattivato", "Silenciado", "Micro coup\\xE9", "Stumm", "Silenciado", "\\u30DF\\u30E5\\u30FC\\u30C8\\u4E2D"],\n voiceMic: ["Mic on", "Microfono attivo", "Micr\\xF3fono activo", "Micro actif", "Mikrofon an", "Microfone ativo", "\\u30DE\\u30A4\\u30AF\\u30AA\\u30F3"],\n voiceListening: ["Listening only", "Solo ascolto", "Solo escucha", "\\xC9coute seule", "Nur zuh\\xF6ren", "Somente ouvindo", "\\u805E\\u304F\\u3060\\u3051"],\n voiceSpeaking: ["Speaking", "Sta parlando", "Hablando", "Parle", "Spricht", "Falando", "\\u767A\\u8A71\\u4E2D"],\n voicePeers: ["Voice participants", "Partecipanti in voce", "Participantes de voz", "Participants vocaux", "Sprachteilnehmer", "Participantes de voz", "\\u30DC\\u30A4\\u30B9\\u53C2\\u52A0\\u8005"],\n voiceEmpty: ["No one else in voice yet.", "Nessun altro in voce per ora.", "A\\xFAn no hay nadie m\\xE1s en voz.", "Personne d\\u2019autre en voix pour le moment.", "Noch niemand im Sprachchat.", "Ningu\\xE9m mais na voz ainda.", "\\u4ED6\\u306E\\u53C2\\u52A0\\u8005\\u306F\\u307E\\u3060\\u3044\\u307E\\u305B\\u3093\\u3002"],\n voiceVolume: ["Volume for {name}", "Volume di {name}", "Volumen de {name}", "Volume de {name}", "Lautst\\xE4rke f\\xFCr {name}", "Volume de {name}", "{name}\\u306E\\u97F3\\u91CF"],\n voiceUnavailable: ["Join a room with voice to use these controls.", "Entra in una stanza con voce per usare questi controlli.", "Entra en una sala con voz para usar estos controles.", "Rejoignez une salle vocale pour utiliser ces commandes.", "Diese Steuerung braucht einen Raum mit Sprachchat.", "Entre em uma sala com voz para usar estes controles.", "\\u30DC\\u30A4\\u30B9\\u5BFE\\u5FDC\\u306E\\u30EB\\u30FC\\u30E0\\u306B\\u53C2\\u52A0\\u3057\\u3066\\u304F\\u3060\\u3055\\u3044\\u3002"],\n voiceWatch: ["Voice is unavailable while watching.", "La voce non e\' disponibile in osservazione.", "La voz no est\\xE1 disponible al observar.", "La voix est indisponible en observation.", "Beim Zuschauen ist kein Sprachchat verf\\xFCgbar.", "A voz n\\xE3o est\\xE1 dispon\\xEDvel ao assistir.", "\\u89B3\\u6226\\u4E2D\\u306F\\u30DC\\u30A4\\u30B9\\u3092\\u5229\\u7528\\u3067\\u304D\\u307E\\u305B\\u3093\\u3002"],\n voiceDenied: ["Microphone permission denied. Allow it in your browser, then try again.", "Permesso microfono negato. Consenti l\'accesso nel browser e riprova.", "Permiso de micr\\xF3fono denegado. Act\\xEDvalo en el navegador e int\\xE9ntalo de nuevo.", "Acc\\xE8s au micro refus\\xE9. Autorisez-le dans le navigateur, puis r\\xE9essayez.", "Mikrofonzugriff verweigert. Im Browser erlauben und erneut versuchen.", "Permiss\\xE3o do microfone negada. Permita no navegador e tente novamente.", "\\u30DE\\u30A4\\u30AF\\u304C\\u8A31\\u53EF\\u3055\\u308C\\u3066\\u3044\\u307E\\u305B\\u3093\\u3002\\u30D6\\u30E9\\u30A6\\u30B6\\u3067\\u8A31\\u53EF\\u3057\\u3066\\u304B\\u3089\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002"],\n voiceUnsupported: ["Voice is not supported in this browser.", "Questo browser non supporta la voce.", "Este navegador no admite voz.", "Ce navigateur ne prend pas en charge la voix.", "Dieser Browser unterst\\xFCtzt keinen Sprachchat.", "Este navegador n\\xE3o oferece suporte a voz.", "\\u3053\\u306E\\u30D6\\u30E9\\u30A6\\u30B6\\u306F\\u30DC\\u30A4\\u30B9\\u306B\\u5BFE\\u5FDC\\u3057\\u3066\\u3044\\u307E\\u305B\\u3093\\u3002"],\n voiceFailed: ["Voice could not connect. Try again.", "Connessione voce non riuscita. Riprova.", "No se pudo conectar la voz. Int\\xE9ntalo de nuevo.", "Connexion vocale impossible. R\\xE9essayez.", "Sprachverbindung fehlgeschlagen. Erneut versuchen.", "N\\xE3o foi poss\\xEDvel conectar a voz. Tente novamente.", "\\u30DC\\u30A4\\u30B9\\u306B\\u63A5\\u7D9A\\u3067\\u304D\\u307E\\u305B\\u3093\\u3067\\u3057\\u305F\\u3002\\u3082\\u3046\\u4E00\\u5EA6\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002"],\n voicePeerGone: ["This participant has left voice.", "Questo partecipante e\' uscito dalla voce.", "Este participante sali\\xF3 de voz.", "Ce participant a quitt\\xE9 la voix.", "Diese Person hat den Sprachchat verlassen.", "Este participante saiu da voz.", "\\u3053\\u306E\\u53C2\\u52A0\\u8005\\u306F\\u30DC\\u30A4\\u30B9\\u3092\\u9000\\u51FA\\u3057\\u307E\\u3057\\u305F\\u3002"],\n shortcut: ["Shift+Tab shortcut", "Scorciatoia Shift+Tab", "Atajo Shift+Tab", "Raccourci Maj+Tab", "Umschalt+Tab-Kurzbefehl", "Atalho Shift+Tab", "Shift+Tab\\u30B7\\u30E7\\u30FC\\u30C8\\u30AB\\u30C3\\u30C8"],\n menu: ["Caisual menu", "Menu Caisual", "Menu Caisual", "Menu Caisual", "Caisual-Menu", "Menu Caisual", "Caisual\\u30E1\\u30CB\\u30E5\\u30FC"],\n retry: ["Retry", "Riprova", "Reintentar", "R\\xE9essayer", "Erneut versuchen", "Tentar novamente", "\\u518D\\u8A66\\u884C"]\n};\nvar column = (index) => Object.fromEntries(Object.entries(words).map(([key, row]) => [key, row[index]]));\nvar dictionaries = { en: column(0), it: column(1), es: column(2), fr: column(3), de: column(4), pt: column(5), ja: column(6) };\nfunction translator(language) {\n const dictionary = dictionaries[overlayLanguage(language)];\n return (key, values = {}) => dictionary[key].replace(/\\{(\\w+)\\}/g, (_all, name) => String(values[name] ?? ""));\n}\nfunction errorText(code) {\n if (code === "version_outdated") return "gameUpdated";\n if (code === "permission_denied") return "voiceDenied";\n if (code === "unsupported") return "voiceUnsupported";\n if (code === "voice_disabled") return "voiceUnavailable";\n if (code === "voice_error") return "voiceFailed";\n if (code === "voice_peer_missing") return "voicePeerGone";\n if (code === "not_publishing") return "voiceListening";\n if (["room_not_found", "room_ended", "version_closed", "no_resume"].includes(code)) return "noRoom";\n if (["room_full", "role_full"].includes(code)) return "full";\n if (code === "replaced") return "replaced";\n if (code === "no_match") return "noMatch";\n if (code === "invalid_code") return "invalidCode";\n if (["offline", "timeout"].includes(code)) return "offline";\n if (code.startsWith("role_") || ["not_in_lobby", "not_host", "session_replaced"].includes(code)) return "refused";\n if (code === "save_failed") return "saveFailed";\n return "error";\n}\n\n// src/overlay/ui-model.ts\nfunction phase(session) {\n if (!session || session.kind === "boot") return "boot";\n if (session.kind === "attaching" || session.kind === "matching") return session.kind;\n if (session.room && ["closed", "replaced"].includes(session.room.connection)) return "error";\n if (session.kind === "local") return session.localStatus === "ended" ? "ended" : "playing";\n if (session.room?.status === "ended" || session.room?.status === "finished") return "ended";\n if (session.kind === "watch") return "watching";\n if (session.kind === "room" && session.room) return session.room.status;\n return "home";\n}\nfunction initialUi(manifest) {\n return { session: null, panel: "auto", mode: manifest.modes[0]?.id ?? "", busy: false, error: null, notice: null, shortcutEnabled: true };\n}\nfunction groupModes(manifest) {\n const groups = { singlePlayer: [], multiplayer: [] };\n for (const mode of manifest.modes) groups[risolviModalita(manifest, mode.id).players.max === 1 ? "singlePlayer" : "multiplayer"].push(mode);\n return groups;\n}\nfunction reduceUi(model, action) {\n switch (action.type) {\n case "session": {\n const next = action.session, changed = next?.id !== model.session?.id || next === null;\n const pending = next?.kind === "attaching" || next?.kind === "matching";\n const nextPhase = phase(next), transition = phase(model.session) !== nextPhase;\n const automatic = transition && ["lobby", "countdown", "playing", "ended"].includes(nextPhase) && [null, "auto", "room", "invite", "home"].includes(model.panel);\n return {\n ...model,\n session: next,\n mode: next?.mode ?? model.mode,\n panel: changed || pending || automatic ? "auto" : model.panel,\n error: changed ? null : model.error,\n notice: changed ? null : model.notice\n };\n }\n case "panel":\n return { ...model, panel: action.panel, error: null, notice: null };\n case "mode":\n return { ...model, mode: action.mode, error: null };\n case "busy":\n return { ...model, busy: action.busy };\n case "error":\n return { ...model, error: action.code, busy: false };\n case "notice":\n return { ...model, notice: action.notice };\n case "shortcut":\n return { ...model, shortcutEnabled: action.enabled };\n }\n}\nfunction visiblePanel(model) {\n const current = phase(model.session);\n if (current === "boot" || current === "attaching" || current === "matching" || current === "error") return current;\n if (model.session?.room?.limits.max === 1 && ["room", "invite"].includes(model.panel ?? "")) return "home";\n if (model.panel !== "auto") return model.panel;\n if (model.session?.room?.limits.max === 1 && current === "lobby") return null;\n return current === "home" ? "home" : current === "lobby" ? "room" : current === "countdown" ? "countdown" : null;\n}\nfunction primaryAction(manifest, mode) {\n const selected = manifest.modes.find((item) => item.id === mode);\n if (!selected) return null;\n return {\n op: selected.execution === "local" ? "local.start" : "room.create",\n friends: selected.execution === "room" && risolviModalita(manifest, mode).players.max > 1\n };\n}\nfunction startReason(manifest, session) {\n const room = session?.room;\n if (!room || session.kind !== "room" || room.status !== "lobby" || room.connection !== "connected") return "unavailable";\n const connected = room.players.filter((p) => p.connected), active = connected.filter((p) => p.role !== "spectator");\n if (active.length < room.limits.min) return "needPlayers";\n if (connected.some((p) => !p.ready)) return "needReady";\n if (manifest.roles.some((role) => active.filter((p) => p.role === role.id).length < role.min)) return "needRoles";\n if (manifest.teams && (active.some((p) => p.team === null) || new Set(active.map((p) => p.team)).size < manifest.teams.min)) return "needTeams";\n return room.host !== room.you ? "waitHost" : null;\n}\nfunction canPlayAgain(session) {\n if (phase(session) !== "ended") return false;\n if (session?.kind === "local") return true;\n if (session?.room?.status === "finished") return session.kind === "room" && session.room.connection === "connected" && (session.room.limits.max === 1 || session.room.players.some((p) => p.id === session.room.you && p.connected && p.role !== "spectator" && !p.ready));\n return session?.kind === "room" && (session.room?.limits.max === 1 || !session.room?.lobby || session.room.host === session.room.you);\n}\nfunction normalizeInvite(code) {\n const value = code.toUpperCase().replace(/[\\s-]/g, "");\n return /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/.test(value) ? value : null;\n}\nfunction matchPresentation(session) {\n const room = session?.room;\n if (!room || !["finished", "ended"].includes(room.status)) return null;\n const result = readMatchResult(room.result, room.players.map((p) => p.id));\n if (!result) return null;\n const own = session?.kind === "room" && room.players.some((p) => p.id === room.you && p.role !== "spectator") && result.standings.some((p) => p.playerId === room.you);\n const first = result.standings[0];\n const winners = result.winners ?? result.standings.filter((p, i) => i === 0 || first.rank !== void 0 && p.rank === first.rank).map((p) => p.playerId);\n return { result, outcome: !own ? "ended" : result.draw ? "draw" : winners.includes(room.you) ? "won" : "lost" };\n}\n\n// src/overlay/styles.ts\nvar styles = `\n.game-icon{width:28px;height:28px;aspect-ratio:1;object-fit:contain;border-radius:7px;flex:none;vertical-align:middle}.game-icon-title{width:36px;height:36px;border-radius:10px}\n.safe-area-probe{position:fixed;visibility:hidden;pointer-events:none;padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px)}\n:host{all:initial;position:fixed;inset:0;z-index:10000;pointer-events:none;font:15px/1.45 system-ui,sans-serif;color:#f4f4f1;color-scheme:dark;--accent:#a8efc5}\n[data-layout],[data-surface],.sr{pointer-events:none}*{box-sizing:border-box}button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}button,select,input{border:1px solid #ffffff30;background:#25292b;color:inherit;border-radius:12px;min-height:44px;padding:10px 14px}button{cursor:pointer}button:disabled{opacity:.45;cursor:default}button:hover:not(:disabled){background:#343b3a}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--accent);outline-offset:3px}a{color:var(--accent)}.primary{background:var(--accent);color:var(--accent-ink,#11221b);border-color:transparent;font-weight:700}.primary:hover:not(:disabled){filter:brightness(1.1);background:var(--accent)}.quiet{background:transparent}label{display:grid;gap:6px;text-align:left}select,input{width:100%;min-width:0}h1,h2,p{margin:0}h1{font-size:clamp(26px,5vw,42px);line-height:1.1;letter-spacing:-.035em}h2{font-size:20px}small,.muted{color:#bdc5c1}.stack{display:grid;gap:16px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row>*{flex:0 1 auto}.row .grow,.grow{flex:1}.split{display:grid;grid-template-columns:1fr 1fr;gap:10px}.pill{position:absolute;top:max(10px,env(safe-area-inset-top));right:max(10px,env(safe-area-inset-right));display:flex;height:44px;border:1px solid #ffffff35;border-radius:24px;background:#171e20eF;box-shadow:0 4px 20px #0004;pointer-events:auto;overflow:hidden}.pill button{border:0;border-radius:0;padding:8px 13px;background:transparent}.pill button:focus-visible{outline-offset:-4px}.pill small{margin-left:8px}.backdrop{position:absolute;inset:0;background:#0b151ce8;backdrop-filter:blur(10px);pointer-events:auto;display:grid;place-items:center;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));overflow:auto}.backdrop.home{background-color:#142127;background-size:contain;background-repeat:no-repeat;background-position:center}.dialog{position:relative;width:min(100%,540px);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#141b1df5;border:1px solid #ffffff25;border-radius:22px;padding:24px;box-shadow:0 20px 80px #0005}.dialog.wide{width:min(100%,700px)}.top{display:flex;align-items:center;gap:12px;margin-bottom:18px}.top h2{flex:1}.tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid #ffffff25;padding-bottom:12px}.tabs button{min-height:36px;padding:6px 10px}.tabs [aria-current=true]{border-color:var(--accent)}.roster{list-style:none;padding:0;margin:0;display:grid;gap:8px;max-height:32dvh;overflow:auto}.roster li{display:flex;align-items:center;gap:8px;padding:10px;background:#ffffff08;border-radius:10px}.roster .name{flex:1;overflow-wrap:anywhere}.badge{border:1px solid #ffffff30;border-radius:6px;padding:2px 6px;font-size:12px}.code{font-size:24px;letter-spacing:.13em;font-variant-numeric:tabular-nums}.notice,.error{border-radius:10px;padding:10px;background:#a8efc514;overflow-wrap:anywhere}.error{background:#ff8b7720;color:#ffd2c9}.countdown{font-size:88px;line-height:1;text-align:center;font-variant-numeric:tabular-nums}.ended{position:absolute;bottom:max(12px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);max-width:calc(100% - 24px);width:max-content;background:#171e20f5;pointer-events:auto;border:1px solid #ffffff30;border-radius:16px;padding:10px;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap}.ended [data-rematch-players]{max-width:100%;max-height:3.2em;overflow:auto;overflow-wrap:anywhere}.standings{list-style:none;margin:0;padding:0;flex-basis:100%;max-height:20dvh;overflow:auto;font-size:13px}.standings li{display:flex;justify-content:space-between;gap:16px;overflow-wrap:anywhere}.ended{max-height:calc(100dvh - 80px);overflow:auto}.ended strong{padding:0 8px}.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.table-wrap{overflow:auto;max-height:38dvh}table{width:100%;border-collapse:collapse;text-align:left}th,td{padding:9px 6px;border-bottom:1px solid #ffffff20}td:nth-child(3){text-align:right}td small{display:block}.self{background:#ffffff0a}.checkbox{display:flex;align-items:center;gap:8px;font-size:13px}.checkbox input{width:18px;min-height:18px}.full{width:100%}\n.game-heading{display:flex;align-items:center;gap:12px;min-width:0;flex:1}.game-heading h1{font-size:28px;overflow-wrap:anywhere}.home .top{margin-bottom:12px}.game-description{font-size:13px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.experience-tabs{display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:4px;border:1px solid #ffffff18;border-radius:14px;background:#0003}.experience-tabs button{background:transparent;border-color:transparent;font-size:14px;font-weight:600;padding:10px 8px;border-radius:10px;color:#bdc5c1}.experience-tabs [aria-selected=true]{background:#ffffff16;color:#f4f4f1;box-shadow:0 1px 4px #0003}.experience-tabs button:focus-visible{outline-offset:-3px}.home-content{gap:14px;min-width:0}.mode-details{display:grid;gap:6px;min-width:0}.mode-details h2{font-size:16px;font-weight:600}.mode-details label{font-size:13px}.mode-instructions{font-size:12px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.play-actions{gap:8px}.play-actions .primary{min-height:50px;font-size:16px}.home-links{gap:4px}.home-links button{border-color:transparent;font-size:13px}.home-links button:hover:not(:disabled){background:#ffffff0a}.resume-action{gap:4px}.resume-action small{text-align:center}.panel-footer{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:18px;padding-top:10px;border-top:1px solid #ffffff18;color:#bdc5c1}.panel-footer .checkbox{font-size:11px;white-space:nowrap;min-height:32px;gap:6px}.panel-footer input{margin:0;accent-color:var(--accent);width:14px;min-height:14px}.panel-footer small{min-width:0;text-align:right;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase}\n[hidden]{display:none!important}.voice-peers{list-style:none;margin:0;padding:0;display:grid;gap:10px}.voice-peers li{border:1px solid #ffffff25;border-radius:12px;padding:12px;display:grid;gap:8px}.voice-peers [data-speaking=true]{border-color:var(--accent)}.voice-peers input{width:100%;accent-color:var(--accent);padding:0}.voice-peers label{font-size:13px}.pill .voice-toggle{width:44px;padding:8px}.voice-toggle[data-voice-state=on][data-muted=false]{color:var(--accent)}\n.boot{position:absolute;inset:0;z-index:2;isolation:isolate;display:grid;place-items:center;overflow:auto;overscroll-behavior:contain;padding:max(100px,env(safe-area-inset-top)) max(24px,env(safe-area-inset-right)) max(48px,env(safe-area-inset-bottom)) max(24px,env(safe-area-inset-left));background:#0b151c;opacity:1;transition:opacity .4s ease;pointer-events:auto;outline:none}\n.boot::before,.boot::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:-1}.boot::before{background:radial-gradient(ellipse at 50% 38%,color-mix(in srgb,var(--accent),transparent 80%),transparent 65%)}.boot::after{background:radial-gradient(ellipse at 50% 38%,#0b151c20,#0b151cd9 85%),linear-gradient(#0b151c66,#0b151cbf)}\n.boot-cover{position:fixed;inset:0;z-index:-2;width:100%;height:100%;aspect-ratio:3 / 2;object-fit:contain;filter:blur(20px);opacity:.65;pointer-events:none}\n.boot-brand{position:absolute;top:max(28px,env(safe-area-inset-top));left:max(32px,env(safe-area-inset-left));display:flex;align-items:center;gap:10px;font-size:14px;font-weight:650;letter-spacing:.02em;color:#f4f4f1b3}.boot-brand span{display:grid;place-items:center;width:36px;height:36px;border:1px solid #ffffff25;border-radius:12px;background:#171e20af;box-shadow:0 4px 20px #0004;color:var(--boot-accent);font-size:20px;font-weight:800}\n.boot-content{width:min(100%,900px);text-align:center;display:grid;justify-items:center;gap:24px}.boot h1{max-width:16ch;font-size:clamp(44px,8vw,108px);font-weight:800;line-height:1.04;letter-spacing:-.05em;overflow-wrap:anywhere;text-wrap:balance;color:var(--boot-accent);text-shadow:0 20px 80px #0005}\n.boot-progress{width:112px;height:3px;border-radius:12px;background:#ffffff20;overflow:hidden;margin-top:12px}.boot-progress span{display:block;width:44%;height:100%;border-radius:inherit;background:var(--boot-accent);animation:boot-progress 1.8s ease-in-out infinite}.boot-status{max-width:42ch;min-height:3em;font-size:14px;line-height:1.5;color:#d3dad6;text-wrap:balance}.boot-recovery{min-height:44px}.boot-recovery .row{justify-content:center}.boot-leaving{opacity:0;pointer-events:none}\n@keyframes boot-progress{0%{transform:translateX(-110%)}100%{transform:translateX(340%)}}\n@media(max-width:480px){.dialog{padding:18px;border-radius:18px}.split{grid-template-columns:1fr 1fr;gap:8px}.tabs{gap:4px}.tabs button{font-size:13px;padding:6px 8px}.pill button:focus-visible{outline-offset:-4px}.pill small{display:none}.ended{gap:6px}.standings{list-style:none;margin:0;padding:0;flex-basis:100%;max-height:20dvh;overflow:auto;font-size:13px}.standings li{display:flex;justify-content:space-between;gap:16px;overflow-wrap:anywhere}.ended{max-height:calc(100dvh - 80px);overflow:auto}.ended strong{font-size:13px}.ended button{padding:8px 10px;font-size:13px}.roster{max-height:28dvh}}\n.replay-controls{position:absolute;bottom:max(12px,env(safe-area-inset-bottom));left:12px;right:12px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px;background:#141b1df5;border:1px solid #ffffff30;border-radius:16px;pointer-events:auto}.replay-position{flex:1;min-width:100px}.replay-controls label{font-size:12px}.replay-controls span{font-variant-numeric:tabular-nums;font-size:13px}.replay-link{display:inline-flex;align-items:center;min-height:44px;padding:10px 14px;border-radius:12px;text-decoration:none}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}.boot{transition:none}.boot-progress span{animation:none;transform:translateX(65%)}}\n`;\n\n// src/overlay/voice-panel.ts\nfunction voiceEligible(manifest, session) {\n return manifest.voice !== "none" && session?.kind !== "watch" && session?.room?.players.find((player) => player.id === session.room?.you)?.role !== "spectator";\n}\nfunction voiceStatus(voice, t) {\n const key = voice.state === "joining" ? "voiceJoining" : voice.state === "reconnecting" ? "reconnecting" : voice.state === "off" ? "voiceOff" : "voiceOn";\n return t(key);\n}\nfunction updateVoicePanel(container, input) {\n const { session, t } = input, voice = session?.kind === "room" ? session.voice : null;\n if (!voiceEligible(input.manifest, session) || !voice) {\n container.replaceChildren();\n const note = container.ownerDocument.createElement("p");\n note.textContent = t(session?.kind === "watch" || session?.room?.players.find((p) => p.id === session.room?.you)?.role === "spectator" ? "voiceWatch" : "voiceUnavailable");\n container.append(note);\n return;\n }\n if (!container.querySelector("[data-voice-status]")) container.innerHTML = `<p role="status" aria-live="polite" data-voice-status></p><p data-voice-self></p>\n <div class="row"><button type="button" data-action="voice-join"></button><button type="button" data-action="voice-mute"></button><button type="button" data-action="voice-leave"></button></div>\n <p class="error" role="alert" data-voice-error hidden></p><h3 data-voice-heading></h3><ul class="voice-peers" data-voice-peers></ul><p class="muted" data-voice-empty></p>`;\n const get = (selector) => container.querySelector(selector);\n const status = get("[data-voice-status]");\n status.textContent = voiceStatus(voice, t);\n status.dataset.voiceState = voice.state;\n const mic = (value) => t(!value.mic ? "voiceListening" : value.muted ? "voiceMuted" : value.speaking ? "voiceSpeaking" : "voiceMic");\n get("[data-voice-self]").textContent = voice.state === "off" ? "" : `${t("you")}: ${mic(voice)}`;\n const join = get(\'[data-action="voice-join"]\'), mute = get(\'[data-action="voice-mute"]\'), leave = get(\'[data-action="voice-leave"]\');\n join.textContent = t("voiceJoin");\n join.hidden = voice.state !== "off";\n join.disabled = session?.room?.connection !== "connected" || input.pending === "voice.join";\n mute.textContent = t(voice.muted ? "voiceUnmute" : "voiceMute");\n mute.hidden = voice.state !== "on" || !voice.mic;\n mute.disabled = input.pending === "voice.mute";\n mute.setAttribute("aria-pressed", String(voice.muted));\n leave.textContent = t("voiceLeave");\n leave.hidden = voice.state === "off" && input.pending !== "voice.join";\n leave.disabled = input.pending === "voice.leave";\n const error = get("[data-voice-error]");\n error.hidden = !input.error;\n error.textContent = input.error ? t(errorText(input.error)) : "";\n get("[data-voice-heading]").textContent = t("voicePeers");\n get("[data-voice-empty]").textContent = t("voiceEmpty");\n get("[data-voice-empty]").hidden = voice.peers.length > 0;\n const list = get("[data-voice-peers]"), ids = new Set(voice.peers.map((peer) => peer.id));\n for (const row of list.querySelectorAll("[data-voice-peer]")) if (!ids.has(row.dataset.voicePeer)) row.remove();\n for (const peer of voice.peers) {\n let row = [...list.children].find((node) => node.dataset.voicePeer === peer.id);\n if (!row) {\n row = container.ownerDocument.createElement("li");\n row.dataset.voicePeer = peer.id;\n row.innerHTML = \'<div class="row"><strong data-peer-name></strong><small data-peer-status></small></div><label><span data-volume-label></span><input type="range" min="0" max="1" step="0.05" data-control="voice-volume"></label>\';\n row.querySelector("input").dataset.peer = peer.id;\n list.append(row);\n }\n const name = session?.room?.players.find((player) => player.id === peer.id)?.name ?? peer.id;\n row.dataset.mic = String(peer.mic);\n row.dataset.muted = String(peer.muted);\n row.dataset.speaking = String(peer.speaking);\n row.querySelector("[data-peer-name]").textContent = name;\n row.querySelector("[data-peer-status]").textContent = mic(peer);\n row.querySelector("[data-volume-label]").textContent = t("voiceVolume", { name });\n const range = row.querySelector("input");\n if (range.dataset.editing !== "true") range.value = String(peer.volume);\n range.setAttribute("aria-valuetext", `${Math.round(Number(range.value) * 100)}%`);\n range.disabled = voice.state !== "on";\n }\n}\n\n// src/overlay/ui.ts\nvar escape = (value) => String(value ?? "").replace(/[&<>"\']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", \'"\': "&quot;", "\'": "&#39;" })[c]);\nfunction mountOverlay(input) {\n const manifest = input.configuration.manifest;\n if (manifest.overlay?.version !== 1 && !input.bridge.replay) return null;\n const document = input.container.ownerDocument, win = document.defaultView, t = translator(input.language);\n const host = document.createElement("div");\n host.dataset.caisualOverlay = "";\n host.lang = overlayLanguage(input.language);\n host.style.setProperty("pointer-events", "none", "important");\n const root = host.attachShadow({ mode: "open" });\n if (typeof win.CSSStyleSheet?.prototype.replaceSync === "function" && "adoptedStyleSheets" in root) {\n const sheet = new win.CSSStyleSheet();\n sheet.replaceSync(styles);\n root.adoptedStyleSheets = [sheet];\n } else {\n const sheet = document.createElement("link");\n sheet.rel = "stylesheet";\n sheet.href = "/__caisual/overlay/v1.css";\n root.append(sheet);\n }\n const elements = document.createElement("div");\n elements.dataset.layout = "";\n elements.style.pointerEvents = "none";\n elements.innerHTML = `<div data-surface></div><div class="sr" role="status" aria-live="polite" data-live></div>`;\n const safeProbe = document.createElement("div");\n safeProbe.className = "safe-area-probe";\n safeProbe.setAttribute("aria-hidden", "true");\n root.append(elements, safeProbe);\n const surface = root.querySelector("[data-surface]"), live = root.querySelector("[data-live]");\n surface.style.pointerEvents = "none";\n live.style.pointerEvents = "none";\n const accent = manifest.overlay?.accent ?? "#a8efc5";\n host.style.setProperty("--accent", accent);\n const rgb = [1, 3, 5].map((i) => parseInt(accent.slice(i, i + 2), 16) / 255).map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\n const luminance = rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722;\n host.style.setProperty("--accent-ink", luminance > 0.179 ? "#000000" : "#ffffff");\n host.style.setProperty("--boot-accent", luminance > 0.179 ? accent : `color-mix(in srgb, ${accent}, #ffffff 70%)`);\n input.container.append(host);\n let model = initialUi(manifest), disposed = false, operation = 0, lastView = "", geometryFrame = 0;\n let inviteAfterCreate;\n let lastPhase = "", wasModal = false, copyFallback = null;\n let boot = null, bootTimer = 0, bootFadeTimer = 0;\n let voiceError = null, voicePending = null, voiceOperation = 0;\n let codeDraft = input.configuration.invite ?? "";\n const oldInert = Boolean(input.frame.inert), oldTabIndex = input.frame.getAttribute("tabindex");\n try {\n model.shortcutEnabled = win.localStorage.getItem("caisual-overlay-shortcut-v1") !== "off";\n } catch {\n }\n const boards = input.boards ? createBoardController({ manifest, player: input.player.id, guests: input.player.guest, read: input.boards, changed: () => render() }) : null;\n const stops = [];\n const modeGroups = groupModes(manifest);\n const selectedMode = () => manifest.modes.find((mode) => mode.id === model.mode);\n const disabled = () => model.busy ? " disabled" : "";\n const button = (action, key, extra = "", off = false) => `<button type="button" data-action="${action}"${extra}${off || model.busy ? " disabled" : ""}>${t(key)}</button>`;\n function resetBootWait() {\n win.clearTimeout(bootTimer);\n if (!boot || phase(model.session) !== "boot") return;\n boot.querySelector("[data-boot-message]").textContent = t("loading");\n boot.querySelector("[data-boot-recovery]").hidden = true;\n bootTimer = win.setTimeout(() => {\n bootTimer = 0;\n if (disposed || !boot) return;\n boot.querySelector("[data-boot-message]").textContent = t("loadingSlow");\n boot.querySelector("[data-boot-recovery]").hidden = false;\n }, 9e3);\n }\n function updateBoot(loading) {\n if (loading) {\n if (boot) {\n win.clearTimeout(bootFadeTimer);\n bootFadeTimer = 0;\n boot.inert = false;\n boot.removeAttribute("aria-hidden");\n boot.style.pointerEvents = "auto";\n boot.classList.remove("boot-leaving");\n return;\n }\n boot = document.createElement("section");\n boot.className = "boot";\n boot.tabIndex = -1;\n boot.setAttribute("aria-labelledby", "boot-title");\n boot.setAttribute("aria-describedby", "boot-status");\n boot.style.pointerEvents = "auto";\n boot.innerHTML = `<div class="boot-brand" aria-hidden="true"><span>C</span>Caisual</div>\n <div class="boot-content"><h1 id="boot-title">${escape(manifest.name)}</h1>\n <div class="boot-progress" aria-hidden="true"><span></span></div>\n <p class="boot-status" id="boot-status" role="status" aria-live="polite" aria-atomic="true"><span class="sr">${escape(manifest.name)}. </span><span data-boot-message></span></p>\n <div class="boot-recovery"><div class="row" data-boot-recovery hidden>${button("reload", "retry", \' class="primary"\')}${button("exit-now", "exit", \' class="quiet"\')}</div></div>\n </div>`;\n if (input.configuration.coverUrl) {\n const cover = document.createElement("img");\n cover.className = "boot-cover";\n cover.alt = "";\n cover.setAttribute("aria-hidden", "true");\n cover.addEventListener("error", () => {\n cover.hidden = true;\n }, { once: true });\n cover.src = input.configuration.coverUrl;\n boot.prepend(cover);\n }\n elements.append(boot);\n resetBootWait();\n } else if (boot && !boot.inert) {\n boot.inert = true;\n boot.setAttribute("aria-hidden", "true");\n boot.style.pointerEvents = "none";\n boot.classList.add("boot-leaving");\n const remove = () => {\n win.clearTimeout(bootTimer);\n bootTimer = 0;\n boot?.remove();\n boot = null;\n bootFadeTimer = 0;\n };\n if (win.matchMedia?.("(prefers-reduced-motion: reduce)").matches) remove();\n else bootFadeTimer = win.setTimeout(remove, 420);\n }\n }\n const dispatch = (action) => {\n if (disposed) return;\n model = reduceUi(model, action);\n render();\n };\n const announce = (text) => {\n if (live.textContent !== text) live.textContent = text;\n };\n const controls = () => [...root.querySelectorAll(\'button:not(:disabled),a[href],input:not(:disabled),select:not(:disabled),[tabindex="0"]\')].filter((el) => el.tabIndex !== -1 && !el.closest("[hidden]"));\n const soloMode = (mode) => (mode === null || manifest.modes.some((item) => item.id === mode)) && risolviModalita(manifest, mode).players.max === 1;\n const solo = () => model.session?.room?.limits.max === 1;\n const roomCode = () => solo() ? null : model.session?.room?.code ?? null;\n const setPanel = (panel) => {\n if (panel === "boards" && boards && !boards.state.query) {\n const id = Object.keys(manifest.boards)[0];\n if (id) boards.select({ board: id, period: (manifest.boards[id].periods ?? ["all-time"])[0], guests: input.player.guest });\n }\n copyFallback = null;\n dispatch({ type: "panel", panel });\n };\n const close = () => {\n const current = phase(model.session), panel = visiblePanel(model);\n if (current === "boot") return;\n if (current === "home") setPanel("home");\n else if (current === "lobby" && panel !== "room") setPanel("room");\n else setPanel(null);\n };\n const toggle = () => {\n if (visiblePanel(model)) close();\n else setPanel(model.session?.room && !model.session.room.replay ? "room" : "home");\n };\n async function perform(op, args, after) {\n const token = ++operation;\n dispatch({ type: "error", code: null });\n dispatch({ type: "busy", busy: true });\n try {\n await input.bridge.request(op, args);\n if (token === operation && !disposed) await after?.();\n } catch (error) {\n if (token === operation && !disposed && error.code !== "cancelled") dispatch({ type: "error", code: error.code ?? "offline" });\n } finally {\n if (token === operation && !disposed) dispatch({ type: "busy", busy: false });\n }\n }\n function updateVoice() {\n const container = root.querySelector("[data-voice-panel]");\n if (container) updateVoicePanel(container, { manifest, session: model.session, t, error: voiceError, pending: voicePending });\n const toggle2 = root.querySelector("[data-voice-toggle]"), voice = model.session?.voice;\n if (toggle2) {\n toggle2.dataset.voiceState = voice?.state ?? "off";\n toggle2.dataset.muted = String(voice?.muted ?? false);\n toggle2.setAttribute("aria-label", `${t("voice")}: ${voice ? voiceStatus(voice, t) : t("voiceOff")}`);\n toggle2.textContent = voice?.state === "on" && !voice.muted ? "\\u25CF" : "\\u25CB";\n }\n }\n async function performVoice(op, args) {\n const sessionId = model.session?.id, epoch = input.bridge.epoch, volume = op === "voice.setVolume";\n const token = volume ? voiceOperation : ++voiceOperation;\n const current = () => !disposed && model.session?.id === sessionId && input.bridge.epoch === epoch && token === voiceOperation;\n voiceError = null;\n if (!volume) voicePending = op;\n updateVoice();\n try {\n await input.bridge.request(op, args);\n } catch (error) {\n if (current()) voiceError = error.code ?? "voice_error";\n } finally {\n if (current()) {\n if (!volume) voicePending = null;\n updateVoice();\n }\n }\n }\n async function copyInvite() {\n const code = roomCode();\n if (!code) return;\n const url = input.inviteUrl(code);\n try {\n await win.navigator.clipboard.writeText(url);\n dispatch({ type: "notice", notice: t("copied") });\n } catch {\n copyFallback = url;\n render();\n root.querySelector(\'input[data-control="invite-link"]\')?.select();\n }\n }\n function invitation() {\n if (solo()) return "";\n const code = roomCode();\n if (!code) return `<p>${t("noRoom")}</p>`;\n return `<div class="row"><div class="grow"><small>${t("code")}</small><div class="code" data-room-code>${escape(code)}</div></div>${button("copy", "copy")}</div>${copyFallback ? `<label>${t("copyFailed")}<input data-control="invite-link" readonly value="${escape(copyFallback)}"></label>` : ""}`;\n }\n function navigation(panel) {\n const items = [];\n if (model.session?.room && !solo()) items.push(["room", "room"], ["invite", "copy"]);\n items.push(["friends", "friends"]);\n if (Object.keys(manifest.boards).length) items.push(["boards", "boards"]);\n if (voiceEligible(manifest, model.session)) items.push(["voice", "voice"]);\n return `<nav class="tabs" aria-label="Caisual">${items.map(([id, key]) => button(`panel:${id}`, key, ` aria-current="${id === panel}"`)).join("")}</nav>`;\n }\n function footer() {\n const languages2 = escape(manifestLanguages(manifest).join(" \\xB7 "));\n return `<footer class="panel-footer"><label class="checkbox" title="${t("shortcut")}"><input type="checkbox" data-control="shortcut" aria-label="${t("shortcut")}"${model.shortcutEnabled ? " checked" : ""}>Shift+Tab</label><small data-game-languages title="${t("gameLanguages")}: ${languages2}" aria-label="${t("gameLanguages")}: ${languages2}">${languages2}</small></footer>`;\n }\n function home() {\n const selected = selectedMode(), action = primaryAction(manifest, model.mode), session = model.session;\n const description = resolveText(manifest.description, input.language, manifestLanguages(manifest)[0]);\n const singlePlayer = soloMode(model.mode), active = singlePlayer ? "singlePlayer" : "multiplayer";\n const modes = modeGroups[active], hasTabs = modeGroups.singlePlayer.length > 0 && modeGroups.multiplayer.length > 0;\n const playKey = singlePlayer ? "play" : input.crew?.getSnapshot().party ? "friendsPlay" : "createRoom";\n const resume = session?.resume && soloMode(session.resume.mode) === singlePlayer;\n return `${description ? `<p class="muted game-description" data-game-description title="${escape(description)}">${escape(description)}</p>` : ""}\n ${input.configuration.invite && phase(session) === "home" ? button("join-invite", input.bridge.watch ? "watch" : "joinInvite", \' class="primary"\', !session?.ready) : ""}\n ${hasTabs ? `<div class="experience-tabs" role="tablist" aria-label="${t("mode")}">${["singlePlayer", "multiplayer"].map((key) => button(`mode:${key}`, key, ` role="tab" id="tab-${key}" aria-selected="${key === active}" aria-controls="experience-panel" tabindex="${key === active ? 0 : -1}" data-mode="${escape(modeGroups[key][0].id)}"`)).join("")}</div>` : ""}\n <div class="stack home-content"${hasTabs ? ` role="tabpanel" id="experience-panel" aria-labelledby="tab-${active}"` : ""}>\n <div class="mode-details">${modes.length > 1 ? `<label>${t("mode")}<select data-control="mode"${disabled()}>${modes.map((mode) => `<option value="${escape(mode.id)}"${mode.id === model.mode ? " selected" : ""}>${escape(risolviPresentazione(manifest, mode.id, input.language).label)}</option>`).join("")}</select></label>` : `<h2 data-mode-label>${escape(risolviPresentazione(manifest, model.mode, input.language).label)}</h2>`}\n ${selected?.instructions ? `<p class="muted mode-instructions" data-mode-instructions title="${escape(resolveText(selected.instructions, input.language, manifestLanguages(manifest)[0]))}">${escape(resolveText(selected.instructions, input.language, manifestLanguages(manifest)[0]))}</p>` : ""}</div>\n ${resume ? `<div class="stack resume-action">${button("resume", "resume", "", !session.ready)}${singlePlayer ? "" : `<small>${escape(session.resume.code)}</small>`}</div>` : ""}\n <div class="stack play-actions">${!singlePlayer && selected?.matchmaking ? button("match", "find", \' class="primary"\', !selected.matchmaking.defaults || !session?.ready) : ""}\n ${action ? button("play", playKey, singlePlayer || !selected?.matchmaking ? \' class="primary"\' : "", !session?.ready) : ""}</div>\n ${!singlePlayer ? `<div class="split home-links">${button("panel:join", "join", \' class="quiet"\', !session?.ready)}${manifest.spectators ? button("panel:watch", "watch", \' class="quiet"\', !session?.ready) : ""}</div>` : ""}\n ${!singlePlayer || Object.keys(manifest.boards).length ? `<div class="${singlePlayer ? "stack" : "split"} home-links">${!singlePlayer ? button("panel:friends", "friends", \' class="quiet"\') : ""}${Object.keys(manifest.boards).length ? button("panel:boards", "boards", \' class="quiet"\') : ""}</div>` : ""}\n ${solo() ? button("panel:exit", "exit", \' class="quiet"\') : ""}\n </div>`;\n }\n function room() {\n const session = model.session, room2 = session?.room;\n if (!room2) return `<p>${t("noRoom")}</p>`;\n const own = room2.players.find((player) => player.id === room2.you), lobby = room2.status === "lobby" && session?.kind === "room";\n const canRole = session?.kind === "room" && (lobby || room2.status === "playing" && room2.requestRole);\n const reason = startReason(manifest, session);\n return `${invitation()}<ul class="roster" aria-label="${t("room")}">${room2.players.map((p) => `<li data-player-id="${escape(p.id)}"><span class="name">${escape(p.name)} ${p.id === room2.you ? `<small>(${t("you")})</small>` : ""}</span>${p.id === room2.host ? `<span class="badge">${t("host")}</span>` : ""}${p.role ? `<small>${escape(resolveText(manifest.roles.find((r) => r.id === p.role)?.label, input.language, manifestLanguages(manifest)[0], p.role))}</small>` : ""}${p.team ? `<small>${t("team")} ${p.team}</small>` : ""}<small>${!p.connected ? t("away") : lobby ? t(p.ready ? "ready" : "unready") : ""}</small></li>`).join("")}</ul>\n ${canRole && manifest.roles.length ? `<label>${t("role")}<select data-control="role"${disabled()}><option value="" disabled${!own?.role ? " selected" : ""}>${t("role")}</option>${manifest.roles.map((role) => `<option value="${escape(role.id)}"${role.id === own?.role ? " selected" : ""}>${escape(resolveText(role.label, input.language, manifestLanguages(manifest)[0], role.id))}</option>`).join("")}</select></label>` : ""}\n ${lobby && manifest.teams ? `<label>${t("team")}<select data-control="team"${disabled()}><option value="" disabled${!own?.team ? " selected" : ""}>${t("team")}</option>${Array.from({ length: manifest.teams.max }, (_, i) => `<option value="${i + 1}"${own?.team === i + 1 ? " selected" : ""}>${t("team")} ${i + 1}</option>`).join("")}</select></label>` : ""}\n ${lobby ? `<div class="row">${button("ready", own?.ready ? "unready" : "ready", \' class="primary"\', room2.connection !== "connected")}${room2.host === room2.you ? button("start", "start", "", reason !== null) : ""}</div>${reason ? `<p class="muted" data-start-reason>${t(reason)}</p>` : ""}` : ""}\n ${session?.kind === "watch" ? `<p>${t("watching")} \\xB7 ${t("delay", { n: (room2.delayMs ?? 0) / 1e3 })}</p>` : ""}\n ${navigation("room")}${button("panel:exit", "exit", \' class="quiet"\')}`;\n }\n function crew() {\n const provider = input.crew, state = provider?.getSnapshot();\n if (!provider || provider.unavailable || !state?.you) return `<p>${t(provider?.unavailable === "local" ? "localCrew" : "loginCrew")}</p>`;\n const online = state.friends.filter((friend) => friend.online), party = state.party;\n const person = (p) => `<li>${p.game ? `<img class="game-icon" src="${escape(p.game.iconUrl)}" alt="" />` : ""}<span class="name">${escape(p.name)}<small>${p.game ? ` \\xB7 ${escape(p.game.name)}` : ""}</small></span>${p.room && p.game ? button("follow", "follow", ` data-code="${escape(p.room.code)}" data-game="${escape(p.game.slug)}"`) : ""}${party?.leader === state.you.id && !party.members.some((member) => member.id === p.id) ? button("party-invite", "inviteParty", ` data-player="${escape(p.id)}"`) : ""}</li>`;\n return `${!state.connected ? `<p>${t("reconnecting")}</p>` : ""}${party ? `<ul class="roster">${party.members.map(person).join("")}</ul>${button("party-leave", "leaveParty")}` : button("party-create", "createParty")}\n ${state.invites.map((invite) => `<div class="row"><span class="grow">${escape(invite.from.name)}</span>${button("party-accept", "accept", ` data-party="${escape(invite.party)}"`)}${button("party-decline", "decline", ` data-party="${escape(invite.party)}"`)}</div>`).join("")}\n ${state.follow ? `<div class="row"><span class="grow"><img class="game-icon" src="${escape(state.follow.game.iconUrl)}" alt="" /> ${escape(state.follow.from.name)} \\xB7 ${escape(state.follow.game.name)}</span>${button("follow", "follow", ` data-code="${escape(state.follow.code)}" data-game="${escape(state.follow.game.slug)}"`)}</div>` : ""}\n <h2>${t("online")}</h2>${online.length ? `<ul class="roster">${online.map(person).join("")}</ul>` : `<p class="muted">${t("noFriends")}</p>`}`;\n }\n function leaderboard() {\n if (!boards || !boards.state.query) return `<p>${t("unavailable")}</p>`;\n const { query, data, loading, error, saving } = boards.state;\n const board = manifest.boards[query.board];\n return `<label>${t("board")}<select data-control="board">${Object.entries(manifest.boards).map(([id, value]) => `<option value="${escape(id)}"${query.board === id ? " selected" : ""}>${escape(resolveText(value.label, input.language, manifestLanguages(manifest)[0], id))}</option>`).join("")}</select></label>\n <div class="split"><label>${t("period")}<select data-control="period">${(board.periods ?? ["all-time"]).map((period) => `<option value="${period}"${query.period === period ? " selected" : ""}>${t(period === "daily" ? "daily" : "allTime")}</option>`).join("")}</select></label><label>${t("category")}<select data-control="category"><option value="accounts"${!query.guests ? " selected" : ""}>${t("accounts")}</option><option value="guests"${query.guests ? " selected" : ""}>${t("guests")}</option></select></label></div>\n ${query.period === "daily" ? `<small data-board-day>${escape(data?.day ?? query.day ?? new Date(input.bridge.serverTime() ?? Date.now()).toISOString().slice(0, 10))}</small>` : ""}\n ${saving ? `<p role="status" data-saving>${t(saving)}</p>` : ""}${error ? `<p role="alert">${t("offline")}</p>` : ""}\n ${data ? `<div class="table-wrap"><table><thead><tr><th>${t("rank")}</th><th>${t(query.guests ? "guests" : "accounts")}</th><th>${t("score")}</th></tr></thead><tbody>${data.entries.map((entry) => `<tr${entry.me ? \' class="self"\' : ""}><td>${entry.rank}</td><td>${escape(entry.name)}${entry.verified ? `<small>${t("verified")}</small>` : ""}</td><td>${entry.score}</td></tr>`).join("")}</tbody></table>${data.entries.length ? "" : `<p>${t("empty")}</p>`}</div><p data-own-score>${t("own")} (${t(data.ownGuest ? "guests" : "accounts")}): ${data.me ? `#${data.me.rank} \\xB7 ${data.me.score}${data.me.verified ? ` \\xB7 ${t("verified")}` : ""}` : t("empty")}</p>` : `<p>${t(loading ? "loading" : "empty")}</p>`}\n ${button("refresh", "refresh", "", loading)}`;\n }\n function content(panel) {\n switch (panel) {\n case "home":\n if (model.session?.room?.replay) return `${button("close", "back")}${button("exit-now", "exit")}`;\n return home();\n case "room":\n return room();\n case "invite":\n return invitation();\n case "friends":\n return crew();\n case "voice":\n return \'<div class="stack" data-voice-panel></div>\';\n case "boards":\n return leaderboard();\n case "join":\n case "watch":\n return `<form class="stack" data-form="${panel}"><label>${t("code")}<input data-control="code" name="code" autocomplete="off" autocapitalize="characters" spellcheck="false" maxlength="16" value="${escape(codeDraft)}" required></label><button class="primary" type="submit"${disabled()}>${t(panel === "join" ? "join" : "watch")}</button></form>`;\n case "attaching":\n case "matching":\n return `<p role="status">${t(panel === "matching" ? "matching" : "joining")}</p>${model.session?.waiting ? `<p>${t("queue", { n: model.session.waiting.players, max: model.session.waiting.max })}</p>` : ""}<button type="button" data-action="cancel">${t("cancel")}</button>`;\n case "countdown":\n return `<p>${t("starting")}</p><div class="countdown" data-countdown></div>`;\n case "boot":\n return "";\n case "error":\n return `<p role="alert">${t(model.session?.room?.connection === "replaced" ? "replaced" : "noRoom")}</p>${button("leave", "home")}${button("exit-now", "exit")}`;\n case "exit":\n return model.session?.kind === "room" && phase(model.session) !== "ended" ? `<p>${t(model.session.room?.persistent ? "leaveHint" : "temporaryHint")}</p>${invitation()}${button("disconnect-exit", "leaveNow", \' class="primary"\')}<p class="muted">${t("abandonHint")}</p>${button("leave-exit", "leaveRoom")}` : button("leave-exit", "exit", \' class="primary"\');\n }\n }\n function title(panel) {\n const keys = { boot: "loading", home: "home", room: "room", invite: "copy", friends: "friends", voice: "voice", boards: "boards", join: "join", watch: "watch", attaching: "joining", matching: "matching", countdown: "starting", error: "error", exit: "exit" };\n return t(keys[panel]);\n }\n function updateCountdown() {\n const at = model.session?.room?.countdownAt, now = input.bridge.serverTime();\n const value = at === null || at === void 0 || now === null ? "..." : String(Math.max(0, Math.round((at - now) / 1e3)));\n const node = root.querySelector("[data-countdown]");\n if (node && node.textContent !== value) {\n node.textContent = value;\n announce(`${t("starting")} ${value}`);\n }\n }\n function geometry() {\n geometryFrame = 0;\n if (disposed || !input.bridge.epoch || !model.session) return;\n const frame = gameViewport(input.frame), { scaleX, scaleY } = frame;\n const reservedRects = [...root.querySelectorAll("[data-reserve]")].map((el) => {\n const rect = el.getBoundingClientRect(), left = Math.max(frame.left, rect.left), top = Math.max(frame.top, rect.top), right = Math.min(frame.right, rect.right), bottom = Math.min(frame.bottom, rect.bottom);\n return { x: Math.max(0, Math.round((left - frame.left) * scaleX)), y: Math.max(0, Math.round((top - frame.top) * scaleY)), width: Math.max(0, Math.round((right - left) * scaleX)), height: Math.max(0, Math.round((bottom - top) * scaleY)) };\n }).filter((rect) => rect.width && rect.height).slice(0, 8);\n const view = { inputBlocked: phase(model.session) === "boot" || !!visiblePanel(model), reservedRects, safeArea: measureSafeArea(input.frame, safeProbe), shortcutEnabled: model.shortcutEnabled };\n const serialized = `${input.bridge.epoch}:${JSON.stringify(view)}`;\n if (lastView === serialized) return;\n lastView = serialized;\n void input.bridge.request("overlay.view", view).catch(async (error) => {\n if (error?.code === "invalid_request" && lastView === serialized) {\n const { safeArea, ...legacy } = view;\n try {\n await input.bridge.request("overlay.view", legacy);\n return;\n } catch {\n }\n }\n if (lastView === serialized) lastView = "";\n });\n }\n function resize() {\n if (!geometryFrame) geometryFrame = win.requestAnimationFrame(geometry);\n }\n const replayLink = () => {\n const id = model.session?.room?.replayId;\n return manifest.replays && id && input.replayUrl ? input.replayUrl(id) : null;\n };\n function replayActions() {\n const url = replayLink();\n return url ? `<a class="primary replay-link" href="${escape(url)}">${t("watchReplay")}</a>${button("copy-replay", "copyReplay")}` : "";\n }\n const replayTime = (ms) => `${Math.floor(ms / 6e4)}:${String(Math.floor(ms / 1e3) % 60).padStart(2, "0")}`;\n function updateReplayPosition() {\n const playback = model.session?.room?.replay;\n if (!playback) return;\n const slider = root.querySelector("[data-control=replay-seek]");\n if (slider && slider.dataset.editing !== "true") slider.value = String(playback.positionMs);\n const time = root.querySelector("[data-replay-time]");\n if (time) time.textContent = `${replayTime(playback.positionMs)} / ${replayTime(playback.durationMs)}`;\n }\n function replayBar() {\n const playback = model.session?.room?.replay;\n if (!playback) return "";\n return `<div class="replay-controls" data-reserve role="region" aria-label="${t("replay")}">\n ${button(playback.paused ? "replay-play" : "replay-pause", playback.paused ? "replayPlay" : "replayPause")}\n <label class="replay-position">${t("replaySeek")}<input type="range" data-control="replay-seek" min="0" max="${playback.durationMs}" step="1" value="${playback.positionMs}" aria-label="${t("replaySeek")}"></label>\n <span data-replay-time>${replayTime(playback.positionMs)} / ${replayTime(playback.durationMs)}</span>\n <label>${t("replaySpeed")}<select data-control="replay-speed">${[0.5, 1, 2, 4].map((speed) => `<option value="${speed}"${speed === playback.speed ? " selected" : ""}>${speed}\\xD7</option>`).join("")}</select></label>\n ${playback.truncated ? `<small>${t("replayTruncated")}</small>` : ""}</div>`;\n }\n function resultBar() {\n const presentation = matchPresentation(model.session);\n if (!presentation) return `<strong>${t("ended")}</strong>`;\n const room2 = model.session.room, { result } = presentation;\n const unit = result.unit && ["points", "time", "distance"].includes(result.unit) ? t(result.unit) : result.unit;\n const rows = result.standings.map((p, i) => {\n const name = room2.players.find((player) => player.id === p.playerId).name;\n const score = p.score === void 0 ? "" : `<span>${escape(String(p.score))}${unit ? ` ${escape(unit)}` : ""}</span>`;\n return `<li><span>${p.rank ?? i + 1}. ${escape(name)}</span>${score}</li>`;\n }).join("");\n return `<strong data-outcome>${t(presentation.outcome)}</strong><ol class="standings" aria-label="${t("standings")}">${rows}</ol>`;\n }\n function rematchBar() {\n const session = model.session, room2 = session?.room;\n if (!room2 || room2.status !== "finished" || solo()) return "";\n const active = room2.players.filter((p) => p.connected && p.role !== "spectator");\n const ready = active.filter((p) => p.ready), host2 = session.kind === "room" && room2.host === room2.you;\n const canStart = room2.connection === "connected" && active.length >= room2.limits.min && ready.length === active.length;\n return `<small role="status" data-rematch-ready>${t("rematchReady", { n: ready.length, max: active.length })}</small>\n ${ready.length ? `<small data-rematch-players>${ready.map((p) => escape(p.name)).join(", ")}</small>` : ""}\n ${room2.rematch?.autoStart ? "" : host2 ? button("restart", "rematchStart", \' class="primary"\', !canStart) : `<small>${t("waitHost")}</small>`}`;\n }\n function render() {\n if (disposed) return;\n const panel = visiblePanel(model), current = phase(model.session), room2 = model.session?.room;\n const focused = root.activeElement;\n const focusPeer = focused?.dataset.peer;\n const focusKey = focused?.dataset.control ? ["control", focused.dataset.control] : focused?.dataset.action ? ["action", focused.dataset.action] : null;\n const previousScroll = root.querySelector(".dialog")?.scrollTop ?? 0;\n const selection = focused?.tagName === "INPUT" ? { start: focused.selectionStart, end: focused.selectionEnd } : null;\n const crewState = input.crew?.getSnapshot(), invitations = (crewState?.invites.length ?? 0) + (crewState?.follow ? 1 : 0);\n const label = current === "watching" ? t("watching") : room2?.connection === "reconnecting" ? t("reconnecting") : solo() ? "Caisual" : room2?.code ?? "Caisual";\n surface.innerHTML = current === "boot" ? "" : `<div class="pill" data-reserve><button type="button" data-action="menu" aria-label="${t("menu")}" aria-expanded="${!!panel}">C<span aria-hidden="true"><small>${escape(label)}</small></span></button>${voiceEligible(manifest, model.session) && model.session?.kind === "room" ? `<button type="button" class="voice-toggle" data-voice-toggle data-action="panel:voice"></button>` : ""}${invitations ? `<button type="button" data-action="panel:friends" aria-label="${t("friends")} (${invitations})">${invitations}</button>` : ""}</div>\n ${!panel ? replayBar() : ""}\n ${current === "ended" && !panel && !room2?.replay ? `<div class="ended" data-reserve role="region" aria-label="${t("ended")}">${resultBar()}${replayActions()}${boards?.state.saving ? `<small role="status" data-saving>${t(boards.state.saving)}</small>` : ""}${canPlayAgain(model.session) ? button("again", "again", \' class="primary"\') : model.session?.kind === "room" && room2?.status !== "finished" && !solo() ? `<small>${t("waitHost")}</small>` : ""}${rematchBar()}${Object.keys(manifest.boards).length ? button("panel:boards", "boards") : ""}${button("panel:home", "homeMenu")}</div>` : ""}\n ${panel ? `<div class="backdrop${panel === "home" ? " home" : ""}"><section class="dialog${panel === "boards" || panel === "friends" ? " wide" : ""}" role="dialog" aria-modal="true" aria-labelledby="panel-title" tabindex="-1"><div class="top">${panel === "home" ? `<div class="game-heading">${input.configuration.iconUrl ? `<img class="game-icon game-icon-title" src="${escape(input.configuration.iconUrl)}" alt="" />` : ""}<h1 id="panel-title">${escape(manifest.name)}</h1></div>` : `<h2 id="panel-title">${title(panel)}</h2>`}<button type="button" data-action="close" aria-label="${t("close")}">\\xD7</button></div><div class="stack">${content(panel)}${model.error ? `<p class="error" role="alert" data-error>${t(errorText(model.error))}</p>${model.error === "version_outdated" ? button("reload-game", "reloadGame", \' class="primary"\') : ""}` : ""}${model.session?.resumeError ? `<p class="error" role="alert">${t("saveFailed")}</p>` : ""}${model.notice ? `<p class="notice" role="status">${escape(model.notice)}</p>` : ""}</div>${panel === "home" || panel === "room" ? footer() : ""}</section></div>` : ""}`;\n for (const element of surface.querySelectorAll(".pill,.backdrop,.ended,.replay-controls")) element.style.pointerEvents = "auto";\n const backdrop = root.querySelector(".backdrop.home");\n if (backdrop && input.configuration.coverUrl) backdrop.style.backgroundImage = `linear-gradient(#0b151c99,#0b151cee),url(${JSON.stringify(input.configuration.coverUrl)})`;\n host.dataset.phase = current;\n host.dataset.panel = panel ?? "";\n input.frame.inert = !!panel || oldInert;\n if (panel) input.frame.tabIndex = -1;\n else if (oldTabIndex === null) input.frame.removeAttribute("tabindex");\n else input.frame.setAttribute("tabindex", oldTabIndex);\n updateVoice();\n updateBoot(current === "boot");\n const dialog = root.querySelector(".dialog");\n if (dialog) dialog.scrollTop = previousScroll;\n const focusPanel = current === "boot" ? boot : dialog;\n const matched = focusKey ? [...root.querySelectorAll(`[data-${focusKey[0]}]`)].find((el) => el.getAttribute(`data-${focusKey[0]}`) === focusKey[1] && el.dataset.peer === focusPeer) : null;\n if (!panel && wasModal && !input.frame.inert && input.frame.isConnected) {\n input.frame.focus({ preventScroll: true });\n input.frame.contentWindow?.focus();\n } else if (matched && (!panel || focusPanel?.contains(matched)) && !matched.hasAttribute("disabled")) {\n matched.focus({ preventScroll: true });\n if (matched.tagName === "INPUT" && selection?.start !== null && selection?.end !== null && selection) {\n try {\n matched.setSelectionRange(selection.start, selection.end);\n } catch {\n }\n }\n } else if (panel && (!wasModal || focused)) (current === "boot" ? boot : dialog?.querySelector(\'select,input,button:not([data-action="close"]):not(:disabled)\') ?? dialog)?.focus({ preventScroll: true });\n wasModal = !!panel;\n if (lastPhase !== current) {\n lastPhase = current;\n announce(current === "boot" ? "" : t({ home: "home", attaching: "joining", matching: "matching", lobby: "room", countdown: "starting", playing: "playing", ended: "ended", watching: "watching", error: "error" }[current]));\n }\n updateCountdown();\n resize();\n }\n const click = (event) => {\n const target = event.target.closest("button[data-action]");\n if (!target || target.disabled) return;\n const action = target.dataset.action;\n event.stopPropagation();\n if (action.startsWith("mode:") && target.dataset.mode) {\n dispatch({ type: "mode", mode: target.dataset.mode });\n return;\n }\n if (action.startsWith("panel:")) {\n setPanel(action.slice(6));\n return;\n }\n switch (action) {\n case "replay-play":\n void perform("replay.play", {});\n break;\n case "replay-pause":\n void perform("replay.pause", {});\n break;\n case "copy-replay": {\n const url = replayLink();\n if (url) void Promise.resolve().then(() => win.navigator.clipboard.writeText(url)).then(() => {\n announce(t("replayCopied"));\n }, () => {\n setPanel("home");\n dispatch({ type: "notice", notice: `${t("copyFailed")} ${url}` });\n });\n break;\n }\n case "menu":\n toggle();\n break;\n case "close":\n close();\n break;\n case "play": {\n const selected = primaryAction(manifest, model.mode);\n if (selected) void perform(selected.op, { mode: model.mode });\n break;\n }\n case "match":\n void perform("room.match", { mode: model.mode });\n break;\n case "join-invite":\n if (input.configuration.invite) void perform(input.bridge.watch ? "room.watch" : "room.join", { code: input.configuration.invite });\n break;\n case "resume":\n void perform("session.resume", {});\n break;\n case "cancel":\n void perform("session.cancel", {});\n break;\n case "ready":\n void perform("room.ready", { ready: !model.session?.room?.players.find((p) => p.id === model.session?.room?.you)?.ready });\n break;\n case "start":\n void perform("room.start", {});\n break;\n case "restart":\n void perform("room.restart", {});\n break;\n case "copy":\n void copyInvite();\n break;\n case "voice-join":\n void performVoice("voice.join", {});\n break;\n case "voice-mute":\n void performVoice("voice.mute", { muted: !model.session?.voice?.muted });\n break;\n case "voice-leave":\n void performVoice("voice.leave", {});\n break;\n case "again": {\n if (!canPlayAgain(model.session)) break;\n if (model.session?.kind === "local") void perform("local.start", { mode: model.session.mode ?? model.mode });\n else if (model.session?.room?.status === "finished") void perform("room.restart", {});\n else if (solo()) void perform("room.create", { mode: model.session?.room?.mode ?? null });\n else {\n inviteAfterCreate = roomCode();\n void perform("room.create", { mode: model.session?.room?.mode ?? null }).then(() => {\n if (model.error) inviteAfterCreate = void 0;\n });\n }\n break;\n }\n case "disconnect-exit":\n void perform("session.disconnect", {}, input.exit);\n break;\n case "leave-exit":\n void perform("session.leave", {}, input.exit);\n break;\n case "leave":\n void perform("session.leave", {}, () => setPanel("home"));\n break;\n case "exit-now":\n input.exit();\n break;\n case "reload-game":\n input.bridge.reload();\n break;\n case "reload":\n boot?.focus({ preventScroll: true });\n resetBootWait();\n input.frame.src = input.frame.src;\n break;\n case "refresh":\n void boards?.refresh();\n break;\n case "party-create":\n input.crew?.party.create();\n break;\n case "party-leave":\n input.crew?.party.leave();\n break;\n case "party-invite":\n input.crew?.party.invite(target.dataset.player);\n break;\n case "party-accept":\n input.crew?.party.accept(target.dataset.party);\n break;\n case "party-decline":\n input.crew?.party.decline(target.dataset.party);\n break;\n case "follow":\n input.crew?.follow(target.dataset.game, target.dataset.code);\n break;\n }\n };\n const change = (event) => {\n const target = event.target, field = target.dataset.control;\n if (field === "replay-seek") {\n delete target.dataset.editing;\n void perform("replay.seek", { positionMs: Number(target.value) });\n return;\n }\n if (field === "replay-speed") {\n void perform("replay.speed", { speed: Number(target.value) });\n return;\n }\n if (field === "voice-volume") {\n target.dataset.editing = "true";\n void performVoice("voice.setVolume", { playerId: target.dataset.peer, volume: Number(target.value) }).finally(() => {\n delete target.dataset.editing;\n updateVoice();\n });\n return;\n }\n if (field === "mode") dispatch({ type: "mode", mode: target.value });\n if (field === "role") void perform(model.session?.room?.status === "lobby" ? "room.role" : "room.requestRole", { role: target.value });\n if (field === "team") void perform("room.team", { team: Number(target.value) });\n if (field === "shortcut") {\n const enabled = target.checked;\n try {\n win.localStorage.setItem("caisual-overlay-shortcut-v1", enabled ? "on" : "off");\n } catch {\n }\n dispatch({ type: "shortcut", enabled });\n }\n const query = boards?.state.query;\n if (query && ["board", "period", "category"].includes(field ?? "")) {\n const next = { ...query };\n if (field === "board") {\n next.board = target.value;\n next.period = (manifest.boards[next.board].periods ?? ["all-time"])[0];\n delete next.day;\n }\n if (field === "period") {\n next.period = target.value;\n delete next.day;\n }\n if (field === "category") next.guests = target.value === "guests";\n boards.select(next);\n }\n };\n const submit = (event) => {\n const form = event.target;\n if (!form.dataset.form) return;\n event.preventDefault();\n const code = normalizeInvite(form.querySelector(\'input[data-control="code"]\').value);\n if (!code) {\n dispatch({ type: "error", code: "invalid_code" });\n return;\n }\n void perform(form.dataset.form === "watch" ? "room.watch" : "room.join", { code });\n };\n const keydown = (event) => {\n const panel = visiblePanel(model);\n const tab = root.activeElement;\n if (tab?.getAttribute("role") === "tab" && ["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {\n event.preventDefault();\n event.stopImmediatePropagation();\n const tabs = [...root.querySelectorAll(\'[role="tab"]:not(:disabled)\')];\n const next = event.key === "Home" ? tabs[0] : event.key === "End" ? tabs.at(-1) : tabs.find((item) => item !== tab);\n if (next) {\n next.focus();\n next.click();\n }\n return;\n }\n if (panel && event.key === "Escape") {\n event.preventDefault();\n event.stopImmediatePropagation();\n close();\n return;\n }\n if (panel && event.key === "Tab") {\n const items = controls().filter((el) => el.closest(panel === "boot" ? ".boot" : ".dialog")), first = items[0], last = items.at(-1);\n if (!first) {\n event.preventDefault();\n return;\n }\n if (event.shiftKey && (root.activeElement === first || !items.includes(root.activeElement))) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && (root.activeElement === last || !items.includes(root.activeElement))) {\n event.preventDefault();\n first.focus();\n }\n } else if (!panel && model.shortcutEnabled && event.key === "Tab" && event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {\n event.preventDefault();\n toggle();\n }\n };\n root.addEventListener("input", (event) => {\n const node = event.target;\n if (node.dataset.control === "code") codeDraft = node.value;\n if (node.dataset.control === "voice-volume" || node.dataset.control === "replay-seek") node.dataset.editing = "true";\n });\n root.addEventListener("click", click);\n root.addEventListener("change", change);\n root.addEventListener("submit", submit);\n win.addEventListener("keydown", keydown, true);\n win.addEventListener("resize", resize);\n win.addEventListener("scroll", resize, true);\n win.visualViewport?.addEventListener("resize", resize);\n win.visualViewport?.addEventListener("scroll", resize);\n const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null;\n observer?.observe(input.frame);\n const countdownTimer = win.setInterval(updateCountdown, 250);\n stops.push(input.bridge.subscribe((session) => {\n const previous = model.session;\n if (!session || session.id !== previous?.id) {\n voiceOperation++;\n voicePending = null;\n voiceError = null;\n }\n if (!session) {\n lastView = "";\n boards?.reset();\n operation++;\n model.busy = false;\n }\n const withoutPosition = (state) => state?.room?.replay ? { ...state, room: { ...state.room, replay: { ...state.room.replay, positionMs: 0 } } } : state;\n if (previous?.room?.replay && session?.room?.replay && JSON.stringify(withoutPosition(previous)) === JSON.stringify(withoutPosition(session))) {\n model = reduceUi(model, { type: "session", session });\n updateReplayPosition();\n } else if (previous && session && JSON.stringify({ ...previous, voice: null }) === JSON.stringify({ ...session, voice: null })) {\n model = reduceUi(model, { type: "session", session });\n updateVoice();\n } else dispatch({ type: "session", session });\n const created = roomCode();\n if (inviteAfterCreate !== void 0 && created && created !== inviteAfterCreate) {\n inviteAfterCreate = void 0;\n setPanel("invite");\n dispatch({ type: "notice", notice: t("newRoom") });\n void copyInvite();\n }\n }));\n stops.push(input.bridge.onOpen((panel) => setPanel(panel)), input.bridge.onShortcut(toggle), input.bridge.onError(({ error }) => {\n if (!visiblePanel(model)) setPanel("room");\n dispatch({ type: "error", code: error.code });\n }));\n stops.push(input.bridge.onScore((score) => boards?.queued(score)));\n if (input.crew) stops.push(input.crew.subscribe(() => {\n const state = input.crew.getSnapshot();\n if (state.follow || state.invites.length) announce(t("friends"));\n render();\n }));\n render();\n return { element: host, root, dispose() {\n disposed = true;\n operation++;\n stops.forEach((stop) => stop());\n boards?.dispose();\n observer?.disconnect();\n win.clearInterval(countdownTimer);\n win.cancelAnimationFrame(geometryFrame);\n win.clearTimeout(bootTimer);\n win.clearTimeout(bootFadeTimer);\n win.removeEventListener("keydown", keydown, true);\n win.removeEventListener("resize", resize);\n win.removeEventListener("scroll", resize, true);\n win.visualViewport?.removeEventListener("resize", resize);\n win.visualViewport?.removeEventListener("scroll", resize);\n input.frame.inert = oldInert;\n if (oldTabIndex === null) input.frame.removeAttribute("tabindex");\n else input.frame.setAttribute("tabindex", oldTabIndex);\n void input.bridge.request("overlay.view", { inputBlocked: false, reservedRects: [], shortcutEnabled: false }).catch(() => {\n });\n host.remove();\n } };\n}\nexport {\n avviaHandshake,\n creaPonteOspite,\n eMessaggioReady,\n eRichiestaBiglietto,\n mountOverlay,\n overlayConfiguration,\n overlayLanguage,\n overlayLocale,\n styles as overlayStyles,\n stanzaDaMessaggio\n};\n');
5087
5327
  return;
5088
5328
  }
5089
5329
  if (url.pathname === "/__caisual/players" && (request.method === "GET" || request.method === "HEAD")) {
@@ -5112,8 +5352,7 @@ var DevService = class {
5112
5352
  "autoplay",
5113
5353
  "pointer-lock",
5114
5354
  ...this.manifest.input.includes("gamepad") ? ["gamepad"] : [],
5115
- ...voceAttiva ? ["microphone"] : [],
5116
- ...this.manifest.isolated ? ["cross-origin-isolated"] : []
5355
+ ...voceAttiva ? ["microphone"] : []
5117
5356
  ].join("; "),
5118
5357
  gameOrigin: this.gameOrigin,
5119
5358
  portalOrigin: this.portalOrigin,
@@ -5181,7 +5420,7 @@ var DevService = class {
5181
5420
  }
5182
5421
  let player = this.playersBySession.get(sessionId);
5183
5422
  if (player === void 0) {
5184
- const id = `dev_${createHash2("sha256").update(sessionId).digest("hex").slice(0, 24)}`;
5423
+ const id = `dev_${createHash3("sha256").update(sessionId).digest("hex").slice(0, 24)}`;
5185
5424
  player = {
5186
5425
  sessionId,
5187
5426
  id,
@@ -5295,8 +5534,8 @@ var DevService = class {
5295
5534
  if (body === null || !Object.hasOwn(body, "value")) {
5296
5535
  throw new DevHttpError(400, "invalid_request", "The request body must contain value.");
5297
5536
  }
5298
- if (!saves.has(key) && saves.size >= 32) {
5299
- throw new DevHttpError(409, "save_limit", "This player already has 32 saves for this game.", [
5537
+ if (!saves.has(key) && saves.size >= 64) {
5538
+ throw new DevHttpError(409, "save_limit", "This player already has 64 saves for this game.", [
5300
5539
  "Remove an existing save before creating a new key."
5301
5540
  ]);
5302
5541
  }
@@ -5756,7 +5995,7 @@ Connection: close\r
5756
5995
  }
5757
5996
  };
5758
5997
  async function runDev(options) {
5759
- const root = resolve(process.cwd(), options.folder);
5998
+ const root = resolve2(process.cwd(), options.folder);
5760
5999
  const stat = await fs3.stat(root).catch(() => null);
5761
6000
  if (stat === null || !stat.isDirectory()) throw new Error(`The game folder was not found: ${root}`);
5762
6001
  const [{ manifest, clientRoot }, definition] = await Promise.all([
@@ -6065,7 +6304,12 @@ test('full game, distinct guests and standard overlay in Chromium', { timeout: 1
6065
6304
  const identities = await Promise.all(games.map(game => game.evaluate(() => window.caisualDebug.c.player.id)));
6066
6305
  assert.equal(new Set(identities).size, count);
6067
6306
  if (mode) {
6068
- await hosts[0].locator('[data-control="mode"]').selectOption(mode.id);
6307
+ for (const host of hosts) {
6308
+ const tab = host.locator('[data-action="mode:multiplayer"]');
6309
+ if (await tab.count()) await tab.click();
6310
+ }
6311
+ const select = hosts[0].locator('[data-control="mode"]');
6312
+ if (await select.count()) await select.selectOption(mode.id);
6069
6313
  await hosts[0].locator('[data-action="play"]').click();
6070
6314
  await until(() => hosts[0].evaluate(() => !!window.caisualDev.roomCode), 'room code');
6071
6315
  const code = await hosts[0].evaluate(() => window.caisualDev.roomCode);
@@ -6348,17 +6592,14 @@ async function scanClient(files, manifest) {
6348
6592
  const path = primi[campo];
6349
6593
  if (path !== void 0 && !manifest.requires[campo]) warnings.push(avviso(path, nomi[campo], campo));
6350
6594
  }
6351
- if (primi.threads !== void 0 && !manifest.isolated) {
6352
- warnings.push(`client/${primi.threads} seems to use shared memory but caisual.json does not set isolated: true. Shared memory will not be available.`);
6595
+ if (primi.threads !== void 0) {
6596
+ warnings.push(`client/${primi.threads} seems to use shared memory, which is unavailable inside the portal page. Use a build without threads.`);
6353
6597
  }
6354
6598
  return warnings;
6355
6599
  }
6356
6600
 
6357
6601
  // src/caisual.ts
6358
6602
  var DEFAULT_ORIGIN = "https://caisual.com";
6359
- var MAX_FILE_BYTES = 5e7;
6360
- var MAX_VERSION_BYTES = 2e8;
6361
- var MAX_FILES = 2e3;
6362
6603
  var UPLOAD_CONCURRENCY = 4;
6363
6604
  var MAX_RETRIES = 3;
6364
6605
  var ApiError = class extends Error {
@@ -6374,7 +6615,7 @@ var ApiError = class extends Error {
6374
6615
  hints;
6375
6616
  };
6376
6617
  function help() {
6377
- return `Caisual ${"0.16.0"}
6618
+ return `Caisual ${"0.17.0"}
6378
6619
 
6379
6620
  Usage:
6380
6621
  caisual init [--multiplayer | --arcade] [folder]
@@ -6421,7 +6662,7 @@ async function writeNewFile(path, content) {
6421
6662
  }
6422
6663
  }
6423
6664
  async function init(folderArgument, multiplayer, arcade) {
6424
- const root = resolve2(process.cwd(), folderArgument);
6665
+ const root = resolve3(process.cwd(), folderArgument);
6425
6666
  try {
6426
6667
  await fs4.mkdir(join4(root, "client"), { recursive: true });
6427
6668
  } catch {
@@ -6463,9 +6704,11 @@ Put game text in client/i18n/ and localize description in caisual.json. Run cais
6463
6704
  process.stdout.write(`${created ? "Created" : "Kept"} ${absolute}
6464
6705
  `);
6465
6706
  }
6707
+ process.stdout.write(`Installed ${await installSkill(root)}
6708
+ `);
6466
6709
  }
6467
6710
  async function sha256(path) {
6468
- const hash = createHash3("sha256");
6711
+ const hash = createHash4("sha256");
6469
6712
  for await (const chunk of createReadStream(path)) hash.update(chunk);
6470
6713
  return hash.digest("hex");
6471
6714
  }
@@ -6510,12 +6753,12 @@ async function listClientFiles(clientRoot) {
6510
6753
  throw new CliError(2, `${relativePath}: only regular files are supported.`);
6511
6754
  }
6512
6755
  const fileStat = await fs4.stat(absolutePath);
6513
- if (fileStat.size > MAX_FILE_BYTES) {
6514
- throw new CliError(2, `${relativePath}: file is larger than 50 MB (${fileStat.size} bytes).`);
6756
+ if (fileStat.size > MASSIMO_BYTE_FILE) {
6757
+ throw new CliError(2, `${relativePath}: file is larger than 100 MB (${fileStat.size} bytes).`);
6515
6758
  }
6516
6759
  found.push({ path: relativePath, absolutePath, bytes: fileStat.size });
6517
- if (found.length > MAX_FILES) {
6518
- throw new CliError(2, `client/: a version can contain at most ${MAX_FILES} files.`);
6760
+ if (found.length > MASSIMO_FILE) {
6761
+ throw new CliError(2, `client/: a version can contain at most ${MASSIMO_FILE} files.`);
6519
6762
  }
6520
6763
  }
6521
6764
  }
@@ -6524,8 +6767,8 @@ async function listClientFiles(clientRoot) {
6524
6767
  throw new CliError(2, "client/index.html: file not found.");
6525
6768
  }
6526
6769
  const totalBytes = found.reduce((total, file) => total + file.bytes, 0);
6527
- if (totalBytes > MAX_VERSION_BYTES) {
6528
- throw new CliError(2, "client/: a version can contain at most 200 MB in total.");
6770
+ if (totalBytes > MASSIMO_BYTE_TOTALI) {
6771
+ throw new CliError(2, "client/: a version can contain at most 500 MB in total.");
6529
6772
  }
6530
6773
  return await mapLimited(found, UPLOAD_CONCURRENCY, async (file) => ({
6531
6774
  ...file,
@@ -6543,20 +6786,16 @@ async function readServerFile(root, notifyBundle = (bytes) => {
6543
6786
  stat = await fs4.lstat(absolutePath);
6544
6787
  } catch (error) {
6545
6788
  if (error.code === "ENOENT") {
6546
- return { file: null, temporaryDirectory: null, bundled: false };
6789
+ return { file: null, wasm: [], temporaryDirectory: null, bundled: false };
6547
6790
  }
6548
6791
  throw new CliError(2, "server.js: file not readable.");
6549
6792
  }
6550
6793
  if (!stat.isFile()) throw new CliError(2, "server.js: must be a regular file.");
6551
6794
  const result = await bundleServer(root);
6552
- if (!result.bundled) {
6795
+ if (!result.bundled && result.wasm.length === 0) {
6553
6796
  return {
6554
- file: {
6555
- path: "server.js",
6556
- absolutePath,
6557
- bytes: stat.size,
6558
- sha256: await sha256(absolutePath)
6559
- },
6797
+ file: { path: "server.js", absolutePath, bytes: stat.size, sha256: await sha256(absolutePath) },
6798
+ wasm: [],
6560
6799
  temporaryDirectory: null,
6561
6800
  bundled: false
6562
6801
  };
@@ -6566,7 +6805,14 @@ async function readServerFile(root, notifyBundle = (bytes) => {
6566
6805
  try {
6567
6806
  await fs4.writeFile(bundledPath, result.source, "utf8");
6568
6807
  const bytes = Buffer.byteLength(result.source);
6569
- notifyBundle(bytes);
6808
+ if (result.bundled) notifyBundle(bytes);
6809
+ const wasm = [];
6810
+ for (const { path, content, bytes: bytes2, sha256: sha2562 } of result.wasm) {
6811
+ const absolutePath2 = join4(temporaryDirectory, path);
6812
+ await fs4.mkdir(dirname3(absolutePath2), { recursive: true });
6813
+ await fs4.writeFile(absolutePath2, content);
6814
+ wasm.push({ path, absolutePath: absolutePath2, bytes: bytes2, sha256: sha2562 });
6815
+ }
6570
6816
  return {
6571
6817
  file: {
6572
6818
  path: "server.js",
@@ -6574,8 +6820,9 @@ async function readServerFile(root, notifyBundle = (bytes) => {
6574
6820
  bytes,
6575
6821
  sha256: await sha256(bundledPath)
6576
6822
  },
6823
+ wasm,
6577
6824
  temporaryDirectory,
6578
- bundled: true
6825
+ bundled: result.bundled
6579
6826
  };
6580
6827
  } catch (error) {
6581
6828
  await fs4.rm(temporaryDirectory, { recursive: true, force: true });
@@ -6746,13 +6993,10 @@ async function uploadFile(file, target, key, origin) {
6746
6993
  await sleep(100 * 2 ** retry);
6747
6994
  }
6748
6995
  }
6749
- function parseUploads(payload, files, server) {
6750
- const versionId = payload.versionId;
6751
- if (!Number.isSafeInteger(versionId) || versionId <= 0 || !Array.isArray(payload.uploads)) {
6752
- throw new CliError(1, "The portal returned an invalid version response.");
6753
- }
6996
+ function parseFileUploads(uploads, files) {
6997
+ if (!Array.isArray(uploads)) throw new CliError(1, "The portal returned invalid upload targets.");
6754
6998
  const byPath = /* @__PURE__ */ new Map();
6755
- for (const raw of payload.uploads) {
6999
+ for (const raw of uploads) {
6756
7000
  const target = object2(raw);
6757
7001
  if (typeof target?.path !== "string" || typeof target.url !== "string" || target.method !== "PUT") {
6758
7002
  throw new CliError(1, "The portal returned an invalid upload target.");
@@ -6766,6 +7010,14 @@ function parseUploads(payload, files, server) {
6766
7010
  return target;
6767
7011
  });
6768
7012
  if (byPath.size !== files.length) throw new CliError(1, "The portal returned an upload URL for an unknown file.");
7013
+ return targets;
7014
+ }
7015
+ function parseUploads(payload, files, server, wasm = []) {
7016
+ const versionId = payload.versionId;
7017
+ if (!Number.isSafeInteger(versionId) || versionId <= 0 || !Array.isArray(payload.uploads)) {
7018
+ throw new CliError(1, "The portal returned an invalid version response.");
7019
+ }
7020
+ const targets = parseFileUploads(payload.uploads, files);
6769
7021
  const rawServerTarget = object2(payload.serverUpload);
6770
7022
  let serverTarget = null;
6771
7023
  if (server !== null) {
@@ -6776,7 +7028,9 @@ function parseUploads(payload, files, server) {
6776
7028
  } else if (payload.serverUpload !== void 0 && payload.serverUpload !== null) {
6777
7029
  throw new CliError(1, "The portal returned an unexpected upload URL for server.js.");
6778
7030
  }
7031
+ const wasmTargets = parseFileUploads(payload.serverWasmUploads ?? [], wasm);
6779
7032
  return {
7033
+ wasmTargets,
6780
7034
  versionId,
6781
7035
  n: Number.isSafeInteger(payload.n) ? payload.n : null,
6782
7036
  targets,
@@ -6802,6 +7056,7 @@ async function readManifest(root, warn) {
6802
7056
  throw new CliError(2, `caisual.json is not valid:
6803
7057
  ${result.errori.map((error) => `- ${error}`).join("\n")}`);
6804
7058
  }
7059
+ for (const warning of avvisiManifest(value)) warn?.(warning);
6805
7060
  return result.manifest;
6806
7061
  }
6807
7062
  async function captureGameError(errors, operation) {
@@ -6911,7 +7166,7 @@ function formatBytes(bytes) {
6911
7166
  return `${value} ${unit}`;
6912
7167
  }
6913
7168
  async function check(folderArgument, json) {
6914
- const report = await checkGame(resolve2(process.cwd(), folderArgument));
7169
+ const report = await checkGame(resolve3(process.cwd(), folderArgument));
6915
7170
  if (json) {
6916
7171
  process.stdout.write(`${JSON.stringify(report, null, 2)}
6917
7172
  `);
@@ -6947,7 +7202,7 @@ async function check(folderArgument, json) {
6947
7202
  if (!report.ok) process.exitCode = 2;
6948
7203
  }
6949
7204
  async function publish(folderArgument) {
6950
- const root = resolve2(process.cwd(), folderArgument);
7205
+ const root = resolve3(process.cwd(), folderArgument);
6951
7206
  const report = await checkGame(root);
6952
7207
  for (const warning of report.warnings) process.stderr.write(`Warning: ${warning}
6953
7208
  `);
@@ -6978,11 +7233,15 @@ async function publish(folderArgument) {
6978
7233
  manifest,
6979
7234
  files: declared,
6980
7235
  ...server === null ? {} : {
6981
- server: { bytes: server.bytes, sha256: server.sha256 }
7236
+ server: {
7237
+ bytes: server.bytes,
7238
+ sha256: server.sha256,
7239
+ ...serverResult.wasm.length === 0 ? {} : { wasm: serverResult.wasm.map(({ path, bytes, sha256: sha2562 }) => ({ path, bytes, sha256: sha2562 })) }
7240
+ }
6982
7241
  }
6983
7242
  })
6984
7243
  });
6985
- const version = parseUploads(opened, files, server);
7244
+ const version = parseUploads(opened, files, server, serverResult.wasm);
6986
7245
  const caricamenti = files.map((file, index) => ({
6987
7246
  file,
6988
7247
  target: version.targets[index]
@@ -6993,6 +7252,7 @@ async function publish(folderArgument) {
6993
7252
  target: { path: "server.js", ...version.serverTarget }
6994
7253
  });
6995
7254
  }
7255
+ serverResult.wasm.forEach((file, index) => caricamenti.push({ file, target: version.wasmTargets[index] }));
6996
7256
  await mapLimited(caricamenti, UPLOAD_CONCURRENCY, async ({ file, target }) => {
6997
7257
  await uploadFile(file, target, key, origin);
6998
7258
  });
@@ -7017,7 +7277,7 @@ async function publish(folderArgument) {
7017
7277
  }
7018
7278
  }
7019
7279
  async function gameIdFromTarget(target) {
7020
- const path = resolve2(process.cwd(), target);
7280
+ const path = resolve3(process.cwd(), target);
7021
7281
  try {
7022
7282
  if ((await fs4.stat(path)).isDirectory()) return (await readManifest(path)).id;
7023
7283
  } catch (error) {
@@ -7081,8 +7341,19 @@ async function gameVersions(target, to) {
7081
7341
  `);
7082
7342
  }
7083
7343
  }
7084
- async function installSkill() {
7085
- const root = process.cwd();
7344
+ var SITO = "https://caisual.com";
7345
+ function guidesSection() {
7346
+ const guides = JSON.parse('[{"slug":"overview","title":"What Caisual is","description":"A free home for small browser games, with multiplayer built in."},{"slug":"quick-start","title":"Quick start","description":"From an empty folder to a permanent link."},{"slug":"overlay","title":"The standard overlay","description":"You make the game. Caisual draws the menu, the lobby, the invites and the results on top of it."},{"slug":"manifest","title":"The manifest","description":"caisual.json is everything the portal knows about your game."},{"slug":"rooms","title":"Rooms","description":"Server-owned state for up to 24 players."},{"slug":"voice","title":"Voice","description":"Room voice in one manifest field, with gains owned by your server."},{"slug":"matchmaking","title":"Matchmaking","description":"One call puts strangers who asked for the same thing in the same room."},{"slug":"spectators","title":"Spectators","description":"Up to 100 people watch a room on a delay, with no change to your server."},{"slug":"friends-and-parties","title":"Friends","description":"Friends, parties and one-click join, with no API to call."},{"slug":"player-identity","title":"Identity","description":"Every player has a stable id before your game draws a frame."},{"slug":"saves-and-leaderboards","title":"Saves and scores","description":"Cloud saves, leaderboards and one daily seed, single player included."},{"slug":"local-development","title":"Local dev","description":"The production handshake, multiple guests and network simulation."},{"slug":"device-requirements","title":"Devices","description":"Declare what you need, then read what the machine has."},{"slug":"versions","title":"Game versions","description":"Updates, rooms, rollback and compatible player data."},{"slug":"limits-and-rules","title":"Limits","description":"Every ceiling, every rule, in one page."},{"slug":"faq","title":"FAQ","description":"Short answers, in one place."}]');
7347
+ return [
7348
+ "# Guides",
7349
+ "",
7350
+ `The two documents above are complete for publishing and for the kit API. Each topic below has its own guide, in Markdown, at the address shown; the index is at ${SITO}/llms.txt. Fetch a guide before working on its topic.`,
7351
+ "",
7352
+ ...guides.map((guide) => `- [${guide.title}](${SITO}/docs/${guide.slug}.md): ${guide.description}`),
7353
+ ""
7354
+ ].join("\n");
7355
+ }
7356
+ async function installSkill(root) {
7086
7357
  const skillPath = join4(root, ".claude", "skills", "caisual", "SKILL.md");
7087
7358
  const skill = `---
7088
7359
  name: caisual
@@ -7092,7 +7363,8 @@ description: Create and publish a browser game on Caisual, with player identity,
7092
7363
  ${publish_default.trim()}
7093
7364
 
7094
7365
  ${kit_default.trim()}
7095
- `;
7366
+
7367
+ ${guidesSection()}`;
7096
7368
  await fs4.mkdir(join4(root, ".claude", "skills", "caisual"), { recursive: true });
7097
7369
  let currentSkill = null;
7098
7370
  try {
@@ -7109,12 +7381,14 @@ ${kit_default.trim()}
7109
7381
  if (error.code !== "ENOENT") throw error;
7110
7382
  }
7111
7383
  if (!/^## Caisual\s*$/m.test(agents)) {
7112
- const section = "## Caisual\nRead `.claude/skills/caisual/SKILL.md` before creating or publishing a Caisual game.\nUse the current guides at https://caisual.com/publish.md and https://caisual.com/kit.md.\n";
7384
+ const section = `## Caisual
7385
+ Read \`.claude/skills/caisual/SKILL.md\` before creating or publishing a Caisual game.
7386
+ Use the current guides at ${SITO}/publish.md and ${SITO}/kit.md; the index of every guide is at ${SITO}/llms.txt.
7387
+ `;
7113
7388
  const separator = agents === "" ? "" : agents.endsWith("\n\n") ? "" : agents.endsWith("\n") ? "\n" : "\n\n";
7114
7389
  await fs4.writeFile(agentsPath, `${agents}${separator}${section}`, "utf8");
7115
7390
  }
7116
- process.stdout.write(`Installed ${skillPath}
7117
- `);
7391
+ return skillPath;
7118
7392
  }
7119
7393
  async function run(argumentsList) {
7120
7394
  const [command, ...argumentsAfterCommand] = argumentsList;
@@ -7123,7 +7397,7 @@ async function run(argumentsList) {
7123
7397
  return;
7124
7398
  }
7125
7399
  if (command === "--version" || command === "-V") {
7126
- process.stdout.write(`${"0.16.0"}
7400
+ process.stdout.write(`${"0.17.0"}
7127
7401
  `);
7128
7402
  return;
7129
7403
  }
@@ -7262,7 +7536,8 @@ async function run(argumentsList) {
7262
7536
  }
7263
7537
  if (command === "skill") {
7264
7538
  if (argumentsAfterCommand.length > 0) throw new CliError(1, "Usage: caisual skill");
7265
- await installSkill();
7539
+ process.stdout.write(`Installed ${await installSkill(process.cwd())}
7540
+ `);
7266
7541
  return;
7267
7542
  }
7268
7543
  throw new CliError(1, `Unknown command: ${command}