@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
|
@@ -40,7 +40,7 @@ async function read(res) {
|
|
|
40
40
|
/** A refusal carries words and a reason and nothing else: no data of the game. */
|
|
41
41
|
const REFUSAL_KEYS = new Set(["error", "reason", "signedInAs"]);
|
|
42
42
|
/** Words of the game's data that a refusal or an answer to a visitor must never hold. */
|
|
43
|
-
const DATA_WORDS = /"orders"|"players"|"totals"|"counts"|"character"|"signin"|"friends"|"warnings"/;
|
|
43
|
+
const DATA_WORDS = /"orders"|"players"|"totals"|"counts"|"character"|"signin"|"friends"|"emotes"|"warnings"/;
|
|
44
44
|
export function adminConformance(host) {
|
|
45
45
|
const cases = [];
|
|
46
46
|
const add = (name, run) => cases.push({ name, run });
|
|
@@ -264,7 +264,7 @@ export function adminStoreConformance(make) {
|
|
|
264
264
|
const order = (id, over) => ({ id, player: A, lookId: "mint-cap", usd: "0.25", amountRaw: rawOf(0.25), status: "paid", createdAt: NOW - 1000, expiresAt: NOW + 1000, paidAt: NOW - 500, payer: A, hash: `H${id}`, ...over });
|
|
265
265
|
add("a new store has no settings of its own", async ({ store }) => {
|
|
266
266
|
const s = await store.readSettings();
|
|
267
|
-
check(s.character === undefined && s.signin === undefined && s.friends === undefined, `expected no settings, got ${JSON.stringify(s)}`);
|
|
267
|
+
check(s.character === undefined && s.signin === undefined && s.friends === undefined && s.emotes === undefined, `expected no settings, got ${JSON.stringify(s)}`);
|
|
268
268
|
});
|
|
269
269
|
add("the friends list is kept as its own setting: each of its three words, and a patch of it leaves the other two", async ({ store }) => {
|
|
270
270
|
await store.saveSettings({ signin: "none", character: "own" }, NOW);
|
|
@@ -274,6 +274,14 @@ export function adminStoreConformance(make) {
|
|
|
274
274
|
check(s.friends === friends && s.signin === "none" && s.character === "own", `after friends ${friends}: ${JSON.stringify(s)}`);
|
|
275
275
|
}
|
|
276
276
|
});
|
|
277
|
+
add("the emotes switch is kept as its own setting: off and on, and a patch of it leaves the other three", async ({ store }) => {
|
|
278
|
+
await store.saveSettings({ signin: "none", character: "own", friends: "off" }, NOW);
|
|
279
|
+
for (const [i, emotes] of ["off", "on"].entries()) {
|
|
280
|
+
await store.saveSettings({ emotes }, NOW + 1 + i);
|
|
281
|
+
const s = await store.readSettings();
|
|
282
|
+
check(s.emotes === emotes && s.signin === "none" && s.character === "own" && s.friends === "off", `after emotes ${emotes}: ${JSON.stringify(s)}`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
277
285
|
add("a patch saves only what it names; the next patch keeps the rest", async ({ store }) => {
|
|
278
286
|
await store.saveSettings({ signin: "none" }, NOW);
|
|
279
287
|
let s = await store.readSettings();
|
|
@@ -288,12 +296,12 @@ export function adminStoreConformance(make) {
|
|
|
288
296
|
add("an empty patch writes nothing and does not fail", async ({ store }) => {
|
|
289
297
|
await store.saveSettings({}, NOW);
|
|
290
298
|
const s = await store.readSettings();
|
|
291
|
-
check(s.character === undefined && s.signin === undefined && s.friends === undefined, "an empty patch must save nothing");
|
|
299
|
+
check(s.character === undefined && s.signin === undefined && s.friends === undefined && s.emotes === undefined, "an empty patch must save nothing");
|
|
292
300
|
});
|
|
293
301
|
add("saving changes nothing but the settings: no order, no owned look, no other meta row", async (h) => {
|
|
294
302
|
await h.seed({ orders: [order("1", {}), order("2", { status: "open" })], owned: [{ player: A, lookId: "mint-cap", at: NOW }], rate: { value: JSON.stringify({ usd: 1.2, sources: ["x"] }), at: NOW } });
|
|
295
303
|
const before = await h.snapshot();
|
|
296
|
-
await h.store.saveSettings({ character: "own", signin: "none", friends: "off" }, NOW + 5);
|
|
304
|
+
await h.store.saveSettings({ character: "own", signin: "none", friends: "off", emotes: "off" }, NOW + 5);
|
|
297
305
|
check((await h.snapshot()) === before, "the rows of the game changed after saving the settings");
|
|
298
306
|
});
|
|
299
307
|
add("the summary counts the orders by status, reads an open order past its expiry as expired, and totals the money", async ({ store, seed }) => {
|
package/dist/admin/handler.d.ts
CHANGED
|
@@ -59,5 +59,5 @@ export declare const ADMIN_LIMITS: {
|
|
|
59
59
|
readonly gameBodyMax: 512;
|
|
60
60
|
};
|
|
61
61
|
export declare function createAdminHandler(options: AdminHandlerOptions): AdminHandler;
|
|
62
|
-
/** The settings as the game's own routes need them (the listing file, the character, the friends list): the stored words, or the game's defaults. A fault in the store gives the defaults. */
|
|
62
|
+
/** The settings as the game's own routes need them (the listing file, the character, the friends list, the emotes): the stored words, or the game's defaults. A fault in the store gives the defaults. */
|
|
63
63
|
export declare function loadGameSettings(store: Pick<AdminStore, "readSettings">, defaults: GameSettings): Promise<GameState>;
|
package/dist/admin/handler.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// GET {base}/summary the numbers: status, orders, the looks that players own
|
|
4
4
|
// GET {base}/game the game switches
|
|
5
|
-
// POST {base}/game a press: { character }, { signin } or {
|
|
5
|
+
// POST {base}/game a press: { character }, { signin }, { friends } or { emotes } (only the known words)
|
|
6
6
|
// POST {base}/check the payment check, when the game gives one
|
|
7
7
|
//
|
|
8
8
|
// EVERY request under the base goes through `adminAccess` first, before the route is even looked up: a visitor who is not the owner gets 401 or 403
|
|
@@ -159,7 +159,7 @@ export function createAdminHandler(options) {
|
|
|
159
159
|
}
|
|
160
160
|
return { routes, basePath: options.basePath ?? "/api/admin", handle };
|
|
161
161
|
}
|
|
162
|
-
/** The settings as the game's own routes need them (the listing file, the character, the friends list): the stored words, or the game's defaults. A fault in the store gives the defaults. */
|
|
162
|
+
/** The settings as the game's own routes need them (the listing file, the character, the friends list, the emotes): the stored words, or the game's defaults. A fault in the store gives the defaults. */
|
|
163
163
|
export async function loadGameSettings(store, defaults) {
|
|
164
164
|
try {
|
|
165
165
|
return mergeSettings(await store.readSettings(), defaults);
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export type { AdminLogin, AdminOrder, AdminPlayer, AdminStatus, AdminStore, Admi
|
|
|
9
9
|
export { ORDERS_SHOWN, moneyOf, parseRate, shapeOrders, shapePlayers, statusOf, summarize } from "./summary.js";
|
|
10
10
|
export type { OrderRow } from "./summary.js";
|
|
11
11
|
export { RAW_PER_XNO, xnoText } from "./amount.js";
|
|
12
|
-
export { CHARACTER_NEEDS_SIGNIN, CHARACTER_SIGNIN_OFF, DEFAULT_FRIENDS, FRIENDS_CHOICES, FRIENDS_TITLE, REVIEW_LINK, REVIEW_NOTE, SWITCHES, isCharacter, isFriends, isSignin, isSigninChoice, mergeSettings, parsePatch, pressOf, signinOn, stateOf } from "./switches.js";
|
|
13
|
-
export type { Character, Friends, GamePatch, GameSettings, GameState, Signin, SwitchId } from "./switches.js";
|
|
12
|
+
export { CHARACTER_NEEDS_SIGNIN, CHARACTER_SIGNIN_OFF, DEFAULT_EMOTES, DEFAULT_FRIENDS, FRIENDS_CHOICES, FRIENDS_TITLE, REVIEW_LINK, REVIEW_NOTE, SWITCHES, isCharacter, isEmotes, isFriends, isSignin, isSigninChoice, mergeSettings, parsePatch, pressOf, signinOn, stateOf } from "./switches.js";
|
|
13
|
+
export type { Character, Emotes, Friends, GamePatch, GameSettings, GameState, Signin, SwitchId } from "./switches.js";
|
package/dist/admin/index.js
CHANGED
|
@@ -11,4 +11,4 @@ export { createMemoryStore } from "./memory.js";
|
|
|
11
11
|
export { ADMIN_LOGINS_SHOWN } from "./store.js";
|
|
12
12
|
export { ORDERS_SHOWN, moneyOf, parseRate, shapeOrders, shapePlayers, statusOf, summarize } from "./summary.js";
|
|
13
13
|
export { RAW_PER_XNO, xnoText } from "./amount.js";
|
|
14
|
-
export { CHARACTER_NEEDS_SIGNIN, CHARACTER_SIGNIN_OFF, DEFAULT_FRIENDS, FRIENDS_CHOICES, FRIENDS_TITLE, REVIEW_LINK, REVIEW_NOTE, SWITCHES, isCharacter, isFriends, isSignin, isSigninChoice, mergeSettings, parsePatch, pressOf, signinOn, stateOf } from "./switches.js";
|
|
14
|
+
export { CHARACTER_NEEDS_SIGNIN, CHARACTER_SIGNIN_OFF, DEFAULT_EMOTES, DEFAULT_FRIENDS, FRIENDS_CHOICES, FRIENDS_TITLE, REVIEW_LINK, REVIEW_NOTE, SWITCHES, isCharacter, isEmotes, isFriends, isSignin, isSigninChoice, mergeSettings, parsePatch, pressOf, signinOn, stateOf } from "./switches.js";
|
package/dist/admin/memory.js
CHANGED
|
@@ -14,7 +14,7 @@ export function createMemoryStore(seed = {}) {
|
|
|
14
14
|
pendingAdminRequests: 0,
|
|
15
15
|
pendingGameRequests: 0,
|
|
16
16
|
async readSettings() {
|
|
17
|
-
return { character: store.rows.get("setting:character"), signin: store.rows.get("setting:signin"), friends: store.rows.get("setting:friends") };
|
|
17
|
+
return { character: store.rows.get("setting:character"), signin: store.rows.get("setting:signin"), friends: store.rows.get("setting:friends"), emotes: store.rows.get("setting:emotes") };
|
|
18
18
|
},
|
|
19
19
|
async saveSettings(patch) {
|
|
20
20
|
if (patch.character)
|
|
@@ -23,6 +23,8 @@ export function createMemoryStore(seed = {}) {
|
|
|
23
23
|
store.rows.set("setting:signin", patch.signin);
|
|
24
24
|
if (patch.friends)
|
|
25
25
|
store.rows.set("setting:friends", patch.friends);
|
|
26
|
+
if (patch.emotes)
|
|
27
|
+
store.rows.set("setting:emotes", patch.emotes);
|
|
26
28
|
store.writes++;
|
|
27
29
|
},
|
|
28
30
|
async summary(now) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type Attrs = Record<string, string | number | boolean | undefined | ((e: Event) => void)>;
|
|
2
2
|
type Child = Node | string | null | undefined | false;
|
|
3
3
|
export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Attrs, ...children: Child[]): HTMLElementTagNameMap[K];
|
|
4
|
+
/** Takes the keyboard focus out of `within` (to the page), so a game's own keys reach the game again. */
|
|
5
|
+
export declare function releaseFocus(within: Element): void;
|
|
4
6
|
/** "nano_1abc…wxyz" */
|
|
5
7
|
export declare const shortAddress: (a: string) => string;
|
|
6
8
|
export declare function copy(text: string, button: HTMLButtonElement): Promise<void>;
|
package/dist/admin/panel/dom.js
CHANGED
|
@@ -18,6 +18,12 @@ export function h(tag, attrs = {}, ...children) {
|
|
|
18
18
|
el.append(c);
|
|
19
19
|
return el;
|
|
20
20
|
}
|
|
21
|
+
/** Takes the keyboard focus out of `within` (to the page), so a game's own keys reach the game again. */
|
|
22
|
+
export function releaseFocus(within) {
|
|
23
|
+
const now = document.activeElement;
|
|
24
|
+
if (now instanceof HTMLElement && within.contains(now))
|
|
25
|
+
now.blur();
|
|
26
|
+
}
|
|
21
27
|
/** "nano_1abc…wxyz" */
|
|
22
28
|
export const shortAddress = (a) => `${a.slice(0, 10)}…${a.slice(-6)}`;
|
|
23
29
|
export async function copy(text, button) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// The "Game" card of /admin: two switches, "Players can play as their Nanoterra character" and "Players can sign in with Nanoterra",
|
|
2
|
-
// choice, "Friends list: Nanoterra / The game's own / Off". A press saves at once (POST {api}/game, behind the
|
|
3
|
-
// the game reads the setting at run time, so the creator pushes and deploys nothing. The words are in
|
|
4
|
-
// settings: never a look, a price or an order.
|
|
1
|
+
// The "Game" card of /admin: two switches, "Players can play as their Nanoterra character" and "Players can sign in with Nanoterra", one three-way
|
|
2
|
+
// choice, "Friends list: Nanoterra / The game's own / Off", and the switch "Allow Nanoterra emotes". A press saves at once (POST {api}/game, behind the
|
|
3
|
+
// same admin guard as every admin call) and the game reads the setting at run time, so the creator pushes and deploys nothing. The words are in
|
|
4
|
+
// ../switches.ts. The card changes ONLY these four settings: never a look, a price or an order.
|
|
5
5
|
import { CHARACTER_NEEDS_SIGNIN, CHARACTER_SIGNIN_OFF, FRIENDS_CHOICES, FRIENDS_TITLE, REVIEW_LINK, REVIEW_NOTE, SWITCHES, pressOf, signinOn, stateOf } from "../switches.js";
|
|
6
6
|
import { ApiError } from "./api.js";
|
|
7
7
|
import { h } from "./dom.js";
|
|
@@ -15,7 +15,7 @@ export function gameCard(api, initial, defaultsFile = "the game's own settings",
|
|
|
15
15
|
/** Under the character switch: it works only through sign-in, so it says so, and says what a player gets while sign-in is Off. */
|
|
16
16
|
const needs = h("p", { class: "fine switch-needs" });
|
|
17
17
|
const card = h("section", { class: "admin-card game-card", "aria-labelledby": "game-title" }, h("div", { class: "card-head" }, h("h2", { id: "game-title" }, "Game")));
|
|
18
|
-
card.append(h("p", { class: "fine card-intro" }, `
|
|
18
|
+
card.append(h("p", { class: "fine card-intro" }, `Four choices about your game. A press saves at once: you push nothing and deploy nothing. They start from ${defaultsFile}, and a press here wins.`));
|
|
19
19
|
/** The friends choice: a radio group of three keys (arrow keys move between them, as a radio group does). */
|
|
20
20
|
const friendsKeys = new Map();
|
|
21
21
|
const friendsNote = h("p", { class: "switch-note", id: "switch-friends-note" });
|
|
@@ -75,7 +75,8 @@ export function gameCard(api, initial, defaultsFile = "the game's own settings",
|
|
|
75
75
|
row.button.disabled = false;
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
|
-
|
|
78
|
+
/** A switch's row: its title, the On/Off key, the sentence for its state and the save status. */
|
|
79
|
+
const switchRow = (id) => {
|
|
79
80
|
const label = h("span", { class: "switch-title", id: `switch-${id}-label` }, SWITCHES[id].title);
|
|
80
81
|
const button = h("button", { class: "switch", type: "button", role: "switch", "aria-checked": "false", "aria-labelledby": `switch-${id}-label`, "aria-describedby": `switch-${id}-note` }, h("span", { class: "switch-word" }, "Off"), h("span", { class: "switch-knob", "aria-hidden": "true" }));
|
|
81
82
|
const sentence = h("p", { class: "switch-note", id: `switch-${id}-note` });
|
|
@@ -83,10 +84,11 @@ export function gameCard(api, initial, defaultsFile = "the game's own settings",
|
|
|
83
84
|
button.addEventListener("click", () => void press(id));
|
|
84
85
|
rows.set(id, { button, sentence, status });
|
|
85
86
|
card.append(h("div", { class: "switch-row" }, h("div", { class: "switch-head" }, label, button), sentence, id === "character" ? needs : null, status));
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
};
|
|
88
|
+
switchRow("character");
|
|
89
|
+
switchRow("signin");
|
|
90
|
+
// The sign-in word is part of the listing file: Nanoterra's review decides what Nanoterra shows. This page cannot see that review.
|
|
91
|
+
card.append(h("p", { class: "fine switch-review" }, REVIEW_NOTE, " ", h("a", { href: REVIEW_LINK, target: "_blank", rel: "noopener noreferrer" }, "Open the launch page")));
|
|
90
92
|
const order = Object.keys(FRIENDS_CHOICES);
|
|
91
93
|
const group = h("div", { class: "seg friends-choice", role: "radiogroup", "aria-labelledby": "switch-friends-label", "aria-describedby": "switch-friends-note" });
|
|
92
94
|
for (const id of order) {
|
|
@@ -105,6 +107,7 @@ export function gameCard(api, initial, defaultsFile = "the game's own settings",
|
|
|
105
107
|
group.append(key);
|
|
106
108
|
}
|
|
107
109
|
card.append(h("div", { class: "switch-row friends-row" }, h("div", { class: "switch-head" }, h("span", { class: "switch-title", id: "switch-friends-label" }, FRIENDS_TITLE), group), friendsNote, friendsStatus));
|
|
110
|
+
switchRow("emotes");
|
|
108
111
|
draw();
|
|
109
112
|
return card;
|
|
110
113
|
}
|
|
@@ -129,9 +129,26 @@ declare const SHAPES: {
|
|
|
129
129
|
d: string;
|
|
130
130
|
fill: string;
|
|
131
131
|
}])[];
|
|
132
|
+
gear: ([string, {
|
|
133
|
+
d: string;
|
|
134
|
+
}] | [string, {
|
|
135
|
+
cx: number;
|
|
136
|
+
cy: number;
|
|
137
|
+
r: number;
|
|
138
|
+
}])[];
|
|
132
139
|
sparkle: [string, {
|
|
133
140
|
d: string;
|
|
134
141
|
}][];
|
|
142
|
+
smile: ([string, {
|
|
143
|
+
cx: number;
|
|
144
|
+
cy: number;
|
|
145
|
+
r: number;
|
|
146
|
+
}] | [string, {
|
|
147
|
+
d: string;
|
|
148
|
+
}] | [string, {
|
|
149
|
+
d: string;
|
|
150
|
+
"stroke-width": number;
|
|
151
|
+
}])[];
|
|
135
152
|
};
|
|
136
153
|
export type IconName = keyof typeof SHAPES;
|
|
137
154
|
/** An icon of `size` CSS pixels (default 20). */
|
|
@@ -27,7 +27,10 @@ const SHAPES = {
|
|
|
27
27
|
sun: [["circle", { cx: 12, cy: 12, r: 4 }], ["path", { d: "M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6" }]],
|
|
28
28
|
moon: [["path", { d: "M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5Z" }]],
|
|
29
29
|
device: [["circle", { cx: 12, cy: 12, r: 8.5 }], ["path", { d: "M12 3.5a8.5 8.5 0 0 1 0 17Z", fill: "currentColor" }]],
|
|
30
|
+
// A gear of 8 teeth (the options key): the settings icon above reads as a sun at 20 px.
|
|
31
|
+
gear: [["path", { d: "M19.23 9.65 21.77 9.86V14.14L19.23 14.35 18.77 15.45 20.42 17.39 17.39 20.42 15.45 18.77 14.35 19.23 14.14 21.77H9.86L9.65 19.23 8.55 18.77 6.61 20.42 3.58 17.39 5.23 15.45 4.77 14.35 2.23 14.14V9.86L4.77 9.65 5.23 8.55 3.58 6.61 6.61 3.58 8.55 5.23 9.65 4.77 9.86 2.23H14.14L14.35 4.77 15.45 5.23 17.39 3.58 20.42 6.61 18.77 8.55Z" }], ["circle", { cx: 12, cy: 12, r: 3.2 }]],
|
|
30
32
|
sparkle: [["path", { d: "M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M6 18l2.5-2.5M15.5 8.5 18 6" }]],
|
|
33
|
+
smile: [["circle", { cx: 12, cy: 12, r: 9 }], ["path", { d: "M8.2 14.2a4.6 4.6 0 0 0 7.6 0" }], ["path", { d: "M9 9.6h.01M15 9.6h.01", "stroke-width": 2.6 }]],
|
|
31
34
|
};
|
|
32
35
|
/** An icon of `size` CSS pixels (default 20). */
|
|
33
36
|
export function icon(name, size = 20) {
|
|
@@ -31,7 +31,7 @@ export function overviewView(looks, colourOf, go) {
|
|
|
31
31
|
const el = h("div", { class: "overview" }, main, side);
|
|
32
32
|
const linkKey = (words, to) => h("button", { class: "btn btn-small btn-quiet link-key", type: "button", onclick: to }, words, h("span", { "aria-hidden": "true" }, "→"));
|
|
33
33
|
function drawGame() {
|
|
34
|
-
const rows = ["character", "signin"].map((id) => {
|
|
34
|
+
const rows = ["character", "signin", "emotes"].map((id) => {
|
|
35
35
|
const on = game ? stateOf(id, game) : null;
|
|
36
36
|
return h("li", {}, h("span", {}, SWITCHES[id].title), on === null ? h("span", { class: "pill" }, "Unknown") : h("span", { class: `pill ${on ? "pill-on" : "pill-off"}` }, on ? "On" : "Off"));
|
|
37
37
|
});
|
package/dist/admin/store.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type AdminLogin = {
|
|
|
71
71
|
/** How many sign-ins the panel lists. */
|
|
72
72
|
export declare const ADMIN_LOGINS_SHOWN = 10;
|
|
73
73
|
/**
|
|
74
|
-
* The game's database, as the handler needs it. An adapter writes ONLY what these calls say: the
|
|
74
|
+
* The game's database, as the handler needs it. An adapter writes ONLY what these calls say: the four settings and, when the game turns it on, the deletion of the admin sessions
|
|
75
75
|
* (`signOutEverywhere`). Nothing else.
|
|
76
76
|
* (There is no call that writes a look, a price or an order: the Shelf gives text to copy, and a change of looks is a change of the repository.)
|
|
77
77
|
*/
|
|
@@ -81,6 +81,7 @@ export interface AdminStore {
|
|
|
81
81
|
character?: unknown;
|
|
82
82
|
signin?: unknown;
|
|
83
83
|
friends?: unknown;
|
|
84
|
+
emotes?: unknown;
|
|
84
85
|
}>;
|
|
85
86
|
/** Saves a checked patch: all of it or none of it. */
|
|
86
87
|
saveSettings(patch: GamePatch, now: number): Promise<void>;
|
package/dist/admin/switches.d.ts
CHANGED
|
@@ -7,35 +7,44 @@ export type Signin = "exchange" | "ticket" | "none";
|
|
|
7
7
|
* A three-way choice, not an On/Off switch.
|
|
8
8
|
*/
|
|
9
9
|
export type Friends = "nanoterra" | "own" | "off";
|
|
10
|
-
/**
|
|
10
|
+
/** Whether the game shows Nanoterra's emotes ("@nano133/nanoterra/emotes"): the wheel's Nanoterra emotes and the other players' on their characters. Off leaves the game's own emotes. */
|
|
11
|
+
export type Emotes = "on" | "off";
|
|
12
|
+
/** The game's own words for the switches. `friends` and `emotes` may be left out: "nanoterra" and "on" then (on by default). */
|
|
11
13
|
export type GameSettings = {
|
|
12
14
|
character: Character;
|
|
13
15
|
signin: Signin;
|
|
14
16
|
friends?: Friends;
|
|
17
|
+
emotes?: Emotes;
|
|
15
18
|
};
|
|
16
19
|
/** What a press can set: sign-in is on ("exchange": a game with a server) or off ("none"). */
|
|
17
20
|
export type GamePatch = {
|
|
18
21
|
character?: Character;
|
|
19
22
|
signin?: "exchange" | "none";
|
|
20
23
|
friends?: Friends;
|
|
24
|
+
emotes?: Emotes;
|
|
21
25
|
};
|
|
22
26
|
/** The settings as they are now, and which ones /admin has set (the others still have the game's default). */
|
|
23
27
|
export type GameState = {
|
|
24
28
|
character: Character;
|
|
25
29
|
signin: Signin;
|
|
26
30
|
friends: Friends;
|
|
31
|
+
emotes: Emotes;
|
|
27
32
|
fromAdmin: {
|
|
28
33
|
character: boolean;
|
|
29
34
|
signin: boolean;
|
|
30
35
|
friends: boolean;
|
|
36
|
+
emotes: boolean;
|
|
31
37
|
};
|
|
32
38
|
};
|
|
33
39
|
export declare const isCharacter: (v: unknown) => v is Character;
|
|
34
40
|
export declare const isSigninChoice: (v: unknown) => v is "exchange" | "none";
|
|
35
41
|
export declare const isSignin: (v: unknown) => v is Signin;
|
|
36
42
|
export declare const isFriends: (v: unknown) => v is Friends;
|
|
43
|
+
export declare const isEmotes: (v: unknown) => v is Emotes;
|
|
37
44
|
/** The friends list a game shows while neither its own words nor /admin say: Nanoterra's. */
|
|
38
45
|
export declare const DEFAULT_FRIENDS: Friends;
|
|
46
|
+
/** Nanoterra's emotes are on while neither the game's own words nor /admin say. */
|
|
47
|
+
export declare const DEFAULT_EMOTES: Emotes;
|
|
39
48
|
/** The sign-in switch is on for any way but "none". */
|
|
40
49
|
export declare const signinOn: (s: Signin) => boolean;
|
|
41
50
|
/** A request body for a press: only the two known words, at least one. Anything else is refused, with the reason in words. */
|
|
@@ -51,6 +60,7 @@ export declare function mergeSettings(stored: {
|
|
|
51
60
|
character?: unknown;
|
|
52
61
|
signin?: unknown;
|
|
53
62
|
friends?: unknown;
|
|
63
|
+
emotes?: unknown;
|
|
54
64
|
}, defaults: GameSettings): GameState;
|
|
55
65
|
/** The friends choice of the card: its title, and for each choice its short name (the card's three keys) and one plain sentence. Nanoterra's is the default. */
|
|
56
66
|
export declare const FRIENDS_TITLE = "Friends list";
|
|
@@ -59,7 +69,7 @@ export declare const FRIENDS_CHOICES: Record<Friends, {
|
|
|
59
69
|
note: string;
|
|
60
70
|
}>;
|
|
61
71
|
/** One switch of the card: its title, and one plain sentence for each state. */
|
|
62
|
-
export type SwitchId = "character" | "signin";
|
|
72
|
+
export type SwitchId = "character" | "signin" | "emotes";
|
|
63
73
|
export declare const SWITCHES: Record<SwitchId, {
|
|
64
74
|
title: string;
|
|
65
75
|
on: string;
|
|
@@ -77,4 +87,4 @@ export declare const REVIEW_NOTE = "Your choice is saved here at once. The sign-
|
|
|
77
87
|
export declare const REVIEW_LINK = "https://nano133.com/build/nanoterra/launch";
|
|
78
88
|
/** The state of a switch from the settings, and the patch that a press sends (the other state). */
|
|
79
89
|
export declare const stateOf: (id: SwitchId, s: GameSettings) => boolean;
|
|
80
|
-
export declare
|
|
90
|
+
export declare function pressOf(id: SwitchId, s: GameSettings): GamePatch;
|
package/dist/admin/switches.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// The switches of the "Game" card in /admin, and the words that say what each does. The handler checks a press with `parsePatch`; the panel
|
|
2
|
-
// draws the card from `SWITCHES` and `FRIENDS_CHOICES`. A switch changes ONLY these
|
|
2
|
+
// draws the card from `SWITCHES` and `FRIENDS_CHOICES`. A switch changes ONLY these four settings; never a look, a price or an order.
|
|
3
3
|
export const isCharacter = (v) => v === "nanoterra" || v === "own";
|
|
4
4
|
export const isSigninChoice = (v) => v === "exchange" || v === "none";
|
|
5
5
|
export const isSignin = (v) => v === "exchange" || v === "ticket" || v === "none";
|
|
6
6
|
export const isFriends = (v) => v === "nanoterra" || v === "own" || v === "off";
|
|
7
|
+
export const isEmotes = (v) => v === "on" || v === "off";
|
|
7
8
|
/** The friends list a game shows while neither its own words nor /admin say: Nanoterra's. */
|
|
8
9
|
export const DEFAULT_FRIENDS = "nanoterra";
|
|
10
|
+
/** Nanoterra's emotes are on while neither the game's own words nor /admin say. */
|
|
11
|
+
export const DEFAULT_EMOTES = "on";
|
|
9
12
|
/** The sign-in switch is on for any way but "none". */
|
|
10
13
|
export const signinOn = (s) => s !== "none";
|
|
11
14
|
/** A request body for a press: only the two known words, at least one. Anything else is refused, with the reason in words. */
|
|
12
15
|
export function parsePatch(body) {
|
|
13
16
|
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
14
|
-
return { ok: false, error: 'Send an object with "character", "signin" or "
|
|
17
|
+
return { ok: false, error: 'Send an object with "character", "signin", "friends" or "emotes".' };
|
|
15
18
|
const patch = {};
|
|
16
19
|
for (const [key, value] of Object.entries(body)) {
|
|
17
20
|
if (key === "character" && isCharacter(value))
|
|
@@ -20,8 +23,12 @@ export function parsePatch(body) {
|
|
|
20
23
|
patch.signin = value;
|
|
21
24
|
else if (key === "friends" && isFriends(value))
|
|
22
25
|
patch.friends = value;
|
|
26
|
+
else if (key === "emotes" && isEmotes(value))
|
|
27
|
+
patch.emotes = value;
|
|
23
28
|
else if (key === "friends")
|
|
24
29
|
return { ok: false, error: '"friends" must be "nanoterra", "own" or "off".' };
|
|
30
|
+
else if (key === "emotes")
|
|
31
|
+
return { ok: false, error: '"emotes" must be "on" or "off".' };
|
|
25
32
|
else if (key === "character")
|
|
26
33
|
return { ok: false, error: '"character" must be "nanoterra" or "own".' };
|
|
27
34
|
else if (key === "signin")
|
|
@@ -30,7 +37,7 @@ export function parsePatch(body) {
|
|
|
30
37
|
return { ok: false, error: `"${key}" is not a game switch.` };
|
|
31
38
|
}
|
|
32
39
|
if (!Object.keys(patch).length)
|
|
33
|
-
return { ok: false, error: 'Send "character", "signin" or "
|
|
40
|
+
return { ok: false, error: 'Send "character", "signin", "friends" or "emotes".' };
|
|
34
41
|
return { ok: true, patch };
|
|
35
42
|
}
|
|
36
43
|
/** The settings from what the store kept and the game's defaults. A stored value that is not one of the known words is ignored. */
|
|
@@ -39,7 +46,8 @@ export function mergeSettings(stored, defaults) {
|
|
|
39
46
|
character: isCharacter(stored.character) ? stored.character : defaults.character,
|
|
40
47
|
signin: isSigninChoice(stored.signin) ? stored.signin : defaults.signin,
|
|
41
48
|
friends: isFriends(stored.friends) ? stored.friends : isFriends(defaults.friends) ? defaults.friends : DEFAULT_FRIENDS,
|
|
42
|
-
|
|
49
|
+
emotes: isEmotes(stored.emotes) ? stored.emotes : isEmotes(defaults.emotes) ? defaults.emotes : DEFAULT_EMOTES,
|
|
50
|
+
fromAdmin: { character: isCharacter(stored.character), signin: isSigninChoice(stored.signin), friends: isFriends(stored.friends), emotes: isEmotes(stored.emotes) },
|
|
43
51
|
};
|
|
44
52
|
}
|
|
45
53
|
/** The friends choice of the card: its title, and for each choice its short name (the card's three keys) and one plain sentence. Nanoterra's is the default. */
|
|
@@ -66,6 +74,11 @@ export const SWITCHES = {
|
|
|
66
74
|
on: "On: this game's listing file says players can sign in with Nanoterra, and the game accepts that sign-in.",
|
|
67
75
|
off: "Off: the listing file says no sign-in, and the game refuses it: a link made before still signs nobody in.",
|
|
68
76
|
},
|
|
77
|
+
emotes: {
|
|
78
|
+
title: "Allow Nanoterra emotes",
|
|
79
|
+
on: "On: players can play Nanoterra's emotes (Wave, Dance, Sit and more) from the Emotes wheel, and see the other players' emotes.",
|
|
80
|
+
off: "Off: the game shows no Nanoterra emote. The game's own emotes, if it has any, stay on the wheel.",
|
|
81
|
+
},
|
|
69
82
|
};
|
|
70
83
|
/**
|
|
71
84
|
* The character comes through the sign-in: only a player who signs in from Nanoterra brings one. So the character switch is shown with its dependency. With sign-in Off the
|
|
@@ -78,5 +91,11 @@ export const CHARACTER_SIGNIN_OFF = "Sign-in from Nanoterra is Off, so every pla
|
|
|
78
91
|
export const REVIEW_NOTE = "Your choice is saved here at once. The sign-in word is part of your listing file, and Nanoterra's review decides what Nanoterra shows for a game that is listed. This page cannot see that review. Check it on Nanoterra's launch page (step 6).";
|
|
79
92
|
export const REVIEW_LINK = "https://nano133.com/build/nanoterra/launch";
|
|
80
93
|
/** The state of a switch from the settings, and the patch that a press sends (the other state). */
|
|
81
|
-
export const stateOf = (id, s) => (id === "character" ? s.character === "nanoterra" : signinOn(s.signin));
|
|
82
|
-
export
|
|
94
|
+
export const stateOf = (id, s) => (id === "character" ? s.character === "nanoterra" : id === "emotes" ? (s.emotes ?? DEFAULT_EMOTES) === "on" : signinOn(s.signin));
|
|
95
|
+
export function pressOf(id, s) {
|
|
96
|
+
if (id === "character")
|
|
97
|
+
return { character: s.character === "nanoterra" ? "own" : "nanoterra" };
|
|
98
|
+
if (id === "emotes")
|
|
99
|
+
return { emotes: stateOf("emotes", s) ? "off" : "on" };
|
|
100
|
+
return { signin: signinOn(s.signin) ? "none" : "exchange" };
|
|
101
|
+
}
|
package/dist/character/create.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import { assemble } from "./assemble.js";
|
|
3
3
|
import { BASE_SPECS } from "./bases/index.js";
|
|
4
4
|
import { HEIGHTS } from "./lists.js";
|
|
5
|
+
import { poseLayerOf } from "./layer.js";
|
|
5
6
|
import { assertLook, recordProblem } from "./look.js";
|
|
6
7
|
import { buildWith, paintCharacter } from "./make.js";
|
|
7
8
|
import { makePose, MAX_DT } from "./pose.js";
|
|
@@ -136,7 +137,7 @@ export function createCharacter(options = {}) {
|
|
|
136
137
|
r.made.hull.dispose();
|
|
137
138
|
r.a.skeleton.dispose();
|
|
138
139
|
};
|
|
139
|
-
|
|
140
|
+
const self = {
|
|
140
141
|
object,
|
|
141
142
|
get record() {
|
|
142
143
|
return frozen(record);
|
|
@@ -159,7 +160,9 @@ export function createCharacter(options = {}) {
|
|
|
159
160
|
return;
|
|
160
161
|
time += step;
|
|
161
162
|
rig.pose.step(step, motion ?? {}, reducedMotion);
|
|
162
|
-
//
|
|
163
|
+
// A layer over the pose (an emote: layer.ts), then the body's own moving parts (a tail, wings, long hair) follow what the pose did.
|
|
164
|
+
const state = rig.pose.state;
|
|
165
|
+
poseLayerOf(self)?.(rig.a.bones, { dt: step, speed: state.speed, airborne: state.air, reducedMotion });
|
|
163
166
|
rig.a.mover?.(step, time, rig.pose.state.speed, reducedMotion);
|
|
164
167
|
},
|
|
165
168
|
wave() {
|
|
@@ -228,4 +231,5 @@ export function createCharacter(options = {}) {
|
|
|
228
231
|
ramp.dispose();
|
|
229
232
|
},
|
|
230
233
|
};
|
|
234
|
+
return self;
|
|
231
235
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
import type { BoneName } from "./skeleton.js";
|
|
3
|
+
/** What the pose did this frame: the step it took (s), its eased speed (m/s), whether it is in the air, and the reduced-motion setting. */
|
|
4
|
+
export type PoseFrame = {
|
|
5
|
+
dt: number;
|
|
6
|
+
speed: number;
|
|
7
|
+
airborne: boolean;
|
|
8
|
+
reducedMotion: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type PoseLayer = (bones: Readonly<Record<BoneName, THREE.Bone>>, frame: PoseFrame) => void;
|
|
11
|
+
/** Gives a character its layer (null takes it off). */
|
|
12
|
+
export declare function setPoseLayer(character: object, layer: PoseLayer | null): void;
|
|
13
|
+
/** The character's layer, if it has one. */
|
|
14
|
+
export declare const poseLayerOf: (character: object) => PoseLayer | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const layers = new WeakMap();
|
|
2
|
+
/** Gives a character its layer (null takes it off). */
|
|
3
|
+
export function setPoseLayer(character, layer) {
|
|
4
|
+
if (layer)
|
|
5
|
+
layers.set(character, layer);
|
|
6
|
+
else
|
|
7
|
+
layers.delete(character);
|
|
8
|
+
}
|
|
9
|
+
/** The character's layer, if it has one. */
|
|
10
|
+
export const poseLayerOf = (character) => layers.get(character);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** One clip as the file holds it: whole numbers, each bone's first frame then each frame's change (`q`: the bone's index, then 4 numbers a frame). */
|
|
3
|
+
export type RawClip = {
|
|
4
|
+
n: number;
|
|
5
|
+
loop: boolean;
|
|
6
|
+
from?: number;
|
|
7
|
+
kind: "full" | "upper";
|
|
8
|
+
hold: number;
|
|
9
|
+
label: string;
|
|
10
|
+
q: number[][];
|
|
11
|
+
p?: number[];
|
|
12
|
+
};
|
|
13
|
+
export type RawClips = {
|
|
14
|
+
v: number;
|
|
15
|
+
fps: number;
|
|
16
|
+
bones: string[];
|
|
17
|
+
clips: Record<string, RawClip>;
|
|
18
|
+
};
|
|
19
|
+
export type Clip = {
|
|
20
|
+
/** Frames (a loop's last frame is its first, so it is not among them). */
|
|
21
|
+
n: number;
|
|
22
|
+
loop: boolean;
|
|
23
|
+
/** The frame a loop goes back to (0: the whole clip loops; more: an opening that plays once, then the loop). */
|
|
24
|
+
from: number;
|
|
25
|
+
kind: "full" | "upper";
|
|
26
|
+
hold: number;
|
|
27
|
+
/** A one-shot's length in seconds. */
|
|
28
|
+
seconds: number;
|
|
29
|
+
turns: {
|
|
30
|
+
bone: string;
|
|
31
|
+
q: Float32Array;
|
|
32
|
+
}[];
|
|
33
|
+
/** The hips' place from their rest place, three numbers a frame (or one place), or null. */
|
|
34
|
+
hips: Float32Array | null;
|
|
35
|
+
};
|
|
36
|
+
/** Fades, in seconds: in at the start, out at a one-shot's end, and out when a move stops it. */
|
|
37
|
+
export declare const FADE_IN = 0.12;
|
|
38
|
+
export declare const FADE_END = 0.22;
|
|
39
|
+
export declare const FADE_STOP = 0.18;
|
|
40
|
+
/**
|
|
41
|
+
* Loads the clips, once (about 84 kB, 19 kB gzipped). Call it when the wheel first opens; a clip that plays before calls it too, and starts when they
|
|
42
|
+
* arrive. A failed load (the network) may be tried again.
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadEmotes(): Promise<void>;
|
|
45
|
+
/** True once the clips are loaded. */
|
|
46
|
+
export declare const emotesLoaded: () => boolean;
|
|
47
|
+
/** A loaded clip by its id, or null. */
|
|
48
|
+
export declare const emoteClip: (id: string) => Clip | null;
|
|
49
|
+
export declare function decodeClips(raw: RawClips): Map<string, Clip>;
|
|
50
|
+
/** What one character keeps between frames: the clip on show, its age (s), when a move or `stop` stopped it (its age then, or -1), and why it ended. */
|
|
51
|
+
export type ClipRun = {
|
|
52
|
+
id: string | null;
|
|
53
|
+
age: number;
|
|
54
|
+
stopAge: number;
|
|
55
|
+
stopWhy: "move" | "stop";
|
|
56
|
+
waiting: boolean;
|
|
57
|
+
ended: "end" | "move" | "stop" | null;
|
|
58
|
+
};
|
|
59
|
+
export declare const newRun: () => ClipRun;
|
|
60
|
+
/**
|
|
61
|
+
* Starts a clip on a run, `age` seconds in (a remote player's emote that began a little before). A loop already on show, and not stopping, goes on
|
|
62
|
+
* through its own repeat: a player's loop is sent again for those who come later.
|
|
63
|
+
*/
|
|
64
|
+
export declare function startClip(run: ClipRun, id: string, age?: number): void;
|
|
65
|
+
/** Stops the clip on show with its fade (`stop` from the game). */
|
|
66
|
+
export declare function stopClip(run: ClipRun): void;
|
|
67
|
+
export type ClipFrame = {
|
|
68
|
+
/** The step (s). */
|
|
69
|
+
dt: number;
|
|
70
|
+
moving: boolean;
|
|
71
|
+
air: boolean;
|
|
72
|
+
swim: boolean;
|
|
73
|
+
calm: boolean;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* One frame of the clip on a character: `bones` by name, already posed by the solver. Returns whether the clip showed. When it ends (its end, a move,
|
|
77
|
+
* the water or `stopClip`), `run.id` goes back to null and `run.ended` says why.
|
|
78
|
+
*/
|
|
79
|
+
export declare function stepClip(run: ClipRun, bones: Readonly<Record<string, THREE.Bone>>, o: ClipFrame): boolean;
|