@nano133/nanoterra 0.7.0 → 0.8.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 (96) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +206 -10
  3. package/ai/SPEC.md +51 -2
  4. package/dist/admin/adapters/d1.d.ts +1 -0
  5. package/dist/admin/adapters/d1.js +6 -4
  6. package/dist/admin/adapters/firestore.d.ts +1 -0
  7. package/dist/admin/adapters/firestore.js +7 -5
  8. package/dist/admin/conformance.d.ts +1 -1
  9. package/dist/admin/conformance.js +12 -4
  10. package/dist/admin/handler.d.ts +1 -1
  11. package/dist/admin/handler.js +2 -2
  12. package/dist/admin/index.d.ts +2 -2
  13. package/dist/admin/index.js +1 -1
  14. package/dist/admin/memory.js +3 -1
  15. package/dist/admin/panel/gameCard.js +13 -10
  16. package/dist/admin/panel/icons.d.ts +17 -0
  17. package/dist/admin/panel/icons.js +3 -0
  18. package/dist/admin/panel/overview.js +1 -1
  19. package/dist/admin/store.d.ts +2 -1
  20. package/dist/admin/switches.d.ts +13 -3
  21. package/dist/admin/switches.js +25 -6
  22. package/dist/character/create.js +6 -2
  23. package/dist/character/layer.d.ts +14 -0
  24. package/dist/character/layer.js +10 -0
  25. package/dist/emotes/clip.d.ts +79 -0
  26. package/dist/emotes/clip.js +165 -0
  27. package/dist/emotes/clips.d.ts +3 -0
  28. package/dist/emotes/clips.js +2 -0
  29. package/dist/emotes/events.d.ts +22 -0
  30. package/dist/emotes/events.js +19 -0
  31. package/dist/emotes/icons.d.ts +1 -0
  32. package/dist/emotes/icons.js +6 -0
  33. package/dist/emotes/index.d.ts +8 -0
  34. package/dist/emotes/index.js +11 -0
  35. package/dist/emotes/list.d.ts +123 -0
  36. package/dist/emotes/list.js +23 -0
  37. package/dist/emotes/options.d.ts +16 -0
  38. package/dist/emotes/options.js +34 -0
  39. package/dist/emotes/panel/index.d.ts +3 -0
  40. package/dist/emotes/panel/index.js +9 -0
  41. package/dist/emotes/panel/mount.d.ts +56 -0
  42. package/dist/emotes/panel/mount.js +360 -0
  43. package/dist/emotes/player.d.ts +37 -0
  44. package/dist/emotes/player.js +101 -0
  45. package/dist/emotes/wheel.d.ts +42 -0
  46. package/dist/emotes/wheel.js +110 -0
  47. package/dist/friends/panel/mount.d.ts +3 -3
  48. package/dist/friends/panel/mount.js +46 -12
  49. package/dist/options/common.d.ts +88 -0
  50. package/dist/options/common.js +59 -0
  51. package/dist/options/core.d.ts +130 -0
  52. package/dist/options/core.js +420 -0
  53. package/dist/options/index.d.ts +7 -0
  54. package/dist/options/index.js +24 -0
  55. package/dist/options/panel/index.d.ts +2 -0
  56. package/dist/options/panel/index.js +10 -0
  57. package/dist/options/panel/mount.d.ts +30 -0
  58. package/dist/options/panel/mount.js +378 -0
  59. package/dist/options/reflect.d.ts +5 -0
  60. package/dist/options/reflect.js +27 -0
  61. package/package.json +12 -4
  62. package/src/admin/adapters/d1.ts +5 -4
  63. package/src/admin/adapters/firestore.ts +6 -5
  64. package/src/admin/conformance.ts +14 -5
  65. package/src/admin/handler.ts +2 -2
  66. package/src/admin/index.ts +2 -2
  67. package/src/admin/memory.ts +2 -1
  68. package/src/admin/panel/gameCard.ts +13 -9
  69. package/src/admin/panel/icons.ts +3 -0
  70. package/src/admin/panel/overview.ts +1 -1
  71. package/src/admin/store.ts +2 -2
  72. package/src/admin/switches.ts +29 -12
  73. package/src/character/create.ts +6 -2
  74. package/src/character/layer.ts +21 -0
  75. package/src/emotes/clip.ts +204 -0
  76. package/src/emotes/clips.ts +7 -0
  77. package/src/emotes/events.ts +28 -0
  78. package/src/emotes/icons.ts +7 -0
  79. package/src/emotes/icons.webp +0 -0
  80. package/src/emotes/index.ts +16 -0
  81. package/src/emotes/list.ts +33 -0
  82. package/src/emotes/options.ts +43 -0
  83. package/src/emotes/panel/index.ts +11 -0
  84. package/src/emotes/panel/mount.ts +390 -0
  85. package/src/emotes/panel/style.css +415 -0
  86. package/src/emotes/player.ts +131 -0
  87. package/src/emotes/wheel.ts +131 -0
  88. package/src/friends/panel/mount.ts +47 -13
  89. package/src/friends/panel/style.css +26 -0
  90. package/src/options/common.ts +65 -0
  91. package/src/options/core.ts +465 -0
  92. package/src/options/index.ts +41 -0
  93. package/src/options/panel/index.ts +12 -0
  94. package/src/options/panel/mount.ts +418 -0
  95. package/src/options/panel/style.css +528 -0
  96. package/src/options/reflect.ts +28 -0
@@ -22,7 +22,6 @@ export function mountFriends(root, options) {
22
22
  return NOTHING;
23
23
  const n = ++mounts;
24
24
  const origin = (options.origin ?? "https://nano133.com").replace(/\/+$/, "");
25
- const ask = options.confirm ?? ((q) => window.confirm(q));
26
25
  let where = options.where === undefined ? null : whereText(options.where);
27
26
  let isOpen = false;
28
27
  let destroyed = false;
@@ -34,7 +33,12 @@ export function mountFriends(root, options) {
34
33
  const title = h("h2", { class: "n133f-title", id: `n133f-title-${n}` }, "Friends");
35
34
  const closeKey = h("button", { class: "n133f-close", type: "button", "aria-label": "Close friends" }, icon("close", 20));
36
35
  const body = h("div", { class: "n133f-body" });
37
- const panel = h("section", { class: `n133f-panel n133f-${options.mode}`, id: `n133f-panel-${n}`, role: "dialog", "aria-labelledby": `n133f-title-${n}`, "data-open": "false" }, h("header", { class: "n133f-head" }, title, closeKey), body);
36
+ const question = h("p", { class: "n133f-ask-q", id: `n133f-ask-${n}` });
37
+ const yesKey = h("button", { class: "n133f-btn n133f-primary", type: "button" });
38
+ const noKey = h("button", { class: "n133f-btn", type: "button" }, "Cancel");
39
+ const askBox = h("div", { class: "n133f-ask", role: "alertdialog", "aria-labelledby": `n133f-ask-${n}` }, question, h("div", { class: "n133f-ask-keys" }, yesKey, noKey));
40
+ askBox.hidden = true;
41
+ const panel = h("section", { class: `n133f-panel n133f-${options.mode}`, id: `n133f-panel-${n}`, role: "dialog", "aria-labelledby": `n133f-title-${n}`, "data-open": "false" }, h("header", { class: "n133f-head" }, title, closeKey), askBox, body);
38
42
  const wrap = h("div", { class: "n133-friends" }, key, panel);
39
43
  root.append(wrap);
40
44
  /** The requests (the thing to do) when there are any, else the friends online (another colour). The key's name says both. */
@@ -60,6 +64,7 @@ export function mountFriends(root, options) {
60
64
  const close = () => {
61
65
  if (destroyed || !isOpen)
62
66
  return;
67
+ answer?.(false);
63
68
  isOpen = false;
64
69
  panel.dataset.open = "false";
65
70
  key.setAttribute("aria-expanded", "false");
@@ -68,8 +73,34 @@ export function mountFriends(root, options) {
68
73
  };
69
74
  key.addEventListener("click", () => (isOpen ? close() : open()));
70
75
  closeKey.addEventListener("click", close);
76
+ /** The question in the panel: one at a time (a new one answers the old with no), and closing the panel answers no. */
77
+ let answer = null;
78
+ const askInPanel = (q, yesWord) => new Promise((resolve) => {
79
+ answer?.(false);
80
+ question.textContent = q;
81
+ yesKey.textContent = yesWord;
82
+ askBox.hidden = false;
83
+ answer = (yes) => {
84
+ answer = null;
85
+ askBox.hidden = true;
86
+ resolve(yes);
87
+ if (isOpen && !destroyed)
88
+ closeKey.focus();
89
+ };
90
+ open();
91
+ yesKey.focus();
92
+ });
93
+ yesKey.addEventListener("click", () => answer?.(true));
94
+ noKey.addEventListener("click", () => answer?.(false));
95
+ const ask = (q, yesWord) => (options.confirm ? Promise.resolve(options.confirm(q)) : askInPanel(q, yesWord));
96
+ cleanup.push(() => answer?.(false));
71
97
  const onKey = (e) => {
72
- if (e.key === "Escape" && isOpen)
98
+ if (e.key !== "Escape" || !isOpen)
99
+ return;
100
+ // Escape answers an open question first, then closes the panel.
101
+ if (answer)
102
+ answer(false);
103
+ else
73
104
  close();
74
105
  };
75
106
  document.addEventListener("keydown", onKey);
@@ -132,12 +163,15 @@ export function mountFriends(root, options) {
132
163
  else if (m.type === "badge")
133
164
  setBadge(m);
134
165
  else if (m.type === "join") {
135
- if (!ask("Leave this game to join your friend?"))
136
- return;
137
- if (options.onJoin)
138
- options.onJoin(m.url);
139
- else
140
- window.location.assign(m.url);
166
+ const url = m.url;
167
+ void ask("Leave this game to join your friend?", "Join").then((yes) => {
168
+ if (!yes || destroyed)
169
+ return;
170
+ if (options.onJoin)
171
+ options.onJoin(url);
172
+ else
173
+ window.location.assign(url);
174
+ });
141
175
  }
142
176
  // "signin": the window needs a tap (Storage Access) inside itself. The panel stays as it is; the kit does nothing else.
143
177
  };
@@ -228,10 +262,10 @@ function ownList(body, options, whereNow, setBadge, ask) {
228
262
  if (p.invite && options.onJoin)
229
263
  keys.push(btn("Join", () => options.onJoin(p.invite.where, { name: p.name }), "n133f-primary"));
230
264
  const here = whereNow();
231
- if (here)
265
+ if (here && options.onJoin)
232
266
  keys.push(btn("Invite", () => void act({ op: "invite", edge: p.edge, where: here }, `Invited ${p.name}.`)));
233
- keys.push(btn("Remove", () => ask(`Remove ${p.name} from your friends?`) && void act({ op: "remove", edge: p.edge })));
234
- keys.push(btn("Block", () => ask(`Block ${p.name}? They will not see you or be able to ask you again.`) && void act({ op: "block", edge: p.edge }), "n133f-quiet"));
267
+ keys.push(btn("Remove", () => void ask(`Remove ${p.name} from your friends?`, "Remove").then((yes) => (yes ? act({ op: "remove", edge: p.edge }) : undefined))));
268
+ keys.push(btn("Block", () => void ask(`Block ${p.name}? They will not see you or be able to ask you again.`, "Block").then((yes) => (yes ? act({ op: "block", edge: p.edge }) : undefined)), "n133f-quiet"));
235
269
  }
236
270
  if (p.state === "outgoing")
237
271
  keys.push(btn("Cancel", () => void act({ op: "decline", edge: p.edge })));
@@ -0,0 +1,88 @@
1
+ import type { OptionDef, RangeDef } from "./core.js";
2
+ export declare const COMMON_OPTIONS: {
3
+ /** 0 to 100 (%), every sound. A game multiplies it into the others; the square of it is a fair loudness curve (`level ** 2`). */
4
+ masterVolume: RangeDef;
5
+ /** 0 to 100 (%), the music only. */
6
+ musicVolume: RangeDef;
7
+ /** 0 to 100 (%), every sound but the music. */
8
+ effectsVolume: RangeDef;
9
+ /** 10 to 100, how fast a drag or the mouse turns the camera; 50 is the game's own speed. */
10
+ cameraSensitivity: {
11
+ kind: "range";
12
+ key: string;
13
+ label: string;
14
+ tab: "basic";
15
+ group: string;
16
+ min: number;
17
+ max: number;
18
+ step: number;
19
+ default: number;
20
+ };
21
+ /** Up and down of the camera swapped. */
22
+ invertLook: {
23
+ kind: "toggle";
24
+ key: string;
25
+ label: string;
26
+ help: string;
27
+ tab: "advanced";
28
+ group: string;
29
+ default: false;
30
+ };
31
+ /** "left" or "right": the side of a touch screen the move stick is on (the camera drag is on the other). Shown on a phone and a tablet only. */
32
+ stickSide: {
33
+ kind: "choice";
34
+ key: string;
35
+ label: string;
36
+ help: string;
37
+ tab: "basic";
38
+ group: string;
39
+ choices: {
40
+ value: string;
41
+ label: string;
42
+ }[];
43
+ default: string;
44
+ devices: ("phone" | "tablet")[];
45
+ };
46
+ /** Fewer shakes, swings and flashes. On by default when the device asks for less motion. */
47
+ reducedMotion: {
48
+ kind: "toggle";
49
+ key: string;
50
+ label: string;
51
+ help: string;
52
+ tab: "advanced";
53
+ group: string;
54
+ default: {
55
+ default: false;
56
+ reducedMotion: true;
57
+ };
58
+ };
59
+ /** "low", "medium" or "high": the game decides what each draws. Medium on a phone. */
60
+ quality: {
61
+ kind: "choice";
62
+ key: string;
63
+ label: string;
64
+ tab: "advanced";
65
+ group: string;
66
+ choices: {
67
+ value: string;
68
+ label: string;
69
+ }[];
70
+ default: {
71
+ phone: string;
72
+ default: string;
73
+ };
74
+ };
75
+ /** A small frames-a-second counter on the screen. */
76
+ showFps: {
77
+ kind: "toggle";
78
+ key: string;
79
+ label: string;
80
+ tab: "advanced";
81
+ group: string;
82
+ default: false;
83
+ };
84
+ };
85
+ export type CommonKey = keyof typeof COMMON_OPTIONS;
86
+ export declare const COMMON_KEYS: CommonKey[];
87
+ /** The common defs for these keys, in the order given (a copy: change it freely). */
88
+ export declare function common(...keys: CommonKey[]): OptionDef[];
@@ -0,0 +1,59 @@
1
+ // The settings most games have, ready to opt into by key: `common("masterVolume", "musicVolume", "quality")`. They are plain defs: the game reads them and
2
+ // does what they say (the kit plays no sound and draws no frame). A game changes one by spreading it: `{ ...COMMON_OPTIONS.quality, default: "low" }`.
3
+ // The words and ranges follow nano133's island and Echo Island (Wes, 2026-10-30: Sound effects and Music sliders; the island's Settings rows).
4
+ const volume = (key, label, help) => ({ kind: "range", key, label, ...(help ? { help } : {}), tab: "basic", group: "Sound", min: 0, max: 100, step: 5, unit: "%", default: 100 });
5
+ export const COMMON_OPTIONS = {
6
+ /** 0 to 100 (%), every sound. A game multiplies it into the others; the square of it is a fair loudness curve (`level ** 2`). */
7
+ masterVolume: volume("masterVolume", "Volume"),
8
+ /** 0 to 100 (%), the music only. */
9
+ musicVolume: volume("musicVolume", "Music"),
10
+ /** 0 to 100 (%), every sound but the music. */
11
+ effectsVolume: volume("effectsVolume", "Sound effects"),
12
+ /** 10 to 100, how fast a drag or the mouse turns the camera; 50 is the game's own speed. */
13
+ cameraSensitivity: { kind: "range", key: "cameraSensitivity", label: "Camera speed", tab: "basic", group: "Camera", min: 10, max: 100, step: 5, default: 50 },
14
+ /** Up and down of the camera swapped. */
15
+ invertLook: { kind: "toggle", key: "invertLook", label: "Invert look", help: "Up and down are swapped", tab: "advanced", group: "Camera", default: false },
16
+ /** "left" or "right": the side of a touch screen the move stick is on (the camera drag is on the other). Shown on a phone and a tablet only. */
17
+ stickSide: {
18
+ kind: "choice",
19
+ key: "stickSide",
20
+ label: "Move stick",
21
+ help: "The side of the screen that moves you",
22
+ tab: "basic",
23
+ group: "Controls",
24
+ choices: [
25
+ { value: "left", label: "Left" },
26
+ { value: "right", label: "Right" },
27
+ ],
28
+ default: "left",
29
+ devices: ["phone", "tablet"],
30
+ },
31
+ /** Fewer shakes, swings and flashes. On by default when the device asks for less motion. */
32
+ reducedMotion: { kind: "toggle", key: "reducedMotion", label: "Less motion", help: "Fewer shakes, swings and flashes", tab: "advanced", group: "Comfort", default: { default: false, reducedMotion: true } },
33
+ /** "low", "medium" or "high": the game decides what each draws. Medium on a phone. */
34
+ quality: {
35
+ kind: "choice",
36
+ key: "quality",
37
+ label: "Quality",
38
+ tab: "advanced",
39
+ group: "Graphics",
40
+ choices: [
41
+ { value: "low", label: "Low" },
42
+ { value: "medium", label: "Medium" },
43
+ { value: "high", label: "High" },
44
+ ],
45
+ default: { phone: "medium", default: "high" },
46
+ },
47
+ /** A small frames-a-second counter on the screen. */
48
+ showFps: { kind: "toggle", key: "showFps", label: "Show frame rate", tab: "advanced", group: "Graphics", default: false },
49
+ };
50
+ export const COMMON_KEYS = Object.keys(COMMON_OPTIONS);
51
+ /** The common defs for these keys, in the order given (a copy: change it freely). */
52
+ export function common(...keys) {
53
+ return keys.map((k) => {
54
+ const def = COMMON_OPTIONS[k];
55
+ if (!def)
56
+ throw new TypeError(`common: "${String(k)}" is not a common setting (${COMMON_KEYS.join(", ")})`);
57
+ return { ...def };
58
+ });
59
+ }
@@ -0,0 +1,130 @@
1
+ export type Tab = "basic" | "advanced";
2
+ export type DeviceClass = "phone" | "tablet" | "desktop";
3
+ export declare const DEVICE_CLASSES: readonly DeviceClass[];
4
+ /** A default for each device class. `default` is for a class left out; `reducedMotion` wins while the device asks for less motion. */
5
+ export type DeviceDefault<V> = {
6
+ phone?: V;
7
+ tablet?: V;
8
+ desktop?: V;
9
+ reducedMotion?: V;
10
+ default: V;
11
+ };
12
+ type DefBase<V> = {
13
+ /** The key the game reads. Letters, digits, "_" and "-", in parts joined by dots; a module's section starts with its own name ("emotes.show"). */
14
+ key: string;
15
+ /** The words on the row: short, the player reads them ("Music", "Invert look"). */
16
+ label: string;
17
+ /** One short line under the label. */
18
+ help?: string;
19
+ tab: Tab;
20
+ /** The heading the row goes under, in the order the groups first appear. Rows with no group come first. */
21
+ group?: string;
22
+ default: V | DeviceDefault<V>;
23
+ /** Show the row only on these device classes (a stick's side means nothing with a mouse). The store keeps the value on every device. */
24
+ devices?: readonly DeviceClass[];
25
+ };
26
+ export type ToggleDef = DefBase<boolean> & {
27
+ kind: "toggle";
28
+ };
29
+ export type ChoiceValue = string | number;
30
+ export type ChoiceDef = DefBase<ChoiceValue> & {
31
+ kind: "choice";
32
+ /** At least two, each value once. */
33
+ choices: readonly {
34
+ value: ChoiceValue;
35
+ label: string;
36
+ }[];
37
+ /** "buttons": a row of keys; "list": a drop-down. Left out: buttons for at most 4 short labels (10 characters), else a list. */
38
+ display?: "buttons" | "list";
39
+ };
40
+ export type RangeDef = DefBase<number> & {
41
+ kind: "range";
42
+ min: number;
43
+ max: number;
44
+ step: number;
45
+ /** Put after the number as written: "%" gives "80%", " s" gives "3 s". */
46
+ unit?: string;
47
+ };
48
+ /** A key binding: the value is a `KeyboardEvent.code` ("KeyE", "Space", "ArrowUp"): the key's place, the same on every keyboard layout. */
49
+ export type KeyDef = DefBase<string> & {
50
+ kind: "key";
51
+ };
52
+ export type OptionDef = ToggleDef | ChoiceDef | RangeDef | KeyDef;
53
+ export type OptionValue = boolean | ChoiceValue;
54
+ type Kept = Pick<Storage, "getItem" | "setItem" | "removeItem">;
55
+ export type OptionsInit = {
56
+ /** The game's id: the storage key is `n133-options:<game>`. */
57
+ game: string;
58
+ defs: readonly OptionDef[];
59
+ /** The device class (default: `detectDevice`). */
60
+ device?: () => DeviceClass;
61
+ /** Whether the device asks for less motion (default: `prefers-reduced-motion: reduce`, followed live). A game's own function is read at start and on `redetect()`. */
62
+ reducedMotion?: () => boolean;
63
+ /** Where the choices are kept (default: the page's localStorage). null keeps them for the life of the page only. */
64
+ storage?: Kept | null;
65
+ /**
66
+ * Changes what was kept before it is read: a renamed key, a new scale, or a game's old save under another key (read it from `storage` when `stored` is
67
+ * empty, and remove it). Gets a copy of the stored object ({} when there is none); returns the object to read. What it returns is checked like any kept value.
68
+ */
69
+ migrate?: (stored: Record<string, unknown>, storage: Kept | null) => Record<string, unknown> | null | undefined;
70
+ };
71
+ export type OptionsStore = {
72
+ readonly game: string;
73
+ /** The device class the defaults are resolved for. */
74
+ readonly device: DeviceClass;
75
+ /** The value now. Throws for a key no def has. */
76
+ get<T extends OptionValue = OptionValue>(key: string): T;
77
+ /**
78
+ * Sets a value, keeps it (only if it differs from the default) and tells the listeners. A range's number is put on its nearest step inside min and max;
79
+ * a key binding that another binding has swaps with it. Throws a TypeError for an unknown key, a value of the wrong type, a number that is not finite,
80
+ * an unknown choice and a key binding that is not a `KeyboardEvent.code`.
81
+ */
82
+ set(key: string, value: OptionValue): void;
83
+ /** One setting back to its default; with no key, every setting. */
84
+ reset(key?: string): void;
85
+ /** Every value now, by key (a copy). */
86
+ values(): Record<string, OptionValue>;
87
+ /** The defs, in order (the game's first, then each section in the order it was added). */
88
+ defs(): readonly OptionDef[];
89
+ /** The def of a key, or undefined. */
90
+ def(key: string): OptionDef | undefined;
91
+ /** This device's default for a key now. Throws for a key no def has. */
92
+ defaultOf(key: string): OptionValue;
93
+ /** True when the key (or, with no key, every setting) is at its default. */
94
+ isDefault(key?: string): boolean;
95
+ /** Hears each change of a value (set, reset, a default that moved): the key and the new value. Returns the call that stops it. */
96
+ subscribe(fn: (key: string, value: OptionValue) => void): () => void;
97
+ /** Hears each add(): a panel draws the new rows. Returns the call that stops it. */
98
+ subscribeDefs(fn: () => void): () => void;
99
+ /** Adds defs later (a module's section). A key that is already there throws, and nothing is added. */
100
+ add(defs: readonly OptionDef[]): void;
101
+ /** Reads the device class and reduced motion again and moves the defaults: a setting the player never changed follows, and its listeners hear it. */
102
+ redetect(): void;
103
+ };
104
+ export declare const OPTIONS_PREFIX = "n133-options:";
105
+ /** The localStorage key of a game's choices. */
106
+ export declare const optionsKey: (game: string) => string;
107
+ /**
108
+ * The device class: a coarse pointer (a finger) and a short side under 600 px is a phone; a coarse pointer otherwise is a tablet; anything else is a
109
+ * desktop. The short side is the screen's, so turning a phone does not change it. With no matchMedia (Node), a desktop.
110
+ */
111
+ export declare function detectDevice(): DeviceClass;
112
+ /** `prefers-reduced-motion: reduce`; false where it cannot be read. */
113
+ export declare const prefersReducedMotion: () => boolean;
114
+ /** The default a def has on a device class (and with reduced motion asked for or not). */
115
+ export declare function resolveDefault<V extends OptionValue>(def: {
116
+ default: V | DeviceDefault<V>;
117
+ }, device: DeviceClass, reducedMotion?: boolean): V;
118
+ /** A number on a range's nearest step from min, inside min and max. */
119
+ export declare function snapToRange(def: Pick<RangeDef, "min" | "max" | "step">, v: number): number;
120
+ /** The words for a `KeyboardEvent.code` on a key cap: "KeyE" is "E", "Digit1" is "1", "ArrowUp" is "↑", "ShiftLeft" is "Shift". */
121
+ export declare function keyLabel(code: string): string;
122
+ /** The first thing wrong with a def, as a sentence, or null. `createOptions` and `add` throw it. */
123
+ export declare function defProblem(def: OptionDef): string | null;
124
+ /** Makes the store, reads what this browser kept for the game, and drops what does not fit. Throws for a bad def or a key that is there twice. */
125
+ export declare function createOptions(init: OptionsInit): OptionsStore;
126
+ /** A module's section: its defs with the module's name before each key ("show" in "emotes" is "emotes.show"), and its group when a def has none. */
127
+ export declare function optionSection(name: string, defs: readonly OptionDef[], group?: string): OptionDef[];
128
+ /** A key binding def: the key `keys.<action>`, in Advanced under "Keys", shown on a desktop only (a touch screen has no keys to bind). */
129
+ export declare function keyBinding(action: string, label: string, code: string, over?: Partial<Omit<KeyDef, "kind">>): KeyDef;
130
+ export {};