@diffenderfer-games/hub 2.0.0-next.5 → 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 (170) hide show
  1. package/CHANGELOG.md +65 -19
  2. package/README.md +3 -6
  3. package/dist/auto.d.ts +5 -5
  4. package/dist/auto.js +2 -2
  5. package/dist/avatar-pixi.d.ts +147 -0
  6. package/dist/avatar-pixi.d.ts.map +1 -0
  7. package/dist/avatar-pixi.js +280 -0
  8. package/dist/avatar-pixi.js.map +1 -0
  9. package/dist/avatar-svg.d.ts +4 -0
  10. package/dist/avatar-svg.js +5 -0
  11. package/dist/avatar-three.d.ts +69 -0
  12. package/dist/avatar-three.d.ts.map +1 -0
  13. package/dist/avatar-three.js +49 -0
  14. package/dist/avatar-three.js.map +1 -0
  15. package/dist/avatar.d.ts +7 -0
  16. package/dist/avatar.js +487 -0
  17. package/dist/avatar.js.map +1 -0
  18. package/dist/daily.d.ts +2 -2
  19. package/dist/doctor/cli.mjs +1 -1
  20. package/dist/index.d.ts +5 -5
  21. package/dist/index.js +2 -2
  22. package/dist/input.d.ts +2 -2
  23. package/dist/shared/animator.d.ts +104 -0
  24. package/dist/shared/animator.d.ts.map +1 -0
  25. package/dist/shared/art.js +1946 -0
  26. package/dist/shared/art.js.map +1 -0
  27. package/dist/shared/atlas.d.ts +134 -0
  28. package/dist/shared/atlas.d.ts.map +1 -0
  29. package/dist/shared/avatar.d.ts +117 -0
  30. package/dist/shared/avatar.d.ts.map +1 -0
  31. package/dist/shared/bridge.js +13 -5
  32. package/dist/shared/bridge.js.map +1 -1
  33. package/dist/shared/client-version.gen.js +1 -1
  34. package/dist/shared/daily.d.ts +129 -18
  35. package/dist/shared/daily.d.ts.map +1 -1
  36. package/dist/shared/daily.js +120 -26
  37. package/dist/shared/daily.js.map +1 -1
  38. package/dist/shared/defineProperty.js +39 -0
  39. package/dist/shared/describe.d.ts +32 -0
  40. package/dist/shared/describe.d.ts.map +1 -0
  41. package/dist/shared/describe.js +109 -0
  42. package/dist/shared/describe.js.map +1 -0
  43. package/dist/shared/globals.js +1 -39
  44. package/dist/shared/globals.js.map +1 -1
  45. package/dist/shared/index.d.ts +503 -14
  46. package/dist/shared/index.d.ts.map +1 -1
  47. package/dist/shared/input.d.ts +31 -13
  48. package/dist/shared/input.d.ts.map +1 -1
  49. package/dist/shared/input.js +40 -2
  50. package/dist/shared/input.js.map +1 -1
  51. package/dist/shared/json-guards.js +48 -0
  52. package/dist/shared/json-guards.js.map +1 -0
  53. package/dist/shared/live-canvas.js +62 -0
  54. package/dist/shared/live-canvas.js.map +1 -0
  55. package/dist/shared/mp.js +1 -1
  56. package/dist/shared/overlays.js +800 -0
  57. package/dist/shared/overlays.js.map +1 -0
  58. package/dist/shared/page-stack.js +2 -1
  59. package/dist/shared/page-stack.js.map +1 -1
  60. package/dist/shared/pose-key.js +249 -0
  61. package/dist/shared/pose-key.js.map +1 -0
  62. package/dist/shared/race.js +1 -1
  63. package/dist/shared/rooms.js +1 -1
  64. package/dist/shared/scene.d.ts +90 -0
  65. package/dist/shared/scene.d.ts.map +1 -0
  66. package/dist/shared/social.d.ts +55 -4
  67. package/dist/shared/social.d.ts.map +1 -1
  68. package/dist/shared/src.js +2020 -144
  69. package/dist/shared/src.js.map +1 -1
  70. package/dist/shared/svg-markup.js +1945 -0
  71. package/dist/shared/svg-markup.js.map +1 -0
  72. package/dist/shared/svg.js +27 -0
  73. package/dist/shared/svg.js.map +1 -0
  74. package/dist/shared/types.d.ts +166 -0
  75. package/dist/shared/types.d.ts.map +1 -0
  76. package/dist/social.d.ts +2 -2
  77. package/dist/social.js +2 -2
  78. package/dist/turns.d.ts +39 -0
  79. package/dist/turns.d.ts.map +1 -0
  80. package/dist/turns.js +117 -0
  81. package/dist/turns.js.map +1 -0
  82. package/package.json +33 -3
  83. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  84. package/src/avatar/adapters/pixi-crowd.ts +302 -0
  85. package/src/avatar/adapters/pixi-types.ts +84 -0
  86. package/src/avatar/adapters/three.ts +126 -0
  87. package/src/avatar/animator/animator.ts +285 -0
  88. package/src/avatar/animator/reactions.ts +44 -0
  89. package/src/avatar/animator/schedule.ts +73 -0
  90. package/src/avatar/art/beard.ts +49 -0
  91. package/src/avatar/art/glasses.ts +67 -0
  92. package/src/avatar/art/hair.ts +254 -0
  93. package/src/avatar/art/headwear.ts +75 -0
  94. package/src/avatar/art/index.ts +21 -0
  95. package/src/avatar/art/shape.ts +87 -0
  96. package/src/avatar/art-store.ts +36 -0
  97. package/src/avatar/atlas.ts +236 -0
  98. package/src/avatar/catalog-view.ts +14 -0
  99. package/src/avatar/colour.ts +109 -0
  100. package/src/avatar/describe.ts +79 -0
  101. package/src/avatar/face.ts +187 -0
  102. package/src/avatar/geom.ts +173 -0
  103. package/src/avatar/layout.ts +47 -0
  104. package/src/avatar/look-like.ts +52 -0
  105. package/src/avatar/outputs/bitmap.ts +75 -0
  106. package/src/avatar/outputs/canvas-paint.ts +177 -0
  107. package/src/avatar/outputs/canvas.ts +62 -0
  108. package/src/avatar/outputs/custom-element.ts +140 -0
  109. package/src/avatar/outputs/element.ts +58 -0
  110. package/src/avatar/outputs/live-canvas.ts +57 -0
  111. package/src/avatar/outputs/overlay-svg.ts +22 -0
  112. package/src/avatar/outputs/pose-key.ts +30 -0
  113. package/src/avatar/outputs/svg-markup.ts +113 -0
  114. package/src/avatar/outputs/svg.ts +41 -0
  115. package/src/avatar/overlays.ts +128 -0
  116. package/src/avatar/render/body.ts +116 -0
  117. package/src/avatar/render/colours.ts +81 -0
  118. package/src/avatar/render/compose.ts +303 -0
  119. package/src/avatar/render/context.ts +117 -0
  120. package/src/avatar/render/eyes.ts +78 -0
  121. package/src/avatar/render/eyewear.ts +118 -0
  122. package/src/avatar/render/frame.ts +60 -0
  123. package/src/avatar/render/head.ts +81 -0
  124. package/src/avatar/render/mouth.ts +86 -0
  125. package/src/avatar/render/props.ts +91 -0
  126. package/src/avatar/render/types.ts +84 -0
  127. package/src/avatar/scene.ts +119 -0
  128. package/src/avatar-pixi.ts +20 -0
  129. package/src/avatar-svg.ts +21 -0
  130. package/src/avatar-three.ts +18 -0
  131. package/src/avatar.ts +62 -0
  132. package/src/core/avatar/avatar-cache.ts +95 -0
  133. package/src/core/avatar/avatar-ref.ts +34 -0
  134. package/src/core/avatar/avatar-types.ts +43 -0
  135. package/src/core/avatar/hub-avatar.ts +308 -0
  136. package/src/core/connection.ts +8 -0
  137. package/src/core/errors.ts +7 -1
  138. package/src/core/hub-progress.ts +39 -8
  139. package/src/core/hub.ts +174 -6
  140. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  141. package/src/core/offline/hub-kinds/index.ts +9 -0
  142. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  143. package/src/core/page-account.ts +121 -0
  144. package/src/core/rest/routes.gen.ts +31 -1
  145. package/src/core/resume/hub-resume.ts +39 -5
  146. package/src/core/resume/resume-types.ts +2 -2
  147. package/src/core/saved/daily-saves.ts +196 -0
  148. package/src/core/saved/daily-seed.ts +16 -0
  149. package/src/core/saved/hub-saved.ts +221 -0
  150. package/src/core/saved/local-saved.ts +122 -0
  151. package/src/core/saved/saved-list.ts +129 -0
  152. package/src/core/saved/saved-open.ts +104 -0
  153. package/src/core/saved/saved-order.ts +49 -0
  154. package/src/core/saved/saved-types.ts +24 -0
  155. package/src/core/turns/hub-turns.ts +140 -0
  156. package/src/core/turns/turn-game.ts +388 -0
  157. package/src/core/version/client-version.gen.ts +2 -2
  158. package/src/daily/daily-system.ts +112 -13
  159. package/src/daily/page-daily.ts +2 -2
  160. package/src/daily/types.ts +46 -9
  161. package/src/doctor/hub-version-fetch.ts +43 -43
  162. package/src/index.ts +10 -1
  163. package/src/input/input-system.ts +10 -1
  164. package/src/input/navigator.ts +11 -0
  165. package/src/input.ts +1 -0
  166. package/src/overlay/overlay.ts +25 -1
  167. package/src/realtime/runtime-api.ts +14 -3
  168. package/src/social.ts +1 -0
  169. package/src/turns-kit/check-rules.ts +182 -0
  170. package/src/turns.ts +13 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The display list every renderer draws: a small tree of groups and shapes
3
+ * in rig units. The composer builds it once per pose; `toSvg` turns it into
4
+ * markup, `paintCanvas` draws it on a 2D canvas, and the Pixi and three.js
5
+ * adapters draw it through a canvas. Colours in a scene are always derived
6
+ * `#rrggbb` values or fixed constants, never caller text.
7
+ */
8
+
9
+ /** One transform step, applied in order (SVG semantics). */
10
+ export type TfOp =
11
+ | { readonly k: 't'; readonly x: number; readonly y: number }
12
+ | { readonly k: 's'; readonly x: number; readonly y: number }
13
+ | { readonly k: 'r'; readonly a: number }
14
+ | { readonly k: 'skx'; readonly a: number };
15
+
16
+ /** How a shape is painted. */
17
+ export interface Paint {
18
+ readonly fill?: string;
19
+ readonly fillOpacity?: number;
20
+ readonly stroke?: string;
21
+ readonly sw?: number;
22
+ readonly cap?: 'round' | 'butt';
23
+ readonly opacity?: number;
24
+ readonly evenOdd?: boolean;
25
+ }
26
+
27
+ /** A path shape. */
28
+ export interface PathNode extends Paint {
29
+ readonly k: 'path';
30
+ readonly d: string;
31
+ readonly tf?: readonly TfOp[];
32
+ }
33
+
34
+ /** An ellipse (a circle when rx = ry). */
35
+ export interface EllipseNode extends Paint {
36
+ readonly k: 'ellipse';
37
+ readonly cx: number;
38
+ readonly cy: number;
39
+ readonly rx: number;
40
+ readonly ry: number;
41
+ readonly tf?: readonly TfOp[];
42
+ }
43
+
44
+ /** A rectangle with optional rounded corners. */
45
+ export interface RectNode extends Paint {
46
+ readonly k: 'rect';
47
+ readonly x: number;
48
+ readonly y: number;
49
+ readonly w: number;
50
+ readonly h: number;
51
+ readonly r?: number;
52
+ readonly tf?: readonly TfOp[];
53
+ }
54
+
55
+ /** A line of text (overlay labels and FX glyphs). */
56
+ export interface TextNode extends Paint {
57
+ readonly k: 'text';
58
+ readonly x: number;
59
+ readonly y: number;
60
+ readonly text: string;
61
+ readonly size: number;
62
+ readonly font: 'mono' | 'pixel';
63
+ readonly anchor?: 'start' | 'middle';
64
+ readonly bold?: boolean;
65
+ }
66
+
67
+ /** A clip region: the union of its shapes (even-odd when asked). */
68
+ export interface Clip {
69
+ readonly shapes: readonly (PathNode | EllipseNode | RectNode)[];
70
+ readonly evenOdd?: boolean;
71
+ }
72
+
73
+ /** A group: a transform, an optional clip and opacity, and children. */
74
+ export interface GroupNode {
75
+ readonly k: 'g';
76
+ readonly tf?: readonly TfOp[];
77
+ readonly clip?: Clip;
78
+ readonly opacity?: number;
79
+ /** A soft light rim around the silhouette (the world, §14.2). */
80
+ readonly rim?: boolean;
81
+ readonly children: readonly SceneNode[];
82
+ }
83
+
84
+ /** Anything in a scene. */
85
+ export type SceneNode = GroupNode | PathNode | EllipseNode | RectNode | TextNode;
86
+
87
+ /** A group helper that drops empty groups' wrapping. */
88
+ export function g(children: readonly SceneNode[], opts: Omit<GroupNode, 'k' | 'children'> = {}): GroupNode {
89
+ return { k: 'g', ...opts, children };
90
+ }
91
+
92
+ /** A path helper. */
93
+ export function path(d: string, paint: Paint, tf?: readonly TfOp[]): PathNode {
94
+ return { k: 'path', d, ...paint, ...(tf ? { tf } : {}) };
95
+ }
96
+
97
+ /** An ellipse helper. */
98
+ export function ellipse(cx: number, cy: number, rx: number, ry: number, paint: Paint, tf?: readonly TfOp[]): EllipseNode {
99
+ return { k: 'ellipse', cx, cy, rx, ry, ...paint, ...(tf ? { tf } : {}) };
100
+ }
101
+
102
+ /** A rect helper. */
103
+ export function rect(left: number, top: number, w: number, h: number, paint: Paint, r?: number): RectNode {
104
+ return { k: 'rect', x: left, y: top, w, h, ...paint, ...(r === undefined ? {} : { r }) };
105
+ }
106
+
107
+ /** A translate step. */
108
+ export const tr = (dx: number, dy: number): TfOp => ({ k: 't', x: dx, y: dy });
109
+ /** A scale step (uniform when `sy` is omitted). */
110
+ export const sc = (sx: number, sy = sx): TfOp => ({ k: 's', x: sx, y: sy });
111
+ /** A rotate step, degrees. */
112
+ export const rot = (a: number): TfOp => ({ k: 'r', a });
113
+ /** A horizontal skew step, degrees. */
114
+ export const skewX = (a: number): TfOp => ({ k: 'skx', a });
115
+
116
+ /** A round-joined stroke paint. */
117
+ export function line(colour: string, width: number, cap: 'round' | 'butt' = 'round', opacity?: number): Paint {
118
+ return { fill: 'none', stroke: colour, sw: width, cap, ...(opacity === undefined ? {} : { opacity }) };
119
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@diffenderfer-games/hub/avatar/pixi`: avatars in PixiJS. Pass your PIXI
3
+ * namespace (the hub has no pixi dependency):
4
+ *
5
+ * ```ts
6
+ * import * as PIXI from 'pixi.js';
7
+ * import { ready, animator } from '@diffenderfer-games/hub/avatar';
8
+ * import { pixiAvatar } from '@diffenderfer-games/hub/avatar/pixi';
9
+ *
10
+ * await ready();
11
+ * const face = pixiAvatar(PIXI, seat.look, { size: 64, animator: animator(seat.look, { seed: seat.userId }) });
12
+ * stage.addChild(face.view);
13
+ * app.ticker.add(() => face.update(performance.now()));
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ export { pixiAvatar, avatarTexture, type PixiAvatar, type PixiAvatarOptions } from './avatar/adapters/pixi-avatar';
19
+ export { createCrowd, type AvatarCrowd, type CrowdMember, type CrowdOptions } from './avatar/adapters/pixi-crowd';
20
+ export type { PixiContainer, PixiNs, PixiSprite, PixiTexture } from './avatar/adapters/pixi-types';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@diffenderfer-games/hub/avatar/svg`: avatars as SVG markup with no DOM,
3
+ * for servers, workers and server-side rendering. The same renderer as
4
+ * `@diffenderfer-games/hub/avatar` (`render.svg`), without the DOM, canvas
5
+ * and animation outputs.
6
+ *
7
+ * ```ts
8
+ * import { ready, renderSvg } from '@diffenderfer-games/hub/avatar/svg';
9
+ *
10
+ * await ready();
11
+ * const markup = renderSvg(player.look, { size: 64, pose: { expression: 'happy' } });
12
+ * ```
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ export { decodeLook, defaultLook, encodeLook, type AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
17
+ export { ready } from './avatar/art-store';
18
+ export { EXPRESSION_NAMES, isExpression, type Expression } from './avatar/face';
19
+ export { renderSvg } from './avatar/outputs/svg';
20
+ export { describe } from './avatar/describe';
21
+ export type { Facing, Frame, LookLike, Pose, RenderOptions } from './avatar/render/types';
@@ -0,0 +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';
package/src/avatar.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `@diffenderfer-games/hub/avatar`: draw players' avatars in any game.
3
+ *
4
+ * ```ts
5
+ * import { ready, render, animator } from '@diffenderfer-games/hub/avatar';
6
+ *
7
+ * await ready(); // loads the art once
8
+ * el.innerHTML = render.svg(seat.look, { size: 48 });
9
+ * const anim = animator(seat.look, { seed: seat.userId });
10
+ * anim.express('happy');
11
+ * render.canvas(ctx, seat.look, { x, y, size: 64, pose: anim.pose(now) });
12
+ * ```
13
+ *
14
+ * The art loads lazily (`ready()`); before it does, renders draw the
15
+ * silhouette. Zero dependencies; the Pixi and three.js adapters take the
16
+ * library as an argument (`/avatar/pixi`, `/avatar/three`). Docs: "Avatars"
17
+ * in docs/hub.md.
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ import { renderBitmap, renderDataUrl } from './avatar/outputs/bitmap';
22
+ import { renderCanvas, renderCanvasAt } from './avatar/outputs/canvas';
23
+ import { renderElement } from './avatar/outputs/element';
24
+ import { renderSvg } from './avatar/outputs/svg';
25
+
26
+ export {
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
+ normaliseHex, partById, partName, partsOf, randomLook, resolveColour, sameLook, seededRng,
30
+ type AccessorySlot, type AvatarLook, type CatalogEntry, type DecodedLook, type LookAccessory, type LookColour,
31
+ type LookPart, type LookProblem, type PaletteName, type PartCategory, type Preset, type RandomLookOptions, type Rng,
32
+ } from '@diffenderfer-games/hub-contract/avatar';
33
+ export { ready, onArtReady } from './avatar/art-store';
34
+ export { animator, Animator, type AnimatorOptions, type At, type Feeling } from './avatar/animator/animator';
35
+ export { REACTIONS, type Reaction } from './avatar/animator/reactions';
36
+ export { EXPRESSIONS, EXPRESSION_NAMES, expressionFromEmote, isExpression, mix, type BrowParams, type Expression, type FaceExtra, type FaceParams, type MouthParams } from './avatar/face';
37
+ export { facingFrom, layout, lookToward } from './avatar/layout';
38
+ export { avatarEmoji, describe } from './avatar/describe';
39
+ export { defineAvatarElement } from './avatar/outputs/custom-element';
40
+ export type { AvatarElement } from './avatar/outputs/element';
41
+ export type { CanvasPlacement } from './avatar/outputs/canvas';
42
+ export { overlays, type ActivityIcon, type Overlay, type ReactionGlyph } from './avatar/overlays';
43
+ export { overlaySvg, paintOverlay } from './avatar/outputs/overlay-svg';
44
+ export { catalog } from './avatar/catalog-view';
45
+ export { composeAvatar, HEAD_Y, type Composed } from './avatar/render/compose';
46
+ export { paintNodes, type Ctx2D } from './avatar/outputs/canvas-paint';
47
+ export { sceneToSvg } from './avatar/outputs/svg-markup';
48
+ export { toCode, toLook } from './avatar/look-like';
49
+ export { contrast, deltaE, lightness } from './avatar/colour';
50
+ export type { SceneNode } from './avatar/scene';
51
+ export type { Anchor, BodyPose, Facing, Frame, LookDir, LookLike, Point, Pose, RenderOptions, StatusProp, View } from './avatar/render/types';
52
+ export { AvatarAtlas, createAtlas, type AtlasFrame, type AtlasOptions } from './avatar/atlas';
53
+
54
+ /** Every output: SVG markup, a live element, Canvas 2D, a bitmap or a data URL. */
55
+ export const render = {
56
+ svg: renderSvg,
57
+ element: renderElement,
58
+ canvas: renderCanvas,
59
+ canvasAt: renderCanvasAt,
60
+ bitmap: renderBitmap,
61
+ dataUrl: renderDataUrl,
62
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The device's avatar cache (avatars.md §5.6), so faces render offline:
3
+ * your own look (`hub:avatar:me`, read synchronously at startup) and an LRU
4
+ * of other players' codes (`hub:avatar:cache`, 200 entries).
5
+ */
6
+ import { isLookCode } from '@diffenderfer-games/hub-contract/avatar';
7
+ import { isRecord, optionalNumber, optionalString, parseJsonText } from '../storage/json-guards';
8
+ import type { KeyValueStore } from '../storage/key-value';
9
+
10
+ /** Your own look as cached. */
11
+ export interface CachedMine {
12
+ readonly userId: number;
13
+ readonly code: string;
14
+ readonly isDefault: boolean;
15
+ readonly updatedAt: number;
16
+ }
17
+
18
+ const MINE_KEY = 'hub:avatar:me';
19
+ const OTHERS_KEY = 'hub:avatar:cache';
20
+ const OTHERS_MAX = 200;
21
+
22
+ /** The device cache over a key-value store. */
23
+ export class AvatarCache {
24
+ private readonly store: KeyValueStore;
25
+ private others: Map<number, string> | null = null;
26
+
27
+ constructor(store: KeyValueStore) {
28
+ this.store = store;
29
+ }
30
+
31
+ /** Your cached look, or `null` (never cached, or another player's on this device). */
32
+ readMine(userId: number | null): CachedMine | null {
33
+ const parsed = parseJsonText(this.store.read(MINE_KEY) ?? '');
34
+ if (!isRecord(parsed)) {
35
+ return null;
36
+ }
37
+ const id = optionalNumber(parsed.userId);
38
+ const code = optionalString(parsed.code);
39
+ if (id === undefined || code === undefined || !isLookCode(code) || (userId !== null && id !== userId)) {
40
+ return null;
41
+ }
42
+ return { userId: id, code, isDefault: parsed.isDefault === true, updatedAt: optionalNumber(parsed.updatedAt) ?? 0 };
43
+ }
44
+
45
+ /** Remember your look. */
46
+ writeMine(mine: CachedMine): void {
47
+ this.store.write(MINE_KEY, JSON.stringify(mine));
48
+ this.remember(mine.userId, mine.code);
49
+ }
50
+
51
+ /** A player's cached code, or `undefined`. */
52
+ codeOf(userId: number): string | undefined {
53
+ return this.loadOthers().get(userId);
54
+ }
55
+
56
+ /** Remember a player's code (most recent last; the oldest drop past 200). */
57
+ remember(userId: number, code: string): void {
58
+ if (code === '') {
59
+ return;
60
+ }
61
+ const others = this.loadOthers();
62
+ if (others.get(userId) === code) {
63
+ return;
64
+ }
65
+ others.delete(userId);
66
+ others.set(userId, code);
67
+ while (others.size > OTHERS_MAX) {
68
+ const oldest = others.keys().next().value;
69
+ if (oldest === undefined) {
70
+ break;
71
+ }
72
+ others.delete(oldest);
73
+ }
74
+ this.store.write(OTHERS_KEY, JSON.stringify([...others]));
75
+ }
76
+
77
+ private loadOthers(): Map<number, string> {
78
+ if (this.others !== null) {
79
+ return this.others;
80
+ }
81
+ const others = new Map<number, string>();
82
+ const parsed = parseJsonText(this.store.read(OTHERS_KEY) ?? '[]');
83
+ if (Array.isArray(parsed)) {
84
+ for (const entry of parsed) {
85
+ const id = Array.isArray(entry) ? optionalNumber(entry[0]) : undefined;
86
+ const code = Array.isArray(entry) ? optionalString(entry[1]) : undefined;
87
+ if (id !== undefined && code !== undefined && isLookCode(code)) {
88
+ others.set(id, code);
89
+ }
90
+ }
91
+ }
92
+ this.others = others;
93
+ return others;
94
+ }
95
+ }
@@ -0,0 +1,34 @@
1
+ /** Building {@link AvatarRef}s from codes, payloads and seeds. */
2
+ import { decodeLook, defaultLook, encodeLook } from '@diffenderfer-games/hub-contract/avatar';
3
+ import type { AvatarRef } from './avatar-types';
4
+
5
+ /** A ref for a code (`''` = hidden). */
6
+ export function refOf(code: string, isDefault = false): AvatarRef {
7
+ if (code === '') {
8
+ return { code: '', look: null, isDefault: false, hidden: true };
9
+ }
10
+ return { code, look: decodeLook(code)?.look ?? null, isDefault, hidden: false };
11
+ }
12
+
13
+ /** A seeded default look as a ref (bots, previews, local pass-and-play seats). */
14
+ export function botRef(seed: string | number): AvatarRef {
15
+ const look = defaultLook(seed);
16
+ return { code: encodeLook(look), look, isDefault: true, hidden: false };
17
+ }
18
+
19
+ /** Anything that carries a player's look: a seat, a `PlayerLite`, a racer, a ticket user. */
20
+ export interface CarriesLook {
21
+ readonly look?: string;
22
+ readonly userId?: number;
23
+ readonly id?: number;
24
+ readonly uid?: number;
25
+ }
26
+
27
+ /** The ref a payload carries; an old hub's payload (no `look`) falls back to the id's default. */
28
+ export function refFrom(carrier: CarriesLook): AvatarRef {
29
+ if (carrier.look !== undefined) {
30
+ return refOf(carrier.look);
31
+ }
32
+ const id = carrier.userId ?? carrier.id ?? carrier.uid;
33
+ return id === undefined ? refOf('') : botRef(id);
34
+ }
@@ -0,0 +1,43 @@
1
+ /** The shapes `hub.avatar` works with (avatars.md §6). */
2
+ import type { AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
3
+
4
+ /**
5
+ * A player's avatar as games hold it: the code (pass it to any renderer),
6
+ * the decoded look, whether it is their generated default, and whether the
7
+ * player is hidden from you (draw the silhouette).
8
+ */
9
+ export interface AvatarRef {
10
+ readonly code: string;
11
+ /** `null` when hidden or the code is malformed (renderers draw the silhouette). */
12
+ readonly look: AvatarLook | null;
13
+ /** They never designed one: this is their generated default. */
14
+ readonly isDefault: boolean;
15
+ /** Hidden where the username is hidden (§2.8). */
16
+ readonly hidden: boolean;
17
+ }
18
+
19
+ /** A player's avatar changed (`hub.avatar.on('change')`). */
20
+ export interface AvatarChange {
21
+ readonly userId: number;
22
+ readonly ref: AvatarRef;
23
+ }
24
+
25
+ /** Options for the designer. */
26
+ export interface OpenDesignerOptions {
27
+ /** Why it opened (`'game'`, `'world'`, …), for metrics only. */
28
+ readonly reason?: string;
29
+ }
30
+
31
+ /** What the designer reports when it closes. */
32
+ export interface DesignerResult {
33
+ /** The player saved a new look. */
34
+ readonly saved: boolean;
35
+ }
36
+
37
+ /** What the social runtime adds for avatars (feature version 5). */
38
+ export interface RuntimeAvatarApi {
39
+ /** Open "Make your avatar" over the page; resolves when it closes. */
40
+ openDesigner(options?: OpenDesignerOptions): Promise<DesignerResult>;
41
+ /** Live `ev:avatar` changes (yours, friends', your room's). */
42
+ on(event: 'change', listener: (change: { readonly userId: number; readonly code: string }) => void): () => void;
43
+ }