@nano133/nanoterra 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE +3 -0
- package/README.md +212 -17
- package/ai/SPEC.md +51 -2
- package/dist/admin/adapters/d1.d.ts +1 -0
- package/dist/admin/adapters/d1.js +6 -4
- package/dist/admin/adapters/firestore.d.ts +1 -0
- package/dist/admin/adapters/firestore.js +7 -5
- package/dist/admin/conformance.d.ts +1 -1
- package/dist/admin/conformance.js +12 -4
- package/dist/admin/handler.d.ts +1 -1
- package/dist/admin/handler.js +2 -2
- package/dist/admin/index.d.ts +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/memory.js +3 -1
- package/dist/admin/panel/dom.d.ts +2 -0
- package/dist/admin/panel/dom.js +6 -0
- package/dist/admin/panel/gameCard.js +13 -10
- package/dist/admin/panel/icons.d.ts +17 -0
- package/dist/admin/panel/icons.js +3 -0
- package/dist/admin/panel/overview.js +1 -1
- package/dist/admin/store.d.ts +2 -1
- package/dist/admin/switches.d.ts +13 -3
- package/dist/admin/switches.js +25 -6
- package/dist/character/create.js +6 -2
- package/dist/character/layer.d.ts +14 -0
- package/dist/character/layer.js +10 -0
- package/dist/emotes/clip.d.ts +79 -0
- package/dist/emotes/clip.js +165 -0
- package/dist/emotes/clips.d.ts +3 -0
- package/dist/emotes/clips.js +2 -0
- package/dist/emotes/events.d.ts +22 -0
- package/dist/emotes/events.js +19 -0
- package/dist/emotes/icons.d.ts +1 -0
- package/dist/emotes/icons.js +6 -0
- package/dist/emotes/index.d.ts +8 -0
- package/dist/emotes/index.js +11 -0
- package/dist/emotes/list.d.ts +123 -0
- package/dist/emotes/list.js +23 -0
- package/dist/emotes/options.d.ts +16 -0
- package/dist/emotes/options.js +34 -0
- package/dist/emotes/panel/index.d.ts +3 -0
- package/dist/emotes/panel/index.js +9 -0
- package/dist/emotes/panel/mount.d.ts +61 -0
- package/dist/emotes/panel/mount.js +364 -0
- package/dist/emotes/player.d.ts +37 -0
- package/dist/emotes/player.js +101 -0
- package/dist/emotes/wheel.d.ts +42 -0
- package/dist/emotes/wheel.js +110 -0
- package/dist/friends/panel/mount.d.ts +5 -0
- package/dist/friends/panel/mount.js +5 -2
- package/dist/options/common.d.ts +88 -0
- package/dist/options/common.js +59 -0
- package/dist/options/core.d.ts +130 -0
- package/dist/options/core.js +420 -0
- package/dist/options/index.d.ts +7 -0
- package/dist/options/index.js +24 -0
- package/dist/options/panel/index.d.ts +2 -0
- package/dist/options/panel/index.js +10 -0
- package/dist/options/panel/mount.d.ts +35 -0
- package/dist/options/panel/mount.js +380 -0
- package/dist/options/reflect.d.ts +5 -0
- package/dist/options/reflect.js +27 -0
- package/package.json +12 -4
- package/src/admin/adapters/d1.ts +5 -4
- package/src/admin/adapters/firestore.ts +6 -5
- package/src/admin/conformance.ts +14 -5
- package/src/admin/handler.ts +2 -2
- package/src/admin/index.ts +2 -2
- package/src/admin/memory.ts +2 -1
- package/src/admin/panel/dom.ts +6 -0
- package/src/admin/panel/gameCard.ts +13 -9
- package/src/admin/panel/icons.ts +3 -0
- package/src/admin/panel/overview.ts +1 -1
- package/src/admin/store.ts +2 -2
- package/src/admin/switches.ts +29 -12
- package/src/character/create.ts +6 -2
- package/src/character/layer.ts +21 -0
- package/src/emotes/clip.ts +204 -0
- package/src/emotes/clips.ts +7 -0
- package/src/emotes/events.ts +28 -0
- package/src/emotes/icons.ts +7 -0
- package/src/emotes/icons.webp +0 -0
- package/src/emotes/index.ts +16 -0
- package/src/emotes/list.ts +33 -0
- package/src/emotes/options.ts +43 -0
- package/src/emotes/panel/index.ts +11 -0
- package/src/emotes/panel/mount.ts +397 -0
- package/src/emotes/panel/style.css +415 -0
- package/src/emotes/player.ts +131 -0
- package/src/emotes/wheel.ts +131 -0
- package/src/friends/panel/mount.ts +8 -2
- package/src/options/common.ts +65 -0
- package/src/options/core.ts +465 -0
- package/src/options/index.ts +41 -0
- package/src/options/panel/index.ts +12 -0
- package/src/options/panel/mount.ts +423 -0
- package/src/options/panel/style.css +528 -0
- package/src/options/reflect.ts +28 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const WHEEL_SLOTS = 8;
|
|
2
|
+
/** The localStorage key of a game's wheel. */
|
|
3
|
+
export declare const wheelKey: (game: string) => string;
|
|
4
|
+
/** A wheel: eight places, each an emote's id once, or null (empty). */
|
|
5
|
+
export type Slots = readonly (string | null)[];
|
|
6
|
+
/** Nanoterra's wheel before the player changes it (the island's): the eight most social emotes. Bow, Shrug and Facepalm wait in Edit. */
|
|
7
|
+
export declare const DEFAULT_WHEEL: Slots;
|
|
8
|
+
/** What the player is told when they put a ninth emote on the wheel. */
|
|
9
|
+
export declare const WHEEL_FULL = "Your wheel is full. Take one off first.";
|
|
10
|
+
/** The first wheel for a list of emotes: `defaults` when given (each id of the list once), else the island's wheel for Nanoterra's list, else the list's first eight. */
|
|
11
|
+
export declare function defaultWheel(ids: readonly string[], defaults?: readonly (string | null)[]): Slots;
|
|
12
|
+
/** A kept wheel: eight places, each an emote of `ids` once or empty. Anything else (no key, broken text, another length) is `fallback`. */
|
|
13
|
+
export declare function readWheel(raw: string | null | undefined, ids: readonly string[], fallback: Slots): Slots;
|
|
14
|
+
/** The emote put on (in the first empty place), taken off, or refused because all eight places are full. */
|
|
15
|
+
export declare function toggleSlot(slots: Slots, id: string): {
|
|
16
|
+
slots: Slots;
|
|
17
|
+
did: "added" | "removed" | "full";
|
|
18
|
+
};
|
|
19
|
+
type Store = Pick<Storage, "getItem" | "setItem">;
|
|
20
|
+
export type WheelOptions = {
|
|
21
|
+
/** The game's id: each game keeps its own wheel. */
|
|
22
|
+
game: string;
|
|
23
|
+
/** Every emote the player may put on the wheel, by id. Default: Nanoterra's eleven. */
|
|
24
|
+
ids?: readonly string[];
|
|
25
|
+
/** The eight places before the player changes them. Default: see `defaultWheel`. */
|
|
26
|
+
defaults?: readonly (string | null)[];
|
|
27
|
+
/** Where the wheel is kept (default: the page's localStorage). null keeps it only for the life of the page. */
|
|
28
|
+
storage?: Store | null;
|
|
29
|
+
};
|
|
30
|
+
export type Wheel = {
|
|
31
|
+
/** The eight places now. */
|
|
32
|
+
readonly slots: Slots;
|
|
33
|
+
/** Puts an emote on (its first empty place) or takes it off; "full" changes nothing. */
|
|
34
|
+
toggle(id: string): "added" | "removed" | "full";
|
|
35
|
+
/** Takes the emote off a place. */
|
|
36
|
+
clear(slot: number): void;
|
|
37
|
+
/** Hears every change. Returns the call that stops it. */
|
|
38
|
+
subscribe(listen: (slots: Slots) => void): () => void;
|
|
39
|
+
};
|
|
40
|
+
/** A game's wheel, read from this browser (or the default), and kept there on each change. */
|
|
41
|
+
export declare function createWheel(options: WheelOptions): Wheel;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The player's emote wheel: eight places round the wheel, slot 1 at the top and on clockwise, each an emote or empty. The player picks which emotes sit
|
|
2
|
+
// there from every emote the game offers (Edit in the panel), so the list can grow past eight. Kept in this browser per game, under `n133-emotes:<game>`;
|
|
3
|
+
// a browser with nothing kept, or a storage that is refused (some private windows), has the game's default wheel and keeps changes for the page's life.
|
|
4
|
+
// No DOM and no Three.js here: the panel (./panel) draws it.
|
|
5
|
+
import { EMOTES } from "./list.js";
|
|
6
|
+
export const WHEEL_SLOTS = 8;
|
|
7
|
+
/** The localStorage key of a game's wheel. */
|
|
8
|
+
export const wheelKey = (game) => `n133-emotes:${game}`;
|
|
9
|
+
/** Nanoterra's wheel before the player changes it (the island's): the eight most social emotes. Bow, Shrug and Facepalm wait in Edit. */
|
|
10
|
+
export const DEFAULT_WHEEL = ["wave", "cheer", "clap", "dance", "laugh", "point", "sit", "spin"];
|
|
11
|
+
/** What the player is told when they put a ninth emote on the wheel. */
|
|
12
|
+
export const WHEEL_FULL = "Your wheel is full. Take one off first.";
|
|
13
|
+
/** Eight places from a list (cut, or filled with empty places), each id of `known` once. */
|
|
14
|
+
function fit(list, known) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
return Array.from({ length: WHEEL_SLOTS }, (_, i) => {
|
|
17
|
+
const x = list[i];
|
|
18
|
+
if (typeof x !== "string" || !known.has(x) || seen.has(x))
|
|
19
|
+
return null;
|
|
20
|
+
seen.add(x);
|
|
21
|
+
return x;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** The first wheel for a list of emotes: `defaults` when given (each id of the list once), else the island's wheel for Nanoterra's list, else the list's first eight. */
|
|
25
|
+
export function defaultWheel(ids, defaults) {
|
|
26
|
+
const known = new Set(ids);
|
|
27
|
+
if (defaults)
|
|
28
|
+
return fit(defaults, known);
|
|
29
|
+
const island = fit(DEFAULT_WHEEL, known);
|
|
30
|
+
return island.some((x) => x !== null) ? island : fit(ids, known);
|
|
31
|
+
}
|
|
32
|
+
/** A kept wheel: eight places, each an emote of `ids` once or empty. Anything else (no key, broken text, another length) is `fallback`. */
|
|
33
|
+
export function readWheel(raw, ids, fallback) {
|
|
34
|
+
if (!raw)
|
|
35
|
+
return fallback;
|
|
36
|
+
try {
|
|
37
|
+
const v = JSON.parse(raw);
|
|
38
|
+
if (!Array.isArray(v) || v.length !== WHEEL_SLOTS)
|
|
39
|
+
return fallback;
|
|
40
|
+
return fit(v, new Set(ids));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The emote put on (in the first empty place), taken off, or refused because all eight places are full. */
|
|
47
|
+
export function toggleSlot(slots, id) {
|
|
48
|
+
const at = slots.indexOf(id);
|
|
49
|
+
if (at >= 0)
|
|
50
|
+
return { slots: slots.map((x, i) => (i === at ? null : x)), did: "removed" };
|
|
51
|
+
const free = slots.indexOf(null);
|
|
52
|
+
if (free < 0)
|
|
53
|
+
return { slots, did: "full" };
|
|
54
|
+
return { slots: slots.map((x, i) => (i === free ? id : x)), did: "added" };
|
|
55
|
+
}
|
|
56
|
+
/** The page's localStorage, or null where reading it throws. */
|
|
57
|
+
function pageStorage() {
|
|
58
|
+
try {
|
|
59
|
+
return globalThis.localStorage ?? null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A game's wheel, read from this browser (or the default), and kept there on each change. */
|
|
66
|
+
export function createWheel(options) {
|
|
67
|
+
const ids = options.ids ?? EMOTES.map((e) => e.id);
|
|
68
|
+
const storage = options.storage === undefined ? pageStorage() : options.storage;
|
|
69
|
+
const key = wheelKey(options.game);
|
|
70
|
+
let raw = null;
|
|
71
|
+
try {
|
|
72
|
+
raw = storage?.getItem(key) ?? null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
raw = null;
|
|
76
|
+
}
|
|
77
|
+
let slots = readWheel(raw, ids, defaultWheel(ids, options.defaults));
|
|
78
|
+
const listeners = new Set();
|
|
79
|
+
const set = (next) => {
|
|
80
|
+
slots = next;
|
|
81
|
+
try {
|
|
82
|
+
storage?.setItem(key, JSON.stringify(next));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// A full or refused storage: the wheel still works for this page.
|
|
86
|
+
}
|
|
87
|
+
for (const l of listeners)
|
|
88
|
+
l(slots);
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
get slots() {
|
|
92
|
+
return slots;
|
|
93
|
+
},
|
|
94
|
+
toggle(id) {
|
|
95
|
+
const r = toggleSlot(slots, id);
|
|
96
|
+
if (r.did !== "full")
|
|
97
|
+
set(r.slots);
|
|
98
|
+
return r.did;
|
|
99
|
+
},
|
|
100
|
+
clear(slot) {
|
|
101
|
+
if (slots[slot] == null)
|
|
102
|
+
return;
|
|
103
|
+
set(slots.map((x, i) => (i === slot ? null : x)));
|
|
104
|
+
},
|
|
105
|
+
subscribe(listen) {
|
|
106
|
+
listeners.add(listen);
|
|
107
|
+
return () => listeners.delete(listen);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -21,6 +21,11 @@ export type FriendsMountOptions = {
|
|
|
21
21
|
fetch?: typeof fetch;
|
|
22
22
|
/** Asks the player a yes or no question (default: a question in the panel, with a key for each answer; never a browser dialog). */
|
|
23
23
|
confirm?: (question: string) => boolean | Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Where the keyboard focus goes when the panel closes. "game" (the default): out of the key and the panel, to the page, so Space and the game's
|
|
26
|
+
* other keys reach the game again (a focused key would take Space and open the panel again). "key": back to the key, for a page with no keys of its own.
|
|
27
|
+
*/
|
|
28
|
+
focusAfterClose?: "game" | "key";
|
|
24
29
|
};
|
|
25
30
|
export type MountedFriends = {
|
|
26
31
|
/** Sets where the player is (friends see it). Null: no place. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The panel is phone first: a sheet from the bottom on a phone held upright, from the side on a wide or sideways screen; every key is 44 px.
|
|
13
13
|
import { ApiError, createApi } from "../../admin/panel/api.js";
|
|
14
|
-
import { h } from "../../admin/panel/dom.js";
|
|
14
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
15
15
|
import { icon } from "../../admin/panel/icons.js";
|
|
16
16
|
import { readWindowMessage, toWindow, whereText, windowUrl } from "./window.js";
|
|
17
17
|
export const FRIENDS_TIMING = { viewOpenMs: 20_000, viewClosedMs: 60_000, pulseMs: 45_000 };
|
|
@@ -69,7 +69,10 @@ export function mountFriends(root, options) {
|
|
|
69
69
|
panel.dataset.open = "false";
|
|
70
70
|
key.setAttribute("aria-expanded", "false");
|
|
71
71
|
onClose();
|
|
72
|
-
key
|
|
72
|
+
if (options.focusAfterClose === "key")
|
|
73
|
+
key.focus();
|
|
74
|
+
else
|
|
75
|
+
releaseFocus(wrap);
|
|
73
76
|
};
|
|
74
77
|
key.addEventListener("click", () => (isOpen ? close() : open()));
|
|
75
78
|
closeKey.addEventListener("click", close);
|
|
@@ -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 {};
|