@rydr/game-sdk 3.1.2 → 3.2.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.
Files changed (82) hide show
  1. package/README.md +15 -2
  2. package/dist/characters/README.md +83 -0
  3. package/dist/characters/assets.d.ts +74 -0
  4. package/dist/characters/assets.d.ts.map +1 -0
  5. package/dist/characters/assets.js +234 -0
  6. package/dist/characters/assets.js.map +1 -0
  7. package/dist/characters/character-actor.d.ts +47 -0
  8. package/dist/characters/character-actor.d.ts.map +1 -0
  9. package/dist/characters/character-actor.js +119 -0
  10. package/dist/characters/character-actor.js.map +1 -0
  11. package/dist/characters/clips.d.ts +15 -0
  12. package/dist/characters/clips.d.ts.map +1 -0
  13. package/dist/characters/clips.js +24 -0
  14. package/dist/characters/clips.js.map +1 -0
  15. package/dist/characters/index.d.ts +30 -0
  16. package/dist/characters/index.d.ts.map +1 -0
  17. package/dist/characters/index.js +28 -0
  18. package/dist/characters/index.js.map +1 -0
  19. package/dist/characters/loader.d.ts +35 -0
  20. package/dist/characters/loader.d.ts.map +1 -0
  21. package/dist/characters/loader.js +84 -0
  22. package/dist/characters/loader.js.map +1 -0
  23. package/dist/characters/session-catalog.d.ts +10 -0
  24. package/dist/characters/session-catalog.d.ts.map +1 -0
  25. package/dist/characters/session-catalog.js +15 -0
  26. package/dist/characters/session-catalog.js.map +1 -0
  27. package/dist/client/PlatformClient.d.ts +9 -0
  28. package/dist/client/PlatformClient.d.ts.map +1 -1
  29. package/dist/client/PlatformClient.js +24 -0
  30. package/dist/client/PlatformClient.js.map +1 -1
  31. package/dist/host/PlatformHost.d.ts +18 -0
  32. package/dist/host/PlatformHost.d.ts.map +1 -1
  33. package/dist/host/PlatformHost.js +33 -0
  34. package/dist/host/PlatformHost.js.map +1 -1
  35. package/dist/protocol/characters.d.ts +82 -0
  36. package/dist/protocol/characters.d.ts.map +1 -0
  37. package/dist/protocol/characters.js +18 -0
  38. package/dist/protocol/characters.js.map +1 -0
  39. package/dist/protocol/guards.d.ts.map +1 -1
  40. package/dist/protocol/guards.js +3 -0
  41. package/dist/protocol/guards.js.map +1 -1
  42. package/dist/protocol/index.d.ts +1 -0
  43. package/dist/protocol/index.d.ts.map +1 -1
  44. package/dist/protocol/index.js +1 -0
  45. package/dist/protocol/index.js.map +1 -1
  46. package/dist/protocol/messages.d.ts +44 -2
  47. package/dist/protocol/messages.d.ts.map +1 -1
  48. package/dist/protocol/version.d.ts +2 -2
  49. package/dist/protocol/version.d.ts.map +1 -1
  50. package/dist/protocol/version.js +5 -2
  51. package/dist/protocol/version.js.map +1 -1
  52. package/dist/three/README.md +141 -0
  53. package/dist/three/index.d.ts +2 -0
  54. package/dist/three/index.d.ts.map +1 -1
  55. package/dist/three/index.js +2 -0
  56. package/dist/three/index.js.map +1 -1
  57. package/dist/three/screen-anchor.d.ts +62 -0
  58. package/dist/three/screen-anchor.d.ts.map +1 -0
  59. package/dist/three/screen-anchor.js +71 -0
  60. package/dist/three/screen-anchor.js.map +1 -0
  61. package/dist/three/waypoint-beacon.d.ts +56 -0
  62. package/dist/three/waypoint-beacon.d.ts.map +1 -0
  63. package/dist/three/waypoint-beacon.js +118 -0
  64. package/dist/three/waypoint-beacon.js.map +1 -0
  65. package/dist/ui/README.md +104 -0
  66. package/dist/ui/action-card.d.ts.map +1 -1
  67. package/dist/ui/action-card.js +16 -0
  68. package/dist/ui/action-card.js.map +1 -1
  69. package/dist/ui/index.d.ts +3 -2
  70. package/dist/ui/index.d.ts.map +1 -1
  71. package/dist/ui/index.js +3 -2
  72. package/dist/ui/index.js.map +1 -1
  73. package/dist/ui/rarity-card.d.ts +74 -0
  74. package/dist/ui/rarity-card.d.ts.map +1 -0
  75. package/dist/ui/rarity-card.js +105 -0
  76. package/dist/ui/rarity-card.js.map +1 -0
  77. package/dist/ui/showcase/index.d.ts.map +1 -1
  78. package/dist/ui/showcase/index.js +52 -1
  79. package/dist/ui/showcase/index.js.map +1 -1
  80. package/dist/ui/styles.js +31 -2
  81. package/dist/ui/styles.js.map +1 -1
  82. package/package.json +8 -3
package/README.md CHANGED
@@ -313,7 +313,14 @@ await applyWorld(scene, world, { loadGlb: (url) => loader.loadAsync(url).then((g
313
313
 
314
314
  Authoring worlds happens in the platform's editor (admin-gated), not in your game; your game only **reads** them.
315
315
 
316
- #### Batch draw calls (optional `@rydr/game-sdk/three` helper)
316
+ #### `@rydr/game-sdk/three` helpers — **read [`src/three/README.md`](src/three/README.md) first (MANDATORY)**
317
+
318
+ > Before hand-rolling world loading, mesh merging, a frame profiler, or projecting a 3D point to a
319
+ > screen-space UI position, read **[`src/three/README.md`](src/three/README.md)** (shipped as
320
+ > `dist/three/README.md`). It lists every helper — `loadWorld`, `mergeByMaterial`, `PerfOverlay`,
321
+ > `ScreenAnchor`, the `perf*` marks — and how to use them. Reinventing one of these in a game is a bug.
322
+
323
+ #### Batch draw calls
317
324
 
318
325
  three.js submits one draw call per visible mesh, so a platform world (often ~1800 props) is usually the dominant render-CPU cost. The optional `@rydr/game-sdk/three` entry point (requires the `three` peer dep) loads a world *and* can collapse it to ≈one draw call per material — **you don't write any merge code**, you flip a flag:
319
326
 
@@ -363,7 +370,13 @@ room.setState({ phase: "racing" }); // merge into shared opaque state (last-writ
363
370
 
364
371
  > **Status: when `room` lands.** The client + protocol are shipped, but the backend `room` party is **not yet deployed** — `joinRoom` works in standalone-dev loopback today, and goes live against the shared backend with the realtime/multiplayer follow-up. Build against it; just don't expect cross-client presence in production until then.
365
372
 
366
- ## UI components (optional `@rydr/game-sdk/ui`)
373
+ ## UI components (optional `@rydr/game-sdk/ui`) — **read [`src/ui/README.md`](src/ui/README.md) first (MANDATORY)**
374
+
375
+ > Before building any in-game prompt, dialogue, choice menu, keycap, or card, read
376
+ > **[`src/ui/README.md`](src/ui/README.md)** (shipped as `dist/ui/README.md`). It lists every
377
+ > component, the state methods, and the **hard rule against restyling** (sizes are tuned for a
378
+ > trainer screen read from a few feet away). Hand-building one of these — or copy-pasting it from
379
+ > another game — fragments the look and is a bug.
367
380
 
368
381
  A self-contained DOM/CSS kit for the **controller-button UI** — showing "which button to press" in one consistent visual language across the library. Like the `three` helper it's a separate subpath export; unlike it, it needs **no peer dep** (no three.js). It injects its own scoped `.rydr-ui-*` CSS once into `<head>`, depends on no global styles or CSS variables, and renders pure DOM, so a game gets the look just by constructing a component.
369
382
 
@@ -0,0 +1,83 @@
1
+ # `@rydr/game-sdk/characters`
2
+
3
+ The three.js runtime for **platform-hosted rigged humanoids** — the "characters" counterpart to
4
+ `@rydr/game-sdk/three`'s worlds. A game asks for a character by catalog id and gets a ready,
5
+ disposable `CharacterActor`; the platform owns the content (meshes/atlases/clips on R2, manifest
6
+ rows in the catalog).
7
+
8
+ > Requires the optional `three` peer dependency (like `@rydr/game-sdk/three`).
9
+
10
+ ## Quick start
11
+
12
+ ```ts
13
+ import { createCharacter, preloadCharacters, type ClipName } from "@rydr/game-sdk/characters";
14
+
15
+ // A game PlatformSession satisfies the CharacterCatalog provider directly.
16
+ await preloadCharacters(["Viking_Leader"], { catalog: session }); // optional: warm the cache
17
+
18
+ const actor = await createCharacter("Fantasy_Male_Peasant_01", {
19
+ catalog: session,
20
+ overrides: { height: 1.7 }, // per-instance height / forwardYaw / ground
21
+ });
22
+ scene.add(actor.root); // you own placement — set actor.root.position / rotation
23
+
24
+ // each frame:
25
+ actor.play("Walking", { loop: true, fade: 0.2 }); // clip by NAME; the rig is invisible
26
+ actor.update(dt);
27
+ actor.setGroundY(terrainY); // re-plants the feet on your terrain height
28
+
29
+ actor.dispose(); // frees this instance; shared assets stay cached
30
+ ```
31
+
32
+ Discover characters for a picker with `session.listCharacters({ category })`. A game that uses a
33
+ known cast doesn't need to list — reference ids directly, optionally behind a `const` map for
34
+ semantic names + typing:
35
+
36
+ ```ts
37
+ const CHARS = { hero: "Fantasy_Male_Peasant_01", boss: "Viking_Leader" } as const;
38
+ await createCharacter(CHARS.hero, { catalog: session });
39
+ ```
40
+
41
+ ## Design
42
+
43
+ - **One data-driven load path.** A character is `{ glbUrl, bodyMesh?, attachments?, rigId }`; a
44
+ "combined pack" (many bodies on one skeleton in one GLB) is simply a character with `bodyMesh` set.
45
+ No per-pack branching — onboarding a new Synty pack is data, not code.
46
+ - **No runtime retargeting.** There is one canonical skeleton that owns the named-clip library. Packs
47
+ with a different skeleton are mapped to canonical and the clips are **baked** onto them **at ingest**
48
+ (in the character editor), so every rig's `clipUrls` are already bound to its own bones. The runtime
49
+ loads and plays; it carries no retarget solver, no bone maps (cf. Unity Humanoid / Unreal IK
50
+ Retargeter — normalization is an authoring concern).
51
+ - **Rig-agnostic clip names.** Clip NAMES are identical across rigs, so `actor.play("Walking")` works
52
+ regardless of which rig backs a character.
53
+ - **Fails soft.** An unknown id or a failed load yields a neutral placeholder capsule and a warning —
54
+ the runtime never throws into a live ride.
55
+ - **Caching.** Source GLBs/atlases are decoded once and cached by URL path (presigned R2 query strings
56
+ vary per request); a rig's baked clip set is loaded once and shared across instances. Per-instance
57
+ clones share cached geometry by reference — `actor.dispose()` frees only what the instance cloned
58
+ (its materials); call `clearCharacterCache()` for real teardown.
59
+ - **Narrow coupling.** The runtime depends only on the 3-method `CharacterCatalog` provider, not the
60
+ full `PlatformSession` — so it also runs in the character editor (Supabase-backed) and in tests.
61
+
62
+ ## API
63
+
64
+ | Export | What |
65
+ |---|---|
66
+ | `createCharacter(id, { catalog, height?, forwardYaw?, ground? })` | Resolve + load + mount → `CharacterActor`. The 90% path. |
67
+ | `loadCharacter(char, rig, overrides?)` | Session-free: build from already-resolved rows. |
68
+ | `preloadCharacters(ids, { catalog })` | Warm caches before a scene (best-effort). |
69
+ | `CharacterActor` | `root`, `play(name, opts?)`, `pick(names)`, `has(name)`, `update(dt)`, `setGroundY(y)`, `clipNames`, `dispose()`. |
70
+ | `sessionCatalog(session)` | Adapt a session to `CharacterCatalog` (a session already conforms). |
71
+ | `clearCharacterCache()` | Free all cached character GPU resources. |
72
+ | `CANONICAL_CLIPS`, `ClipName` | The canonical clip vocabulary + its type. |
73
+ | low-level `loadGltf`/`cloneModel`/`mountCharacter`/… | For callers driving the mixer/mounting themselves. |
74
+
75
+ ## Follow-ups (deliberately deferred)
76
+
77
+ 1. **Clip vocabulary in code vs data.** `ClipName` / `CANONICAL_CLIPS` ship in code as a typed
78
+ contract (like Unity Humanoid's fixed muscle set), so adding a canonical animation implies an SDK
79
+ release — a tension with "content evolves without an SDK release." Acceptable while the canonical
80
+ set is stable; if it starts churning, move the vocabulary to catalog data (with codegen for typing).
81
+ 2. **Crowd scale beyond instance sharing.** v1 shares geometry / atlas / clip sets across instances.
82
+ Animation-LOD (distant/offscreen actors ticking their mixer less often) and hardware instancing are
83
+ deferred until a game renders large crowds (30+ actors).
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Low-level three.js asset helpers for the character runtime — GLB/atlas/clip loading + caching,
3
+ * skinned-mesh-safe cloning, mounting, and per-instance disposal.
4
+ *
5
+ * These are the proven internals distilled from the fps model-viewer / sandbox humanoid layer, with
6
+ * ONE deliberate omission: there is **no retarget solver**. Clips arrive already baked onto each
7
+ * rig's skeleton (done in the character editor at ingest), so the runtime only ever loads and plays.
8
+ *
9
+ * Caching mirrors `@rydr/game-sdk/three`'s world loader: source GLBs are decoded once and cached by
10
+ * URL *path* (presigned R2 query strings vary per request); the cache owns those GPU resources for
11
+ * the process lifetime. Per-instance clones share the cached geometry by reference, so an actor's
12
+ * `dispose()` frees only what it cloned (its materials) — never the shared geometry. Call
13
+ * {@link clearCharacterCache} for real teardown.
14
+ */
15
+ import * as THREE from "three";
16
+ import type { GLTF } from "three/addons/loaders/GLTFLoader.js";
17
+ /** Fetch + decode a GLB once (cached by path). The result is shared — never mutate/dispose it. */
18
+ export declare function loadGltf(url: string): Promise<GLTF>;
19
+ /** Clone a cached GLB scene for a new instance (skinned-mesh safe: a plain `.clone()` shares the
20
+ * skeleton, so two characters would animate as one). Geometry stays shared by reference. */
21
+ export declare function cloneModel(url: string): Promise<THREE.Object3D>;
22
+ /** Load + cache a pack atlas. Keyed by url + flipY (Synty GLB-UV atlases want `false`; the default),
23
+ * nearest-filtered, no mipmaps. */
24
+ export declare function getAtlas(url: string, flipY?: boolean): THREE.Texture;
25
+ /**
26
+ * Paint every mesh under `root` with the pack atlas. Materials are CLONED per instance (newer packs
27
+ * share one material across meshes and mutating it in place doesn't reliably rebind on the rendered
28
+ * instance) — so these clones are the actor's own and are disposed in {@link disposeObject}.
29
+ * `killEmissive` clears the white emissiveFactor newer packs ship (it washes the body to flat white).
30
+ */
31
+ export declare function paintAtlas(root: THREE.Object3D, atlasUrl: string, opts?: {
32
+ killEmissive?: boolean;
33
+ flipY?: boolean;
34
+ }): void;
35
+ /**
36
+ * Load a rig's baked clip GLB(s) into a name→clip map: names trimmed, deduplicated (first source
37
+ * wins), `.position` tracks stripped (the caller owns world placement; grounding re-plants the feet).
38
+ * Cached per rig by its URL set — N instances of a rig share one clip set (clips are immutable data
39
+ * an `AnimationMixer` can safely share). Clips are already bound to this rig's bones (baked), so they
40
+ * bind by name with no retargeting.
41
+ */
42
+ export declare function loadClips(urls: string[]): Promise<Record<string, THREE.AnimationClip>>;
43
+ /**
44
+ * Remove `root` from its parent and dispose the materials this instance cloned (see
45
+ * {@link paintAtlas}). Geometry is NOT disposed — it's shared with the cached source GLB and other
46
+ * live instances; the cache owns it. Textures are cached and shared too. Use {@link clearCharacterCache}
47
+ * for real teardown.
48
+ */
49
+ export declare function disposeObject(root: THREE.Object3D): void;
50
+ /** Free ALL cached character GPU resources (source geometries, atlases) and empty the caches. Real
51
+ * teardown — call when no character instances remain (e.g. leaving a 3D prototype). */
52
+ export declare function clearCharacterCache(): void;
53
+ /** Every SkinnedMesh under `obj`, in traversal order. */
54
+ export declare function skinnedMeshes(obj: THREE.Object3D): THREE.SkinnedMesh[];
55
+ /**
56
+ * Combined-pack isolation: a combined GLB holds many bodies + hair/hood attachments on ONE shared
57
+ * skeleton. Keep only `bodyName` (+ any `attachNames`) visible; hide the rest. Returns the chosen
58
+ * body mesh (for {@link mountSkinned}).
59
+ */
60
+ export declare function isolateBody(model: THREE.Object3D, bodyName: string, attachNames?: string[]): THREE.SkinnedMesh | null;
61
+ /**
62
+ * Mount a character for placement: scale it to `height` m, ground its feet at y=0, recenter its
63
+ * footprint on the origin. Returns a placement group — set its `position`/`rotation`. Bone world
64
+ * positions drive the bbox (a Box3 on a SkinnedMesh is unreliable).
65
+ */
66
+ export declare function mountCharacter(model: THREE.Object3D, height?: number, forwardYaw?: number): THREE.Group;
67
+ /**
68
+ * Mount a combined-pack character by its VISIBLE body mesh's bind-pose bounds (newer combined packs
69
+ * author the mesh at a different scale than the skeleton, so bone-based scaling leaves them giant).
70
+ */
71
+ export declare function mountSkinned(model: THREE.Object3D, bodyMesh: THREE.Object3D, height?: number, forwardYaw?: number): THREE.Group;
72
+ /** Mount a NON-rigged static prop: same contract but the bbox is the mesh geometry (no bones). */
73
+ export declare function mountProp(model: THREE.Object3D, height?: number, forwardYaw?: number): THREE.Group;
74
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/characters/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AA0B/D,kGAAkG;AAClG,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASnD;AAED;6FAC6F;AAC7F,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAGrE;AAKD;oCACoC;AACpC,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG,KAAK,CAAC,OAAO,CAalE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CAoB/H;AAMD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAuBtF;AAGD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,CASxD;AAED;wFACwF;AACxF,wBAAgB,mBAAmB,IAAI,IAAI,CAW1C;AAKD,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,WAAW,EAAE,CAItE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,GAAE,MAAM,EAAO,GAAG,KAAK,CAAC,WAAW,GAAG,IAAI,CAOzH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,SAAM,EAAE,UAAU,SAAI,GAAG,KAAK,CAAC,KAAK,CAQ/F;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,SAAM,EAAE,UAAU,SAAI,GAAG,KAAK,CAAC,KAAK,CAGvH;AAED,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,SAAM,EAAE,UAAU,SAAI,GAAG,KAAK,CAAC,KAAK,CAG1F"}
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Low-level three.js asset helpers for the character runtime — GLB/atlas/clip loading + caching,
3
+ * skinned-mesh-safe cloning, mounting, and per-instance disposal.
4
+ *
5
+ * These are the proven internals distilled from the fps model-viewer / sandbox humanoid layer, with
6
+ * ONE deliberate omission: there is **no retarget solver**. Clips arrive already baked onto each
7
+ * rig's skeleton (done in the character editor at ingest), so the runtime only ever loads and plays.
8
+ *
9
+ * Caching mirrors `@rydr/game-sdk/three`'s world loader: source GLBs are decoded once and cached by
10
+ * URL *path* (presigned R2 query strings vary per request); the cache owns those GPU resources for
11
+ * the process lifetime. Per-instance clones share the cached geometry by reference, so an actor's
12
+ * `dispose()` frees only what it cloned (its materials) — never the shared geometry. Call
13
+ * {@link clearCharacterCache} for real teardown.
14
+ */
15
+ import * as THREE from "three";
16
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
17
+ import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
18
+ import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js";
19
+ /** DRACO decoder — pinned to the version the platform tooling uses (matches the world loader). */
20
+ const DRACO_DECODER_PATH = "https://www.gstatic.com/draco/versioned/decoders/1.5.6/";
21
+ let _loader = null;
22
+ function loader() {
23
+ if (!_loader) {
24
+ _loader = new GLTFLoader();
25
+ const draco = new DRACOLoader();
26
+ draco.setDecoderPath(DRACO_DECODER_PATH);
27
+ _loader.setDRACOLoader(draco);
28
+ }
29
+ return _loader;
30
+ }
31
+ /** Dedup by asset path — presigned R2 URLs carry per-request query strings for the same object. */
32
+ function pathKey(url) {
33
+ const cut = url.search(/[?#]/);
34
+ return cut === -1 ? url : url.slice(0, cut);
35
+ }
36
+ const gltfCache = new Map();
37
+ /** Fetch + decode a GLB once (cached by path). The result is shared — never mutate/dispose it. */
38
+ export function loadGltf(url) {
39
+ const key = pathKey(url);
40
+ let p = gltfCache.get(key);
41
+ if (!p) {
42
+ p = loader().loadAsync(encodeURI(url));
43
+ p.catch(() => gltfCache.delete(key)); // don't cache a failed load
44
+ gltfCache.set(key, p);
45
+ }
46
+ return p;
47
+ }
48
+ /** Clone a cached GLB scene for a new instance (skinned-mesh safe: a plain `.clone()` shares the
49
+ * skeleton, so two characters would animate as one). Geometry stays shared by reference. */
50
+ export async function cloneModel(url) {
51
+ const gltf = await loadGltf(url);
52
+ return SkeletonUtils.clone(gltf.scene);
53
+ }
54
+ // --- Colour atlas (Synty models ship untextured) ---------------------------
55
+ const atlasCache = new Map();
56
+ /** Load + cache a pack atlas. Keyed by url + flipY (Synty GLB-UV atlases want `false`; the default),
57
+ * nearest-filtered, no mipmaps. */
58
+ export function getAtlas(url, flipY = false) {
59
+ const key = `${pathKey(url)}#${flipY ? 1 : 0}`;
60
+ let t = atlasCache.get(key);
61
+ if (!t) {
62
+ t = new THREE.TextureLoader().load(encodeURI(url));
63
+ t.colorSpace = THREE.SRGBColorSpace;
64
+ t.flipY = flipY;
65
+ t.magFilter = THREE.NearestFilter;
66
+ t.minFilter = THREE.NearestFilter;
67
+ t.generateMipmaps = false;
68
+ atlasCache.set(key, t);
69
+ }
70
+ return t;
71
+ }
72
+ /**
73
+ * Paint every mesh under `root` with the pack atlas. Materials are CLONED per instance (newer packs
74
+ * share one material across meshes and mutating it in place doesn't reliably rebind on the rendered
75
+ * instance) — so these clones are the actor's own and are disposed in {@link disposeObject}.
76
+ * `killEmissive` clears the white emissiveFactor newer packs ship (it washes the body to flat white).
77
+ */
78
+ export function paintAtlas(root, atlasUrl, opts = {}) {
79
+ const tex = getAtlas(atlasUrl, opts.flipY ?? false);
80
+ root.traverse((o) => {
81
+ const mesh = o;
82
+ if (!mesh.isMesh || !mesh.material)
83
+ return;
84
+ const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
85
+ const next = mats.map((m) => {
86
+ const sm = m.clone();
87
+ sm.map = tex;
88
+ sm.color.setRGB(1, 1, 1);
89
+ if (opts.killEmissive) {
90
+ sm.emissive.setRGB(0, 0, 0);
91
+ sm.emissiveMap = null;
92
+ }
93
+ sm.vertexColors = false;
94
+ sm.needsUpdate = true;
95
+ return sm;
96
+ });
97
+ if (next.length)
98
+ mesh.material = Array.isArray(mesh.material) ? next : next[0];
99
+ });
100
+ }
101
+ // --- Animation clips (BAKED upstream — the runtime only loads + dedups) -----
102
+ const JUNK_CLIP = /take 001|baselayer/i;
103
+ const clipsCache = new Map();
104
+ /**
105
+ * Load a rig's baked clip GLB(s) into a name→clip map: names trimmed, deduplicated (first source
106
+ * wins), `.position` tracks stripped (the caller owns world placement; grounding re-plants the feet).
107
+ * Cached per rig by its URL set — N instances of a rig share one clip set (clips are immutable data
108
+ * an `AnimationMixer` can safely share). Clips are already bound to this rig's bones (baked), so they
109
+ * bind by name with no retargeting.
110
+ */
111
+ export function loadClips(urls) {
112
+ const key = urls.map(pathKey).join("|");
113
+ let p = clipsCache.get(key);
114
+ if (!p) {
115
+ p = Promise.all(urls.map(loadGltf)).then((gltfs) => {
116
+ const out = {};
117
+ for (const gltf of gltfs) {
118
+ for (const clip of gltf.animations) {
119
+ if (JUNK_CLIP.test(clip.name))
120
+ continue;
121
+ const name = clip.name.trim();
122
+ if (out[name])
123
+ continue;
124
+ const c = clip.clone();
125
+ c.name = name;
126
+ c.tracks = c.tracks.filter((t) => !/\.position$/.test(t.name));
127
+ out[name] = c;
128
+ }
129
+ }
130
+ return out;
131
+ });
132
+ p.catch(() => clipsCache.delete(key));
133
+ clipsCache.set(key, p);
134
+ }
135
+ return p;
136
+ }
137
+ // --- Disposal ---------------------------------------------------------------
138
+ /**
139
+ * Remove `root` from its parent and dispose the materials this instance cloned (see
140
+ * {@link paintAtlas}). Geometry is NOT disposed — it's shared with the cached source GLB and other
141
+ * live instances; the cache owns it. Textures are cached and shared too. Use {@link clearCharacterCache}
142
+ * for real teardown.
143
+ */
144
+ export function disposeObject(root) {
145
+ root.parent?.remove(root);
146
+ root.traverse((o) => {
147
+ const mesh = o;
148
+ if (!mesh.isMesh)
149
+ return;
150
+ for (const m of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
151
+ m?.dispose();
152
+ }
153
+ });
154
+ }
155
+ /** Free ALL cached character GPU resources (source geometries, atlases) and empty the caches. Real
156
+ * teardown — call when no character instances remain (e.g. leaving a 3D prototype). */
157
+ export function clearCharacterCache() {
158
+ for (const p of gltfCache.values()) {
159
+ void p.then((g) => g.scene.traverse((o) => {
160
+ const mesh = o;
161
+ if (mesh.isMesh)
162
+ mesh.geometry?.dispose();
163
+ })).catch(() => { });
164
+ }
165
+ for (const t of atlasCache.values())
166
+ t.dispose();
167
+ gltfCache.clear();
168
+ atlasCache.clear();
169
+ clipsCache.clear();
170
+ }
171
+ // --- Model helpers ----------------------------------------------------------
172
+ const _up = new THREE.Vector3(0, 1, 0);
173
+ /** Every SkinnedMesh under `obj`, in traversal order. */
174
+ export function skinnedMeshes(obj) {
175
+ const out = [];
176
+ obj.traverse((o) => { if (o.isSkinnedMesh)
177
+ out.push(o); });
178
+ return out;
179
+ }
180
+ /**
181
+ * Combined-pack isolation: a combined GLB holds many bodies + hair/hood attachments on ONE shared
182
+ * skeleton. Keep only `bodyName` (+ any `attachNames`) visible; hide the rest. Returns the chosen
183
+ * body mesh (for {@link mountSkinned}).
184
+ */
185
+ export function isolateBody(model, bodyName, attachNames = []) {
186
+ let body = null;
187
+ for (const m of skinnedMeshes(model)) {
188
+ m.visible = m.name === bodyName || attachNames.includes(m.name);
189
+ if (m.name === bodyName)
190
+ body = m;
191
+ }
192
+ return body;
193
+ }
194
+ /**
195
+ * Mount a character for placement: scale it to `height` m, ground its feet at y=0, recenter its
196
+ * footprint on the origin. Returns a placement group — set its `position`/`rotation`. Bone world
197
+ * positions drive the bbox (a Box3 on a SkinnedMesh is unreliable).
198
+ */
199
+ export function mountCharacter(model, height = 1.8, forwardYaw = 0) {
200
+ model.updateMatrixWorld(true);
201
+ const bbox = new THREE.Box3();
202
+ const pt = new THREE.Vector3();
203
+ model.traverse((o) => {
204
+ if (o.isBone)
205
+ bbox.expandByPoint(o.getWorldPosition(pt));
206
+ });
207
+ return finishMount(model, bbox, height, forwardYaw);
208
+ }
209
+ /**
210
+ * Mount a combined-pack character by its VISIBLE body mesh's bind-pose bounds (newer combined packs
211
+ * author the mesh at a different scale than the skeleton, so bone-based scaling leaves them giant).
212
+ */
213
+ export function mountSkinned(model, bodyMesh, height = 1.8, forwardYaw = 0) {
214
+ model.updateMatrixWorld(true);
215
+ return finishMount(model, new THREE.Box3().setFromObject(bodyMesh), height, forwardYaw);
216
+ }
217
+ /** Mount a NON-rigged static prop: same contract but the bbox is the mesh geometry (no bones). */
218
+ export function mountProp(model, height = 1.8, forwardYaw = 0) {
219
+ model.updateMatrixWorld(true);
220
+ return finishMount(model, new THREE.Box3().setFromObject(model), height, forwardYaw);
221
+ }
222
+ function finishMount(model, bbox, height, forwardYaw) {
223
+ const size = bbox.getSize(new THREE.Vector3());
224
+ const s = size.y > 0 ? height / size.y : 1;
225
+ model.scale.multiplyScalar(s);
226
+ model.rotation.y = forwardYaw;
227
+ const c = bbox.getCenter(new THREE.Vector3());
228
+ const off = new THREE.Vector3(c.x, 0, c.z).multiplyScalar(s).applyAxisAngle(_up, forwardYaw);
229
+ model.position.set(-off.x, -bbox.min.y * s, -off.z); // recenter X/Z + feet at y=0
230
+ const root = new THREE.Group();
231
+ root.add(model);
232
+ return root;
233
+ }
234
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../src/characters/assets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import * as THREE from "three";
2
+ import type { ClipName } from "./clips.js";
3
+ /**
4
+ * CharacterActor — one live humanoid: a placement group on its own {@link THREE.AnimationMixer}. The
5
+ * caller owns placement (parent `actor.root`, set its position/rotation), animates with {@link play},
6
+ * and advances it with {@link update} each frame.
7
+ *
8
+ * Rig-agnostic: clips were baked onto this character's skeleton at ingest, so they bind by NAME and
9
+ * the actor never knows or cares which rig it uses. Missing clips fail soft (`play` no-ops) — the
10
+ * runtime must never throw into a live ride.
11
+ *
12
+ * Build one via `createCharacter(id, { catalog })`; this class is the value it resolves to.
13
+ */
14
+ export declare class CharacterActor {
15
+ /** Placement group — parent this and set its position/rotation. */
16
+ readonly root: THREE.Object3D;
17
+ /** True for a rigless static prop (no clips/mixer). */
18
+ readonly isStatic: boolean;
19
+ readonly mixer: THREE.AnimationMixer | null;
20
+ readonly actions: Record<string, THREE.AnimationAction>;
21
+ private current;
22
+ private readonly groundBones;
23
+ private readonly doGround;
24
+ private groundY;
25
+ private readonly _gpt;
26
+ constructor(root: THREE.Object3D, clips: Record<string, THREE.AnimationClip> | null, ground: boolean);
27
+ get clipNames(): string[];
28
+ has(name: ClipName): boolean;
29
+ /** First clip name in `names` this character actually has (graceful fallback). */
30
+ pick(names: ClipName[]): string | undefined;
31
+ /** Name of the clip currently playing (or null). */
32
+ get currentClip(): string | null;
33
+ /** Crossfade to a clip. No-op if it's already the looping current clip; returns null if absent. */
34
+ play(name: ClipName, opts?: {
35
+ loop?: boolean;
36
+ fade?: number;
37
+ }): THREE.AnimationAction | null;
38
+ /** The floor height (root-local) the feet re-seat on each frame when grounding is enabled. */
39
+ setGroundY(y: number): void;
40
+ update(dt: number): void;
41
+ /** Re-plant the lowest foot bone on `groundY` (a clip + mount can drift the hips). */
42
+ private ground;
43
+ /** Free this instance: stop the mixer, remove from parent, dispose the materials it cloned. Shared
44
+ * geometry/textures are owned by the cache (see `clearCharacterCache`), not freed here. */
45
+ dispose(): void;
46
+ }
47
+ //# sourceMappingURL=character-actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character-actor.d.ts","sourceRoot":"","sources":["../../src/characters/character-actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACzB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAM;IAE7D,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;gBAEhC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO;IAyBpG,IAAI,SAAS,IAAI,MAAM,EAAE,CAExB;IAED,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO;IAI5B,kFAAkF;IAClF,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,SAAS;IAI3C,oDAAoD;IACpD,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,mGAAmG;IACnG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,KAAK,CAAC,eAAe,GAAG,IAAI;IAiBhG,8FAA8F;IAC9F,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAI3B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKxB,sFAAsF;IACtF,OAAO,CAAC,MAAM;IAUd;gGAC4F;IAC5F,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,119 @@
1
+ import * as THREE from "three";
2
+ import { disposeObject } from "./assets.js";
3
+ /**
4
+ * CharacterActor — one live humanoid: a placement group on its own {@link THREE.AnimationMixer}. The
5
+ * caller owns placement (parent `actor.root`, set its position/rotation), animates with {@link play},
6
+ * and advances it with {@link update} each frame.
7
+ *
8
+ * Rig-agnostic: clips were baked onto this character's skeleton at ingest, so they bind by NAME and
9
+ * the actor never knows or cares which rig it uses. Missing clips fail soft (`play` no-ops) — the
10
+ * runtime must never throw into a live ride.
11
+ *
12
+ * Build one via `createCharacter(id, { catalog })`; this class is the value it resolves to.
13
+ */
14
+ export class CharacterActor {
15
+ /** Placement group — parent this and set its position/rotation. */
16
+ root;
17
+ /** True for a rigless static prop (no clips/mixer). */
18
+ isStatic;
19
+ mixer;
20
+ actions = {};
21
+ current = null;
22
+ groundBones = [];
23
+ doGround;
24
+ groundY = 0;
25
+ _gpt = new THREE.Vector3();
26
+ constructor(root, clips, ground) {
27
+ this.root = root;
28
+ this.isStatic = !clips;
29
+ this.doGround = ground && !!clips;
30
+ this.mixer = clips ? new THREE.AnimationMixer(root) : null;
31
+ if (clips && this.mixer) {
32
+ for (const [name, clip] of Object.entries(clips))
33
+ this.actions[name] = this.mixer.clipAction(clip);
34
+ // Ground-lock on the FEET (foot/ankle/ball/toe, both rig namings) so the body stays planted;
35
+ // grounding off every bone would jump the lowest point to a swinging hand. Fall back to all
36
+ // deform bones (skip IK helpers + armature root).
37
+ const all = [];
38
+ root.traverse((o) => {
39
+ if (!o.isBone)
40
+ return;
41
+ if (/^ik_|^SK_|^Root$/i.test(o.name))
42
+ return;
43
+ all.push(o);
44
+ if (/(foot|ankle|ball|toe)/i.test(o.name))
45
+ this.groundBones.push(o);
46
+ });
47
+ if (!this.groundBones.length)
48
+ this.groundBones.push(...all);
49
+ // Pose on the spawn frame so the rig isn't a bare T-pose until the first clip plays.
50
+ const idle = this.pick(["Idle", "PistolIdle"]) ?? this.clipNames[0];
51
+ if (idle)
52
+ this.play(idle, { loop: true, fade: 0 });
53
+ this.mixer.update(0);
54
+ }
55
+ }
56
+ get clipNames() {
57
+ return Object.keys(this.actions);
58
+ }
59
+ has(name) {
60
+ return !!this.actions[name];
61
+ }
62
+ /** First clip name in `names` this character actually has (graceful fallback). */
63
+ pick(names) {
64
+ return names.find((n) => this.actions[n]);
65
+ }
66
+ /** Name of the clip currently playing (or null). */
67
+ get currentClip() {
68
+ return this.current ? this.current.getClip().name : null;
69
+ }
70
+ /** Crossfade to a clip. No-op if it's already the looping current clip; returns null if absent. */
71
+ play(name, opts = {}) {
72
+ const loop = opts.loop ?? true;
73
+ const fade = opts.fade ?? 0.2;
74
+ const next = this.actions[name];
75
+ if (!next)
76
+ return null;
77
+ if (next === this.current && loop && next.timeScale > 0)
78
+ return next;
79
+ next.reset();
80
+ next.timeScale = 1;
81
+ next.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, Infinity);
82
+ next.clampWhenFinished = !loop;
83
+ next.play();
84
+ if (this.current && this.current !== next)
85
+ this.current.crossFadeTo(next, fade, false);
86
+ else
87
+ next.fadeIn(fade);
88
+ this.current = next;
89
+ return next;
90
+ }
91
+ /** The floor height (root-local) the feet re-seat on each frame when grounding is enabled. */
92
+ setGroundY(y) {
93
+ this.groundY = y;
94
+ }
95
+ update(dt) {
96
+ this.mixer?.update(dt);
97
+ if (this.doGround && this.groundBones.length)
98
+ this.ground();
99
+ }
100
+ /** Re-plant the lowest foot bone on `groundY` (a clip + mount can drift the hips). */
101
+ ground() {
102
+ this.root.updateMatrixWorld(true);
103
+ let minY = Infinity;
104
+ for (const b of this.groundBones) {
105
+ b.getWorldPosition(this._gpt);
106
+ if (this._gpt.y < minY)
107
+ minY = this._gpt.y;
108
+ }
109
+ if (Number.isFinite(minY))
110
+ this.root.position.y += this.groundY - minY;
111
+ }
112
+ /** Free this instance: stop the mixer, remove from parent, dispose the materials it cloned. Shared
113
+ * geometry/textures are owned by the cache (see `clearCharacterCache`), not freed here. */
114
+ dispose() {
115
+ this.mixer?.stopAllAction();
116
+ disposeObject(this.root);
117
+ }
118
+ }
119
+ //# sourceMappingURL=character-actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character-actor.js","sourceRoot":"","sources":["../../src/characters/character-actor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The canonical clip vocabulary — the animation names authored on the canonical skeleton and baked
3
+ * onto every rig, so a game plays a clip by NAME without caring which rig backs a character.
4
+ *
5
+ * NOTE (see the package README): the vocabulary is intentionally shipped in code as a typed contract
6
+ * (like Unity Humanoid's fixed muscle set), NOT as platform data. `ClipName` accepts any string too
7
+ * (`string & {}`) so autocomplete surfaces the canonical names while `actor.clipNames`/`pick()` — which
8
+ * return whatever a rig's baked GLB actually contains — still typecheck.
9
+ */
10
+ /** The commonly-authored canonical clips. Extend deliberately; adding one implies an SDK release. */
11
+ export declare const CANONICAL_CLIPS: readonly ["Idle", "Walking", "Running", "SlowRun", "Talking", "TalkingAlt", "PistolIdle", "PistolWalk", "PistolRun", "PistolAim", "Death"];
12
+ export type CanonicalClip = (typeof CANONICAL_CLIPS)[number];
13
+ /** A clip name: a canonical one (with autocomplete) or any string a rig's baked GLB provides. */
14
+ export type ClipName = CanonicalClip | (string & {});
15
+ //# sourceMappingURL=clips.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clips.d.ts","sourceRoot":"","sources":["../../src/characters/clips.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,qGAAqG;AACrG,eAAO,MAAM,eAAe,4IAYlB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,iGAAiG;AACjG,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC"}