@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.
- package/README.md +15 -2
- package/dist/characters/README.md +83 -0
- package/dist/characters/assets.d.ts +74 -0
- package/dist/characters/assets.d.ts.map +1 -0
- package/dist/characters/assets.js +234 -0
- package/dist/characters/assets.js.map +1 -0
- package/dist/characters/character-actor.d.ts +47 -0
- package/dist/characters/character-actor.d.ts.map +1 -0
- package/dist/characters/character-actor.js +119 -0
- package/dist/characters/character-actor.js.map +1 -0
- package/dist/characters/clips.d.ts +15 -0
- package/dist/characters/clips.d.ts.map +1 -0
- package/dist/characters/clips.js +24 -0
- package/dist/characters/clips.js.map +1 -0
- package/dist/characters/index.d.ts +30 -0
- package/dist/characters/index.d.ts.map +1 -0
- package/dist/characters/index.js +28 -0
- package/dist/characters/index.js.map +1 -0
- package/dist/characters/loader.d.ts +35 -0
- package/dist/characters/loader.d.ts.map +1 -0
- package/dist/characters/loader.js +84 -0
- package/dist/characters/loader.js.map +1 -0
- package/dist/characters/session-catalog.d.ts +10 -0
- package/dist/characters/session-catalog.d.ts.map +1 -0
- package/dist/characters/session-catalog.js +15 -0
- package/dist/characters/session-catalog.js.map +1 -0
- package/dist/client/PlatformClient.d.ts +9 -0
- package/dist/client/PlatformClient.d.ts.map +1 -1
- package/dist/client/PlatformClient.js +24 -0
- package/dist/client/PlatformClient.js.map +1 -1
- package/dist/host/PlatformHost.d.ts +18 -0
- package/dist/host/PlatformHost.d.ts.map +1 -1
- package/dist/host/PlatformHost.js +33 -0
- package/dist/host/PlatformHost.js.map +1 -1
- package/dist/protocol/characters.d.ts +82 -0
- package/dist/protocol/characters.d.ts.map +1 -0
- package/dist/protocol/characters.js +18 -0
- package/dist/protocol/characters.js.map +1 -0
- package/dist/protocol/guards.d.ts.map +1 -1
- package/dist/protocol/guards.js +3 -0
- package/dist/protocol/guards.js.map +1 -1
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/messages.d.ts +44 -2
- package/dist/protocol/messages.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +2 -2
- package/dist/protocol/version.d.ts.map +1 -1
- package/dist/protocol/version.js +5 -2
- package/dist/protocol/version.js.map +1 -1
- package/dist/three/README.md +141 -0
- package/dist/three/index.d.ts +2 -0
- package/dist/three/index.d.ts.map +1 -1
- package/dist/three/index.js +2 -0
- package/dist/three/index.js.map +1 -1
- package/dist/three/screen-anchor.d.ts +62 -0
- package/dist/three/screen-anchor.d.ts.map +1 -0
- package/dist/three/screen-anchor.js +71 -0
- package/dist/three/screen-anchor.js.map +1 -0
- package/dist/three/waypoint-beacon.d.ts +56 -0
- package/dist/three/waypoint-beacon.d.ts.map +1 -0
- package/dist/three/waypoint-beacon.js +118 -0
- package/dist/three/waypoint-beacon.js.map +1 -0
- package/dist/ui/README.md +104 -0
- package/dist/ui/action-card.d.ts.map +1 -1
- package/dist/ui/action-card.js +16 -0
- package/dist/ui/action-card.js.map +1 -1
- package/dist/ui/index.d.ts +3 -2
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +3 -2
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/rarity-card.d.ts +74 -0
- package/dist/ui/rarity-card.d.ts.map +1 -0
- package/dist/ui/rarity-card.js +105 -0
- package/dist/ui/rarity-card.js.map +1 -0
- package/dist/ui/showcase/index.d.ts.map +1 -1
- package/dist/ui/showcase/index.js +52 -1
- package/dist/ui/showcase/index.js.map +1 -1
- package/dist/ui/styles.js +31 -2
- package/dist/ui/styles.js.map +1 -1
- 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
|
-
####
|
|
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"}
|