@diffenderfer-games/hub 2.0.0-next.6 → 2.0.0-next.7

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 (38) hide show
  1. package/CHANGELOG.md +39 -28
  2. package/dist/avatar-pixi.js +1 -1
  3. package/dist/avatar-svg.js +1 -1
  4. package/dist/avatar.d.ts +2 -2
  5. package/dist/avatar.js +3 -3
  6. package/dist/doctor/cli.mjs +1 -1
  7. package/dist/shared/art.js +145 -3
  8. package/dist/shared/art.js.map +1 -1
  9. package/dist/shared/avatar.d.ts +3 -3
  10. package/dist/shared/client-version.gen.js +1 -1
  11. package/dist/shared/daily.js +1 -1
  12. package/dist/shared/describe.d.ts.map +1 -1
  13. package/dist/shared/describe.js +9 -1
  14. package/dist/shared/describe.js.map +1 -1
  15. package/dist/shared/index.d.ts +2 -2
  16. package/dist/shared/live-canvas.js +1 -1
  17. package/dist/shared/overlays.js +1 -1
  18. package/dist/shared/pose-key.js +1 -1
  19. package/dist/shared/svg-markup.js +112 -27
  20. package/dist/shared/svg-markup.js.map +1 -1
  21. package/dist/shared/svg.js +1 -1
  22. package/package.json +2 -2
  23. package/src/avatar/adapters/pixi-crowd.ts +302 -302
  24. package/src/avatar/adapters/pixi-types.ts +84 -84
  25. package/src/avatar/adapters/three.ts +126 -126
  26. package/src/avatar/art/glasses.ts +61 -3
  27. package/src/avatar/art/shape.ts +22 -3
  28. package/src/avatar/describe.ts +4 -1
  29. package/src/avatar/geom.ts +5 -0
  30. package/src/avatar/render/compose.ts +67 -22
  31. package/src/avatar/render/eyewear.ts +75 -13
  32. package/src/avatar-three.ts +18 -18
  33. package/src/avatar.ts +2 -2
  34. package/src/core/avatar/hub-avatar.ts +308 -308
  35. package/src/core/hub-progress.ts +269 -269
  36. package/src/core/page-account.ts +121 -121
  37. package/src/core/version/client-version.gen.ts +1 -1
  38. package/src/overlay/overlay.ts +3 -3
@@ -3,9 +3,9 @@
3
3
  * y down, the head centre near (0, −96) (§14.2). Layers go back to front as
4
4
  * in §3.2, with facial hair split into beard and moustache layers.
5
5
  */
6
- import { partName, type AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
6
+ import { partName, resolveColour, type AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
7
7
  import type { ArtSet } from '../art';
8
- import type { HeadwearPart } from '../art/shape';
8
+ import type { HeadwearPart, Shape } from '../art/shape';
9
9
  import { currentArt } from '../art-store';
10
10
  import { EXPRESSIONS } from '../face';
11
11
  import { n, type Warp } from '../geom';
@@ -13,13 +13,16 @@ import { g, path, rect, rot, sc, tr, type Clip, type SceneNode } from '../scene'
13
13
  import { bodyLayout, groundShadow } from './body';
14
14
  import { accessoryColour, lookColours, tones, type Colours } from './colours';
15
15
  import { HEAD_PATH, LINE, paintShapes, SKULL_R, turnWarp, type Ctx } from './context';
16
- import { glasses, goggles, gogglesStrapBack } from './eyewear';
16
+ import { FRAME_BLACK, glasses, goggles, gogglesStrapBack, type LensGlass } from './eyewear';
17
17
  import { brow, eye } from './eyes';
18
18
  import { cheeks, ears, head, type EarExtra } from './head';
19
19
  import { extras, mouth, nose } from './mouth';
20
20
  import { crown, handProp, headset } from './props';
21
21
  import type { Anchor, Facing, Point, Pose, RenderOptions } from './types';
22
22
 
23
+ /** The lens tint drawn with a mirror finish. */
24
+ const MIRROR_TINT = 'p:lens-mirror';
25
+
23
26
  /** The head centre's height above the feet. */
24
27
  export const HEAD_Y = -96;
25
28
 
@@ -49,6 +52,7 @@ interface Parts {
49
52
  readonly beard: ArtSet['beard'][string] | undefined;
50
53
  readonly glasses: ArtSet['glasses'][string] | undefined;
51
54
  readonly glassesColour: string;
55
+ readonly lensGlass: LensGlass;
52
56
  readonly freckles: boolean;
53
57
  readonly rosy: boolean;
54
58
  readonly earExtra: EarExtra | undefined;
@@ -68,7 +72,8 @@ function resolveParts(look: AvatarLook, art: ArtSet): Parts {
68
72
  hatColour: accessoryColour(look, hatAcc),
69
73
  beard: art.beard[partName(look.facialHair.style)],
70
74
  glasses: glassAcc ? art.glasses[partName(glassAcc.id)] : undefined,
71
- glassesColour: glassAcc?.colour === undefined ? '#1d1a1e' : accessoryColour(look, glassAcc),
75
+ glassesColour: glassAcc?.colour === undefined ? FRAME_BLACK : accessoryColour(look, glassAcc),
76
+ lensGlass: glassAcc?.tint === undefined ? {} : { tint: resolveColour(glassAcc.tint), ...(glassAcc.tint === MIRROR_TINT ? { isMirror: true } : {}) },
72
77
  freckles: face?.id === 'face.freckles',
73
78
  rosy: face?.id === 'face.rosy',
74
79
  earExtra: earAcc ? { kind: earAcc.id === 'ears.hoops' ? 'hoops' : 'studs', colour: earAcc.colour === undefined ? '#f5c542' : accessoryColour(look, earAcc) } : undefined,
@@ -89,8 +94,41 @@ function backOfHead(phi: number): { readonly crescent: Clip; readonly half: Clip
89
94
  };
90
95
  }
91
96
 
97
+ /**
98
+ * The shoulder line in head space: hair below it hangs behind the body and
99
+ * shoulders, hair above it (or over the face) stays over the head (§14.6
100
+ * "Long hair"). The two halves overlap by a unit so the seam never shows.
101
+ */
102
+ export const SHOULDER_Y = 36;
103
+ const ABOVE_SHOULDERS: Clip = { shapes: [rect(-200, -200, 400, 200 + SHOULDER_Y + 1, {}), path(HEAD_PATH.d(), {})] };
104
+ const BELOW_SHOULDERS: Clip = { shapes: [rect(-200, SHOULDER_Y - 1, 400, 300, {})] };
105
+
106
+ /** True when a layer reaches below the shoulder line. */
107
+ function hangs(shapes: readonly Shape[] | undefined): boolean {
108
+ return shapes?.some((s) => s.d.bottom() > SHOULDER_Y) === true;
109
+ }
110
+
111
+ /** A head's drawing: what goes behind the body (hanging hair) and the head itself. */
112
+ interface HeadNodes {
113
+ readonly behindBody: SceneNode[];
114
+ readonly head: SceneNode[];
115
+ }
116
+
117
+ /** Splits a hair layer at the shoulders: the part above stays in `over`, the part below goes in `behind`. */
118
+ function splitAtShoulders(nodes: readonly SceneNode[], doesHang: boolean, over: SceneNode[], behind: SceneNode[]): void {
119
+ if (nodes.length === 0) {
120
+ return;
121
+ }
122
+ if (!doesHang) {
123
+ over.push(...nodes);
124
+ return;
125
+ }
126
+ over.push(g(nodes, { clip: ABOVE_SHOULDERS }));
127
+ behind.push(g(nodes, { clip: BELOW_SHOULDERS }));
128
+ }
129
+
92
130
  /** The front and side views of the head. */
93
- function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'], angle: number, gazeX: number): SceneNode[] {
131
+ function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'], angle: number, gazeX: number): HeadNodes {
94
132
  const hatCut: Clip | undefined = p.hat && typeof p.hat.hides === 'number' ? { shapes: [rect(-120, p.hat.hides, 240, 260, {})] } : undefined;
95
133
  const isHairShown = !(p.hat && p.hat.hides === 'all');
96
134
  const cut = (nodes: SceneNode[]): SceneNode[] => (hatCut && nodes.length > 0 ? [g(nodes, { clip: hatCut })] : nodes);
@@ -98,13 +136,15 @@ function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'
98
136
  const hatTones = tones(p.hatColour);
99
137
  const warpFront = turnWarp((angle + gazeX * 6) * DEG);
100
138
  const warpBack: Warp | undefined = ctx.phi === 0 ? undefined : (px, py) => [px - Math.sin(ctx.phi) * 8, py];
139
+ const behindBody: SceneNode[] = [];
101
140
  const out: SceneNode[] = [];
141
+ if (isHairShown) {
142
+ // The hair behind the head is behind the body too: it hangs down the back.
143
+ behindBody.push(...cut(paintShapes(ctx.c, hairTie, p.hair?.back, warpBack)));
144
+ }
102
145
  if (p.hat?.back) {
103
146
  out.push(...paintShapes(ctx.c, hatTones, p.hat.back, warpBack));
104
147
  }
105
- if (isHairShown) {
106
- out.push(...cut(paintShapes(ctx.c, hairTie, p.hair?.back, warpBack)));
107
- }
108
148
  if (!p.hat?.hidesEars) {
109
149
  out.push(...ears(ctx, 'behind', p.earExtra));
110
150
  }
@@ -114,13 +154,13 @@ function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'
114
154
  // A turned head shows the back of the hair (the rear art) on the far side (§14.2).
115
155
  const co = Math.cos(ctx.phi);
116
156
  const shift: Warp = (px, py) => [px * (0.92 + 0.08 * co) - turnS * 5, py];
117
- const rear = [
118
- ...(isHairShown ? cut(paintShapes(ctx.c, hairTie, p.hair?.rear, shift)) : []),
119
- ...(p.hat ? paintShapes(ctx.c, hatTones, p.hat.rear, shift) : []),
120
- ];
121
157
  const clips = backOfHead(ctx.phi);
122
- if (rear.length > 0) {
123
- out.push(g([g(rear, { clip: clips.crescent })], { clip: clips.half }));
158
+ const onFarSide = (nodes: SceneNode[]): SceneNode[] => (nodes.length > 0 ? [g([g(nodes, { clip: clips.crescent })], { clip: clips.half })] : []);
159
+ if (isHairShown) {
160
+ splitAtShoulders(onFarSide(cut(paintShapes(ctx.c, hairTie, p.hair?.rear, shift))), hangs(p.hair?.rear), out, behindBody);
161
+ }
162
+ if (p.hat) {
163
+ out.push(...onFarSide(paintShapes(ctx.c, hatTones, p.hat.rear, shift)));
124
164
  }
125
165
  }
126
166
  if (!p.hat?.hidesEars) {
@@ -139,7 +179,7 @@ function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'
139
179
  out.push(...nose(ctx), ...mouth(ctx));
140
180
  out.push(...paintShapes(ctx.c, hairTie, p.beard?.moustache, faceWarp));
141
181
  if (isHairShown) {
142
- out.push(...cut(paintShapes(ctx.c, hairTie, p.hair?.front, warpFront)));
182
+ splitAtShoulders(cut(paintShapes(ctx.c, hairTie, p.hair?.front, warpFront)), hangs(p.hair?.front), out, behindBody);
143
183
  if (p.hair?.front?.some((s) => s.clip === 'head')) {
144
184
  out.push(path(HEAD_PATH.d(), { fill: 'none', stroke: ctx.c.skinInk, sw: LINE }));
145
185
  }
@@ -147,7 +187,7 @@ function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'
147
187
  if (isPlaying) {
148
188
  out.push(...goggles(ctx));
149
189
  } else if (p.glasses) {
150
- out.push(...glasses(ctx, p.glasses, p.glassesColour));
190
+ out.push(...glasses(ctx, p.glasses, p.glassesColour, p.lensGlass));
151
191
  }
152
192
  if (p.hat) {
153
193
  out.push(...paintShapes(ctx.c, hatTones, p.hat.front, warpFront));
@@ -156,11 +196,11 @@ function frontHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'
156
196
  out.push(...headset(ctx, false));
157
197
  }
158
198
  out.push(...extras(ctx));
159
- return out;
199
+ return { behindBody, head: out };
160
200
  }
161
201
 
162
202
  /** The back views (N, NE): the rear art wrapped past 180°. */
163
- function backHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'], angle: number): SceneNode[] {
203
+ function backHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status'], angle: number): HeadNodes {
164
204
  const phi = (angle - 180) * DEG;
165
205
  const warp = turnWarp(phi);
166
206
  const isHairShown = !(p.hat && p.hat.hides === 'all');
@@ -186,7 +226,8 @@ function backHead(ctx: Ctx, p: Parts, isPlaying: boolean, status: Pose['status']
186
226
  if (status === 'headset') {
187
227
  out.push(...headset(ctx, true));
188
228
  }
189
- return out;
229
+ // From behind, long hair falls down the back, over the body.
230
+ return { behindBody: [], head: out };
190
231
  }
191
232
 
192
233
  /** The grey silhouette: a malformed code, a hidden player, or before the art loads. */
@@ -236,15 +277,19 @@ export function composeAvatar(look: AvatarLook | null, opts: RenderOptions = {})
236
277
  const headY = HEAD_Y + b.headDy + (pose.bob?.y ?? 0);
237
278
  const tilt = face.headTilt + b.tilt + (pose.tilt ?? 0);
238
279
  const sq = face.squash + (pose.squash ?? 0);
239
- const headNodes = isBack ? backHead(ctx, parts, isPlaying, status, angle) : frontHead(ctx, parts, isPlaying, status, angle, gaze.x);
280
+ const drawn = isBack ? backHead(ctx, parts, isPlaying, status, angle) : frontHead(ctx, parts, isPlaying, status, angle, gaze.x);
281
+ const headNodes = drawn.head;
240
282
  const top = headY - 50 - Math.max(parts.hair?.top ?? 0, parts.hat?.top ?? 0);
241
283
  if (status === 'crown') {
242
284
  headNodes.push(...crown(Math.max(parts.hair?.top ?? 0, parts.hat?.top ?? 0)));
243
285
  }
244
286
  const handProps = status && !isBack ? handProp(status, b.prop.x, b.prop.y, parts.c.body, parts.c.bodyInk) : [];
287
+ const headTf = [tr(headX, headY), rot(tilt), sc(1 + sq, 1 - sq)];
245
288
  const character: SceneNode[] = [
246
- ...b.behind, ...b.body,
247
- g(headNodes, { tf: [tr(headX, headY), rot(tilt), sc(1 + sq, 1 - sq)] }),
289
+ ...b.behind,
290
+ ...(drawn.behindBody.length > 0 ? [g(drawn.behindBody, { tf: headTf })] : []),
291
+ ...b.body,
292
+ g(headNodes, { tf: headTf }),
248
293
  ...b.front, ...handProps,
249
294
  ];
250
295
  const lifted = b.lift > 0 ? [g(character, { tf: [tr(0, -b.lift)] })] : character;
@@ -1,31 +1,93 @@
1
1
  /** Glasses and the `goggles` status prop, both following the head turn. */
2
2
  import type { GlassesPart } from '../art/shape';
3
- import { ink } from '../colour';
3
+ import { deltaE, ink, lightness, outline } from '../colour';
4
4
  import { blob, Path } from '../geom';
5
- import { g, line, path, sc, tr, type SceneNode } from '../scene';
5
+ import { g, line, path, rect, sc, tr, type SceneNode } from '../scene';
6
6
  import { feat, LINE, turnWarp, type Ctx } from './context';
7
7
 
8
- /** Glasses: arms toward the ears, a lens at each eye, and the bridge. */
9
- export function glasses(ctx: Ctx, part: GlassesPart, frame: string): SceneNode[] {
8
+ /** What fills a lens: clear glass, a tint, or a mirror finish. */
9
+ export interface LensGlass {
10
+ /** The tint colour; absent for clear glass. */
11
+ readonly tint?: string;
12
+ readonly isMirror?: boolean;
13
+ }
14
+
15
+ /** The default frame colour. */
16
+ export const FRAME_BLACK = '#1d1a1e';
17
+
18
+ const SHINE = 'M-6 -4L-2 -7';
19
+ const MIRROR_STREAKS = 'M-9 7L3 -11M-1 10L9 -4';
20
+
21
+ /** The frame's colour and line width, and the dark edge a frame gets when it is light or close to the skin. */
22
+ interface FrameInk {
23
+ readonly colour: string;
24
+ readonly sw: number;
25
+ readonly edge: string | undefined;
26
+ }
27
+
28
+ /** A frame stroke along a path, over its edge when it has one. */
29
+ function frameLine(d: string, frame: FrameInk, sw = frame.sw): SceneNode[] {
30
+ return [...(frame.edge === undefined ? [] : [path(d, line(frame.edge, sw + 1.6))]), path(d, line(frame.colour, sw))];
31
+ }
32
+
33
+ /** One lens (or visor) in its own coordinates: the glass, then the frame. */
34
+ function lensNodes(d: string, glass: LensGlass, frame: FrameInk, shines: readonly (readonly [number, number])[]): SceneNode[] {
35
+ const out: SceneNode[] = [];
36
+ const clip = { shapes: [path(d, {})] };
37
+ if (glass.tint === undefined) {
38
+ out.push(path(d, { fill: '#ffffff', fillOpacity: 0.16 }));
39
+ } else if (glass.isMirror === true) {
40
+ out.push(path(d, { fill: glass.tint }), g([
41
+ rect(-60, 2, 120, 40, { fill: outline(glass.tint), opacity: 0.75 }),
42
+ ...shines.map(([px, py]) => path(MIRROR_STREAKS, line('#ffffff', 2.6, 'round', 0.85), [tr(px, py)])),
43
+ ], { clip }));
44
+ } else {
45
+ // Dark enough to read as sunglasses, light enough that winks show through.
46
+ out.push(path(d, { fill: glass.tint, fillOpacity: 0.86 }), g([rect(-60, 3, 120, 40, { fill: '#ffffff', opacity: 0.12 })], { clip }));
47
+ }
48
+ out.push(...frameLine(d, frame));
49
+ out.push(...shines.map(([px, py]) => path(SHINE, line('#ffffff', 1.8, 'round', glass.tint === undefined ? 0.7 : 0.9), [tr(px, py)])));
50
+ return out;
51
+ }
52
+
53
+ /** Glasses: arms toward the ears, a lens at each eye (or one visor), and the bridge. */
54
+ export function glasses(ctx: Ctx, part: GlassesPart, colour: string, glass: LensGlass): SceneNode[] {
10
55
  const out: SceneNode[] = [];
11
56
  const warp = turnWarp(ctx.phi, ctx.dy);
12
- for (const side of [-1, 1] as const) {
13
- if (feat(ctx, 30 * side).vis) {
14
- out.push(path(new Path().m(30 * side, 2).l(45 * side, 0).d(warp), line(frame, 2.6)));
57
+ const isEdged = lightness(colour) > 0.7 || deltaE(colour, ctx.c.skin) < 24;
58
+ const frame: FrameInk = { colour, sw: part.frame ?? 3.2, edge: isEdged ? ink(colour) : undefined };
59
+ const tinted: LensGlass = part.tint === undefined ? {} : { tint: glass.tint ?? part.tint, ...(glass.isMirror === true ? { isMirror: true } : {}) };
60
+ if (part.isSingle !== true) {
61
+ for (const side of [-1, 1] as const) {
62
+ if (feat(ctx, 30 * side).vis) {
63
+ out.push(...frameLine(new Path().m(30 * side, 2).l(45 * side, 0).d(warp), frame, 2.6));
64
+ }
15
65
  }
16
66
  }
67
+ if (part.visor) {
68
+ const shines = [feat(ctx, -17), feat(ctx, 17)].filter((f) => f.vis).map((f) => [f.x, 4 + ctx.dy] as const);
69
+ out.push(...lensNodes(part.visor.d(warp), tinted, frame, shines));
70
+ return out;
71
+ }
72
+ const lens = part.lens;
17
73
  for (const side of [-1, 1] as const) {
18
74
  const f = feat(ctx, 17 * side);
19
- if (!f.vis) {
75
+ if (!lens || !f.vis || (part.isSingle === true && side < 0)) {
20
76
  continue;
21
77
  }
78
+ const flip = part.isMirrored === true && side < 0 ? -1 : 1;
22
79
  out.push(g([
23
- path(part.lens.d(), { fill: '#ffffff', fillOpacity: 0.16, stroke: frame, sw: 3.2 }),
24
- path('M-6 -4L-2 -7', line('#ffffff', 1.8, 'round', 0.7)),
25
- ], { tf: [tr(f.x, 4 + ctx.dy), sc(f.sx, 1)] }));
80
+ ...lensNodes(lens.d(), tinted, frame, [[0, 0]]),
81
+ ...(part.bar ? frameLine(part.bar.d(), frame, 4.4) : []),
82
+ ], { tf: [tr(f.x, 4 + ctx.dy), sc(f.sx * flip, 1)] }));
83
+ }
84
+ if (part.chain && feat(ctx, 30).vis) {
85
+ out.push(path(part.chain.d(warp), line(colour, 1.6, 'round', 0.9)));
86
+ }
87
+ if (part.bridge) {
88
+ const fb = feat(ctx, 0);
89
+ out.push(g(frameLine(part.bridge.d(), frame, 2.8), { tf: [tr(fb.x, 4 + ctx.dy), sc(fb.sx, 1)] }));
26
90
  }
27
- const fb = feat(ctx, 0);
28
- out.push(g([path(part.bridge.d(), line(frame, 2.8))], { tf: [tr(fb.x, 4 + ctx.dy), sc(fb.sx, 1)] }));
29
91
  return out;
30
92
  }
31
93
 
@@ -1,18 +1,18 @@
1
- /**
2
- * `@diffenderfer-games/hub/avatar/three`: avatars in three.js, as a
3
- * `CanvasTexture` or a billboard `Sprite`. Pass your THREE namespace (the hub
4
- * has no three dependency):
5
- *
6
- * ```ts
7
- * import * as THREE from 'three';
8
- * import { ready } from '@diffenderfer-games/hub/avatar';
9
- * import { avatarSprite } from '@diffenderfer-games/hub/avatar/three';
10
- *
11
- * await ready();
12
- * const { sprite } = avatarSprite(THREE, me.look, { frame: 'head', size: 128, height: 0.4 });
13
- * helmet.add(sprite);
14
- * ```
15
- *
16
- * @packageDocumentation
17
- */
18
- export { avatarCanvasTexture, avatarSprite, type ThreeAvatarOptions, type ThreeAvatarTexture, type ThreeNs, type ThreeTextureNs } from './avatar/adapters/three';
1
+ /**
2
+ * `@diffenderfer-games/hub/avatar/three`: avatars in three.js, as a
3
+ * `CanvasTexture` or a billboard `Sprite`. Pass your THREE namespace (the hub
4
+ * has no three dependency):
5
+ *
6
+ * ```ts
7
+ * import * as THREE from 'three';
8
+ * import { ready } from '@diffenderfer-games/hub/avatar';
9
+ * import { avatarSprite } from '@diffenderfer-games/hub/avatar/three';
10
+ *
11
+ * await ready();
12
+ * const { sprite } = avatarSprite(THREE, me.look, { frame: 'head', size: 128, height: 0.4 });
13
+ * helmet.add(sprite);
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ export { avatarCanvasTexture, avatarSprite, type ThreeAvatarOptions, type ThreeAvatarTexture, type ThreeNs, type ThreeTextureNs } from './avatar/adapters/three';
package/src/avatar.ts CHANGED
@@ -24,8 +24,8 @@ import { renderElement } from './avatar/outputs/element';
24
24
  import { renderSvg } from './avatar/outputs/svg';
25
25
 
26
26
  export {
27
- ACCESSORY_SLOTS, CATALOG, CATALOG_REVISION, DEFAULT_GEN, LOOK_CODE_PREFIX, PALETTES,
28
- canonicalLook, decodeLook, defaultLook, distinctLooks, encodeLook, isLookCode, isValidLook, lookProblem,
27
+ ACCESSORY_SLOTS, CATALOG, CATALOG_REVISION, DEFAULT_GEN, FRAME_PRESETS, LOOK_CODE_PREFIX, PALETTES, SECOND_COLOUR_CODE,
28
+ canonicalLook, decodeLook, defaultLook, distinctLooks, encodeLook, isLookCode, isTinted, isValidLook, lookProblem,
29
29
  normaliseHex, partById, partName, partsOf, randomLook, resolveColour, sameLook, seededRng,
30
30
  type AccessorySlot, type AvatarLook, type CatalogEntry, type DecodedLook, type LookAccessory, type LookColour,
31
31
  type LookPart, type LookProblem, type PaletteName, type PartCategory, type Preset, type RandomLookOptions, type Rng,