@jgengine/core 0.26.0 → 0.28.0

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 (143) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/ai/policeResponse.d.ts +253 -0
  3. package/dist/ai/policeResponse.js +421 -0
  4. package/dist/anim/actionLayer.d.ts +47 -0
  5. package/dist/anim/actionLayer.js +63 -0
  6. package/dist/anim/ikSolver.d.ts +14 -0
  7. package/dist/anim/ikSolver.js +37 -0
  8. package/dist/anim/locomotionGraph.d.ts +58 -3
  9. package/dist/anim/locomotionGraph.js +142 -15
  10. package/dist/anim/ragdollRig.d.ts +53 -0
  11. package/dist/anim/ragdollRig.js +132 -0
  12. package/dist/anim/skeletonStandard.d.ts +140 -0
  13. package/dist/anim/skeletonStandard.js +326 -0
  14. package/dist/combat/damageRuntime.d.ts +14 -1
  15. package/dist/combat/damageRuntime.js +62 -4
  16. package/dist/combat/death.js +7 -3
  17. package/dist/combat/deathReason.d.ts +7 -1
  18. package/dist/combat/effects.d.ts +25 -0
  19. package/dist/combat/effects.js +16 -0
  20. package/dist/combat/statusRuntime.d.ts +2 -0
  21. package/dist/combat/statusRuntime.js +1 -0
  22. package/dist/economy/loanBook.d.ts +182 -0
  23. package/dist/economy/loanBook.js +303 -0
  24. package/dist/economy/market.d.ts +161 -0
  25. package/dist/economy/market.js +248 -0
  26. package/dist/economy/transactionLedger.d.ts +162 -0
  27. package/dist/economy/transactionLedger.js +301 -0
  28. package/dist/editor/creatorStorage.js +3 -1
  29. package/dist/editor/simulation.d.ts +5 -2
  30. package/dist/editor/simulation.js +29 -19
  31. package/dist/game/cameraConfig.d.ts +14 -0
  32. package/dist/game/clipRoles.d.ts +5 -4
  33. package/dist/game/clipRoles.js +34 -4
  34. package/dist/game/events.d.ts +27 -0
  35. package/dist/game/gameSave.d.ts +142 -0
  36. package/dist/game/gameSave.js +174 -0
  37. package/dist/game/playableGame.d.ts +74 -6
  38. package/dist/game/saveSlotIndex.d.ts +54 -0
  39. package/dist/game/saveSlotIndex.js +71 -0
  40. package/dist/game/saveSlots.d.ts +2 -0
  41. package/dist/game/saveSlots.js +1 -1
  42. package/dist/game/saveStore.d.ts +8 -0
  43. package/dist/game/saveStore.js +16 -0
  44. package/dist/gameplay.d.ts +17 -2
  45. package/dist/gameplay.js +15 -0
  46. package/dist/inventory/carryLoad.d.ts +48 -0
  47. package/dist/inventory/carryLoad.js +44 -0
  48. package/dist/inventory/equipment.d.ts +95 -0
  49. package/dist/inventory/equipment.js +169 -0
  50. package/dist/inventory/inventoryModel.d.ts +15 -6
  51. package/dist/inventory/inventoryModel.js +54 -5
  52. package/dist/inventory/shapedGrid.d.ts +26 -0
  53. package/dist/inventory/shapedGrid.js +47 -0
  54. package/dist/loot/lootGen.d.ts +186 -0
  55. package/dist/loot/lootGen.js +437 -0
  56. package/dist/loot/lootWeapon.d.ts +35 -0
  57. package/dist/loot/lootWeapon.js +70 -0
  58. package/dist/meta/changelog.d.ts +1 -1
  59. package/dist/meta/changelog.js +156 -1
  60. package/dist/mission/compositions.d.ts +85 -0
  61. package/dist/mission/compositions.js +130 -0
  62. package/dist/mission/mission.d.ts +435 -0
  63. package/dist/mission/mission.js +770 -0
  64. package/dist/mission/triggers.d.ts +61 -0
  65. package/dist/mission/triggers.js +108 -0
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/traffic.d.ts +222 -0
  69. package/dist/nav/traffic.js +918 -0
  70. package/dist/physics/kinematicVehicle.d.ts +36 -0
  71. package/dist/physics/kinematicVehicle.js +15 -1
  72. package/dist/physics/vehicleDamage.d.ts +136 -0
  73. package/dist/physics/vehicleDamage.js +214 -0
  74. package/dist/physics/vehicleParts.d.ts +62 -0
  75. package/dist/physics/vehicleParts.js +65 -0
  76. package/dist/progression/gearScore.d.ts +56 -0
  77. package/dist/progression/gearScore.js +59 -0
  78. package/dist/progression/skillTree.d.ts +129 -0
  79. package/dist/progression/skillTree.js +265 -0
  80. package/dist/progression/xpCurve.d.ts +60 -0
  81. package/dist/progression/xpCurve.js +141 -0
  82. package/dist/runtime/context/combat.js +11 -0
  83. package/dist/scene/builtinSceneKinds.js +2 -0
  84. package/dist/settings/accessibilitySettings.d.ts +81 -0
  85. package/dist/settings/accessibilitySettings.js +152 -0
  86. package/dist/settings/graphicsProfile.d.ts +3 -0
  87. package/dist/settings/graphicsProfile.js +4 -0
  88. package/dist/settings/settingsModel.d.ts +1 -1
  89. package/dist/settings/settingsModel.js +1 -0
  90. package/dist/sim/nearAgents.d.ts +8 -1
  91. package/dist/sim/nearAgents.js +34 -17
  92. package/dist/sim/offScreen.d.ts +2 -1
  93. package/dist/sim/offScreen.js +14 -3
  94. package/dist/sim/population.d.ts +147 -0
  95. package/dist/sim/population.js +182 -0
  96. package/dist/sim/queue.d.ts +90 -0
  97. package/dist/sim/queue.js +171 -0
  98. package/dist/sim/rides.d.ts +287 -0
  99. package/dist/sim/rides.js +471 -0
  100. package/dist/sim/routine.js +19 -11
  101. package/dist/ui/loadingTips.d.ts +41 -0
  102. package/dist/ui/loadingTips.js +64 -0
  103. package/dist/ui/menuNav.d.ts +79 -0
  104. package/dist/ui/menuNav.js +147 -0
  105. package/dist/ui/menuTransition.d.ts +50 -0
  106. package/dist/ui/menuTransition.js +67 -0
  107. package/dist/ui.d.ts +2 -0
  108. package/dist/ui.js +2 -0
  109. package/dist/vfx/effectLibrary.d.ts +98 -0
  110. package/dist/vfx/effectLibrary.js +181 -0
  111. package/dist/vfx/particleBudget.d.ts +40 -0
  112. package/dist/vfx/particleBudget.js +39 -0
  113. package/dist/vfx/particleDirector.d.ts +16 -0
  114. package/dist/vfx/particleDirector.js +18 -0
  115. package/dist/world/authoredSimulation.js +7 -1
  116. package/dist/world/captureCamera.d.ts +30 -0
  117. package/dist/world/captureCamera.js +19 -0
  118. package/dist/world/decalKind.d.ts +48 -0
  119. package/dist/world/decalKind.js +86 -0
  120. package/dist/world/features.d.ts +18 -5
  121. package/dist/world/features.js +1 -0
  122. package/dist/world/grassKind.d.ts +5 -0
  123. package/dist/world/grassKind.js +3 -0
  124. package/dist/world/terraform.d.ts +8 -0
  125. package/dist/world/terrain.d.ts +1 -0
  126. package/dist/world/terrain.js +2 -0
  127. package/dist/world.d.ts +7 -2
  128. package/dist/world.js +5 -0
  129. package/package.json +1 -1
  130. package/skills/jgengine/api.md +2 -2
  131. package/skills/jgengine-combat/SKILL.md +9 -1
  132. package/skills/jgengine-combat/api.md +5 -4
  133. package/skills/jgengine-combat/reference.md +4 -0
  134. package/skills/jgengine-gameplay/SKILL.md +86 -0
  135. package/skills/jgengine-gameplay/api.md +485 -16
  136. package/skills/jgengine-gameplay/capabilities.md +131 -4
  137. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  138. package/skills/jgengine-world/SKILL.md +8 -0
  139. package/skills/jgengine-world/api.md +243 -11
  140. package/skills/jgengine-world/capabilities.md +61 -1
  141. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  142. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
  143. package/skills/jgengine-world/reference.md +1 -1
@@ -1,16 +1,45 @@
1
1
  import type { ModelAnimationConfig } from "../game/playableGame.js";
2
2
  import type { AnimGraph } from "./animGraph.js";
3
- /** Inputs for {@link locomotionGraph}: the idle/walk/run clip names and the one-shot table a rig config already carries. */
3
+ /** Inputs for {@link locomotionGraph}: the locomotion clip names and the one-shot table a rig config already carries. */
4
4
  export interface LocomotionGraphInput {
5
5
  idle: string;
6
6
  walk: string;
7
7
  run?: string;
8
+ /** Clip for moving to the character's left; adds the `strafe` state. */
9
+ strafeLeft?: string;
10
+ /** Clip for moving to the character's right; adds the `strafe` state. */
11
+ strafeRight?: string;
12
+ /** Clip for moving backwards; adds the `backward` state. */
13
+ backward?: string;
14
+ /** Clip for turning on the spot. Omitted, the turn blend uses `walk` as its pivot step. */
15
+ turn?: string;
8
16
  /** Speed (world units/sec) above which the character walks. Default 0.5. */
9
17
  walkSpeed?: number;
10
18
  /** Speed above which it runs. Default 6. */
11
19
  runSpeed?: number;
12
20
  /** Crossfade seconds between locomotion states. Default 0.2. */
13
21
  fadeSec?: number;
22
+ /** Lateral share above which the character counts as strafing. Default 0.35. */
23
+ strafeGate?: number;
24
+ /** Lateral share at or below which it stops strafing. Default 0.2. */
25
+ strafeRelease?: number;
26
+ /** Facing-vs-motion dot below which it counts as moving backwards. Default -0.35. */
27
+ backwardGate?: number;
28
+ /** Facing-vs-motion dot at or above which it stops. Default -0.2. */
29
+ backwardRelease?: number;
30
+ /** Turn rate (radians/sec) above which the character counts as turning on the spot. Default 1.5. */
31
+ turnGate?: number;
32
+ /** Turn rate at which the turn blend reaches its pivot clip. Default 4. */
33
+ turnFull?: number;
34
+ /** Speed below which a turn counts as turning on the spot. Default 0.5. */
35
+ turnSpeed?: number;
36
+ /**
37
+ * Mark every locomotion state as a root-motion state, so the rig plays in place and the blended
38
+ * root travel comes out as `AnimGraphOutput.rootDelta`. The movement owner consumes it with
39
+ * `takeRootMotion` and resolves it through its controller. Off by default, for rigs whose clips
40
+ * are authored in place.
41
+ */
42
+ rootMotion?: boolean;
14
43
  /** One-shot clips keyed by trigger name; `death` holds its last frame instead of returning. */
15
44
  oneShots?: Readonly<Record<string, string | readonly string[]>>;
16
45
  }
@@ -22,11 +51,37 @@ export declare const LOCOMOTION_GROUNDED_PARAM = "grounded";
22
51
  export declare const LOCOMOTION_VERTICAL_SPEED_PARAM = "verticalSpeed";
23
52
  /** Physically resolved crouch stance from the shared player motor. */
24
53
  export declare const LOCOMOTION_CROUCHED_PARAM = "crouched";
54
+ /**
55
+ * Signed lateral share of the character's motion: `-1` fully to its left, `+1` fully to its right,
56
+ * `0` straight ahead or straight back. A custom mover feeds it under `ANIM_PARAMS_KEY`.
57
+ */
58
+ export declare const LOCOMOTION_DIRECTION_PARAM = "direction";
59
+ /**
60
+ * Dot of the character's facing with its motion direction: `1` running forward, `-1` running
61
+ * backwards. A custom mover feeds it under `ANIM_PARAMS_KEY`.
62
+ */
63
+ export declare const LOCOMOTION_FACING_PARAM = "facing";
64
+ /** Signed yaw rate in radians/sec; a custom mover feeds it under `ANIM_PARAMS_KEY`. */
65
+ export declare const LOCOMOTION_TURN_RATE_PARAM = "turnRate";
25
66
  /** Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`. */
26
67
  export declare const LOCOMOTION_LAYER = "base";
68
+ /** State id of the forward idle/walk/run blend. */
69
+ export declare const LOCOMOTION_STATE = "locomotion";
70
+ /** State id of the lateral strafe blend. */
71
+ export declare const LOCOMOTION_STRAFE_STATE = "strafe";
72
+ /** State id of the backwards blend. */
73
+ export declare const LOCOMOTION_BACKWARD_STATE = "backward";
74
+ /** State id of the on-the-spot turn blend. */
75
+ export declare const LOCOMOTION_TURN_STATE = "turn";
27
76
  /**
28
- * The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk, and run, plus a
29
- * state per one-shot that plays once and returns (or clamps for `death`). What `useModelAnimation` used to hardcode.
77
+ * The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk and
78
+ * run, plus a state per one-shot that plays once and returns (or clamps for `death`). What
79
+ * `useModelAnimation` used to hardcode.
80
+ *
81
+ * Supplying `strafeLeft`/`strafeRight` adds a `strafe` blend on the `direction` parameter, and
82
+ * `backward` and `turn` add their states behind the `facing` and `turnRate` parameters. Each extra
83
+ * state is joined to the base by a crossfaded transition, so a direction change blends rather than
84
+ * pops; without them the graph is exactly the speed-only tree it has always been.
30
85
  *
31
86
  * @capability locomotion-graph build the default idle/walk/run plus one-shot graph from clip names
32
87
  */
@@ -6,11 +6,44 @@ export const LOCOMOTION_GROUNDED_PARAM = "grounded";
6
6
  export const LOCOMOTION_VERTICAL_SPEED_PARAM = "verticalSpeed";
7
7
  /** Physically resolved crouch stance from the shared player motor. */
8
8
  export const LOCOMOTION_CROUCHED_PARAM = "crouched";
9
+ /**
10
+ * Signed lateral share of the character's motion: `-1` fully to its left, `+1` fully to its right,
11
+ * `0` straight ahead or straight back. A custom mover feeds it under `ANIM_PARAMS_KEY`.
12
+ */
13
+ export const LOCOMOTION_DIRECTION_PARAM = "direction";
14
+ /**
15
+ * Dot of the character's facing with its motion direction: `1` running forward, `-1` running
16
+ * backwards. A custom mover feeds it under `ANIM_PARAMS_KEY`.
17
+ */
18
+ export const LOCOMOTION_FACING_PARAM = "facing";
19
+ /** Signed yaw rate in radians/sec; a custom mover feeds it under `ANIM_PARAMS_KEY`. */
20
+ export const LOCOMOTION_TURN_RATE_PARAM = "turnRate";
9
21
  /** Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`. */
10
22
  export const LOCOMOTION_LAYER = "base";
23
+ /** State id of the forward idle/walk/run blend. */
24
+ export const LOCOMOTION_STATE = "locomotion";
25
+ /** State id of the lateral strafe blend. */
26
+ export const LOCOMOTION_STRAFE_STATE = "strafe";
27
+ /** State id of the backwards blend. */
28
+ export const LOCOMOTION_BACKWARD_STATE = "backward";
29
+ /** State id of the on-the-spot turn blend. */
30
+ export const LOCOMOTION_TURN_STATE = "turn";
31
+ const DEFAULT_STRAFE_GATE = 0.35;
32
+ const DEFAULT_STRAFE_RELEASE = 0.2;
33
+ const DEFAULT_BACKWARD_GATE = -0.35;
34
+ const DEFAULT_BACKWARD_RELEASE = -0.2;
35
+ const DEFAULT_TURN_GATE = 1.5;
36
+ const DEFAULT_TURN_FULL = 4;
37
+ const DEFAULT_TURN_SPEED = 0.5;
11
38
  /**
12
- * The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk, and run, plus a
13
- * state per one-shot that plays once and returns (or clamps for `death`). What `useModelAnimation` used to hardcode.
39
+ * The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk and
40
+ * run, plus a state per one-shot that plays once and returns (or clamps for `death`). What
41
+ * `useModelAnimation` used to hardcode.
42
+ *
43
+ * Supplying `strafeLeft`/`strafeRight` adds a `strafe` blend on the `direction` parameter, and
44
+ * `backward` and `turn` add their states behind the `facing` and `turnRate` parameters. Each extra
45
+ * state is joined to the base by a crossfaded transition, so a direction change blends rather than
46
+ * pops; without them the graph is exactly the speed-only tree it has always been.
14
47
  *
15
48
  * @capability locomotion-graph build the default idle/walk/run plus one-shot graph from clip names
16
49
  */
@@ -18,31 +51,119 @@ export function locomotionGraph(input) {
18
51
  const walkSpeed = input.walkSpeed ?? 0.5;
19
52
  const runSpeed = input.runSpeed ?? 6;
20
53
  const fade = input.fadeSec ?? 0.2;
21
- const points = [
22
- { at: 0, clip: input.idle },
23
- { at: walkSpeed, clip: input.walk },
24
- ...(input.run === undefined ? [] : [{ at: runSpeed, clip: input.run }]),
25
- ];
54
+ const rootMotion = input.rootMotion === true ? { rootMotion: true } : {};
26
55
  const states = {
27
- locomotion: { kind: "blend1D", param: LOCOMOTION_SPEED_PARAM, points },
56
+ [LOCOMOTION_STATE]: {
57
+ kind: "blend1D",
58
+ param: LOCOMOTION_SPEED_PARAM,
59
+ points: [
60
+ { at: 0, clip: input.idle },
61
+ { at: walkSpeed, clip: input.walk },
62
+ ...(input.run === undefined ? [] : [{ at: runSpeed, clip: input.run }]),
63
+ ],
64
+ ...rootMotion,
65
+ },
66
+ };
67
+ const directional = [];
68
+ const strafes = [input.strafeLeft, input.strafeRight];
69
+ if (strafes.some((clip) => clip !== undefined)) {
70
+ const gate = input.strafeGate ?? DEFAULT_STRAFE_GATE;
71
+ const release = input.strafeRelease ?? DEFAULT_STRAFE_RELEASE;
72
+ const points = [
73
+ ...(input.strafeLeft === undefined ? [] : [{ at: -1, clip: input.strafeLeft }]),
74
+ ...(input.strafeRight === undefined ? [] : [{ at: 1, clip: input.strafeRight }]),
75
+ ];
76
+ states[LOCOMOTION_STRAFE_STATE] = { kind: "blend1D", param: LOCOMOTION_DIRECTION_PARAM, points, ...rootMotion };
77
+ for (const side of [-1, 1]) {
78
+ directional.push({
79
+ from: LOCOMOTION_STATE,
80
+ to: LOCOMOTION_STRAFE_STATE,
81
+ when: [{ param: LOCOMOTION_DIRECTION_PARAM, op: side < 0 ? "<" : ">", value: side * gate }],
82
+ duration: fade,
83
+ });
84
+ }
85
+ directional.push({
86
+ from: LOCOMOTION_STRAFE_STATE,
87
+ to: LOCOMOTION_STATE,
88
+ when: [
89
+ { param: LOCOMOTION_DIRECTION_PARAM, op: ">=", value: -release },
90
+ { param: LOCOMOTION_DIRECTION_PARAM, op: "<=", value: release },
91
+ ],
92
+ duration: fade,
93
+ });
94
+ }
95
+ if (input.backward !== undefined) {
96
+ const gate = input.backwardGate ?? DEFAULT_BACKWARD_GATE;
97
+ const release = input.backwardRelease ?? DEFAULT_BACKWARD_RELEASE;
98
+ states[LOCOMOTION_BACKWARD_STATE] = { kind: "blend1D", param: LOCOMOTION_SPEED_PARAM, points: [{ at: walkSpeed, clip: input.backward }], ...rootMotion };
99
+ for (const from of [LOCOMOTION_STATE, LOCOMOTION_STRAFE_STATE]) {
100
+ if (states[from] === undefined)
101
+ continue;
102
+ directional.push({
103
+ from,
104
+ to: LOCOMOTION_BACKWARD_STATE,
105
+ when: [
106
+ { param: LOCOMOTION_FACING_PARAM, op: "<", value: gate },
107
+ { param: LOCOMOTION_SPEED_PARAM, op: ">", value: walkSpeed * 0.5 },
108
+ ],
109
+ duration: fade,
110
+ });
111
+ }
112
+ directional.push({
113
+ from: LOCOMOTION_BACKWARD_STATE,
114
+ to: LOCOMOTION_STATE,
115
+ when: [{ param: LOCOMOTION_FACING_PARAM, op: ">=", value: release }],
116
+ duration: fade,
117
+ });
118
+ }
119
+ const turnGate = input.turnGate ?? DEFAULT_TURN_GATE;
120
+ const turnFull = input.turnFull ?? DEFAULT_TURN_FULL;
121
+ const turnSpeed = input.turnSpeed ?? DEFAULT_TURN_SPEED;
122
+ states[LOCOMOTION_TURN_STATE] = {
123
+ kind: "blend1D",
124
+ param: LOCOMOTION_TURN_RATE_PARAM,
125
+ points: [
126
+ { at: turnGate, clip: input.idle },
127
+ { at: turnFull, clip: input.turn ?? input.walk },
128
+ ],
129
+ ...rootMotion,
28
130
  };
131
+ for (const from of [LOCOMOTION_STATE, LOCOMOTION_STRAFE_STATE, LOCOMOTION_BACKWARD_STATE]) {
132
+ if (states[from] === undefined)
133
+ continue;
134
+ directional.push({
135
+ from,
136
+ to: LOCOMOTION_TURN_STATE,
137
+ when: [
138
+ { param: LOCOMOTION_TURN_RATE_PARAM, op: ">", value: turnGate },
139
+ { param: LOCOMOTION_SPEED_PARAM, op: "<", value: turnSpeed },
140
+ ],
141
+ duration: fade,
142
+ });
143
+ }
144
+ directional.push({
145
+ from: LOCOMOTION_TURN_STATE,
146
+ to: LOCOMOTION_STATE,
147
+ when: [{ param: LOCOMOTION_TURN_RATE_PARAM, op: "<=", value: turnGate }],
148
+ duration: fade,
149
+ });
29
150
  const oneShots = Object.entries(input.oneShots ?? {}).filter(([, spec]) => typeof spec === "string" || spec.length > 0);
151
+ oneShots.sort(([a], [b]) => Number(b === "death") - Number(a === "death"));
30
152
  for (const [name, spec] of oneShots) {
31
153
  const clip = typeof spec === "string" ? spec : spec[0];
32
154
  states[name] = { kind: "clip", clip, loop: false, ...(typeof spec === "string" || spec.length < 2 ? {} : { variants: [...spec] }) };
33
155
  }
34
- const transitions = [];
35
- oneShots.sort(([a], [b]) => Number(b === "death") - Number(a === "death"));
156
+ const triggered = [];
36
157
  for (const [name] of oneShots) {
37
158
  for (const from of Object.keys(states)) {
38
159
  if (from !== "death")
39
- transitions.push({ from, to: name, trigger: name, duration: 0.1 });
160
+ triggered.push({ from, to: name, trigger: name, duration: 0.1 });
40
161
  }
41
162
  if (name !== "death")
42
- transitions.push({ from: name, to: "locomotion", exitTime: 1, duration: fade });
163
+ triggered.push({ from: name, to: LOCOMOTION_STATE, exitTime: 1, duration: fade });
43
164
  }
44
165
  return {
45
- layers: [{ id: LOCOMOTION_LAYER, entry: "locomotion", states, transitions }],
166
+ layers: [{ id: LOCOMOTION_LAYER, entry: LOCOMOTION_STATE, states, transitions: [...triggered, ...directional] }],
46
167
  };
47
168
  }
48
169
  /**
@@ -60,10 +181,16 @@ export function animGraphFromConfig(config) {
60
181
  const states = config.states;
61
182
  if (states === undefined || typeof states.idle !== "string" || states.idle.trim().length === 0)
62
183
  return undefined;
184
+ const clip = (value) => typeof value === "string" && value.trim().length > 0 ? value : undefined;
63
185
  return locomotionGraph({
64
186
  idle: states.idle,
65
- walk: typeof states.walk === "string" && states.walk.trim().length > 0 ? states.walk : states.idle,
66
- ...(typeof states.run !== "string" || states.run.trim().length === 0 ? {} : { run: states.run }),
187
+ walk: clip(states.walk) ?? states.idle,
188
+ ...(clip(states.run) === undefined ? {} : { run: clip(states.run) }),
189
+ ...(clip(states.strafeLeft) === undefined ? {} : { strafeLeft: clip(states.strafeLeft) }),
190
+ ...(clip(states.strafeRight) === undefined ? {} : { strafeRight: clip(states.strafeRight) }),
191
+ ...(clip(states.backward) === undefined ? {} : { backward: clip(states.backward) }),
192
+ ...(clip(states.turn) === undefined ? {} : { turn: clip(states.turn) }),
193
+ ...(states.rootMotion === true ? { rootMotion: true } : {}),
67
194
  walkSpeed: states.walkSpeed,
68
195
  runSpeed: states.runSpeed,
69
196
  fadeSec: states.fadeSec,
@@ -0,0 +1,53 @@
1
+ import type { RagdollConfig } from "../physics/ragdoll.js";
2
+ import { type SkeletonMap, type SkeletonSlot } from "./skeletonStandard.js";
3
+ /**
4
+ * One standard slot's world pose, sampled at the moment a character is killed. The orientation is
5
+ * the reference the ragdoll's aim rotates from, so every bone keeps the roll it died with.
6
+ */
7
+ export interface RagdollPose {
8
+ slot: SkeletonSlot;
9
+ position: readonly [number, number, number];
10
+ /** World orientation of the bone, as `[x, y, z, w]`. */
11
+ orientation: readonly [number, number, number, number];
12
+ }
13
+ /** Half-extents and mass of one standard slot's physics body, in the rig's own world units. */
14
+ export interface RagdollBodyShape {
15
+ halfExtents: readonly [number, number, number];
16
+ mass: number;
17
+ }
18
+ /**
19
+ * Humanoid body shapes for the standard slots, sized for a roughly 1.8 m character standing on the
20
+ * ground. A rig of another scale multiplies them (see {@link RagdollRigOptions.scale}).
21
+ */
22
+ export declare const HUMANOID_RAGDOLL_BODIES: Readonly<Partial<Record<SkeletonSlot, RagdollBodyShape>>>;
23
+ /** Tuning for {@link ragdollConfigFromPoses}. */
24
+ export interface RagdollRigOptions {
25
+ /** Overrides {@link HUMANOID_RAGDOLL_BODIES} per slot. A slot absent from both is skipped. */
26
+ bodies?: Readonly<Partial<Record<SkeletonSlot, RagdollBodyShape>>>;
27
+ /** Multiplies every half-extent; a rig taller than 1.8 m passes its height over 1.8. Default 1. */
28
+ scale?: number;
29
+ /** Also build a body for slots with no shape of their own. Default false. */
30
+ includeUnshaped?: boolean;
31
+ }
32
+ /**
33
+ * World orientations for a ragdoll's bones from its bodies' current positions: each bone rotates
34
+ * from its rest direction-to-child onto the current one, so the aim follows the joints while every
35
+ * bone keeps the roll it had in the death pose. A bone with no posed child keeps its rest
36
+ * orientation. The physics bodies carry translation only, so this is what gives the ragdoll a
37
+ * skeleton instead of a pile of boxes.
38
+ *
39
+ * @capability animation orient a ragdoll's bones from its physics bodies' positions
40
+ */
41
+ export declare function ragdollBoneOrientations(rest: readonly RagdollPose[], positions: Readonly<Partial<Record<SkeletonSlot, readonly [number, number, number]>>>): Partial<Record<SkeletonSlot, [number, number, number, number]>>;
42
+ /**
43
+ * A jointed ragdoll for a mapped rig from its bones' world poses at the moment of death: one box
44
+ * body per standard slot that has a shape, sized to the character, and one distance link per slot
45
+ * that has its parent present, holding the bones' own separation. The bodies start where the
46
+ * character actually was, so the collapse continues the death pose instead of snapping to a preset.
47
+ *
48
+ * `handSlot.l`/`handSlot.r` carry no body: they are prop sockets, not body parts. A slot whose bone
49
+ * the rig does not have is skipped, and its children link to the nearest present ancestor instead.
50
+ *
51
+ * @capability animation build a physics ragdoll from a mapped rig's death pose
52
+ */
53
+ export declare function ragdollConfigFromPoses(map: SkeletonMap, poses: readonly RagdollPose[], options?: RagdollRigOptions): RagdollConfig;
@@ -0,0 +1,132 @@
1
+ import { multiplyQuaternions, quaternionFromUnitVectors } from "./ikSolver.js";
2
+ import { SKELETON_PARENT, SKELETON_SLOTS } from "./skeletonStandard.js";
3
+ /**
4
+ * Humanoid body shapes for the standard slots, sized for a roughly 1.8 m character standing on the
5
+ * ground. A rig of another scale multiplies them (see {@link RagdollRigOptions.scale}).
6
+ */
7
+ export const HUMANOID_RAGDOLL_BODIES = {
8
+ root: { halfExtents: [0.1, 0.1, 0.08], mass: 4 },
9
+ hips: { halfExtents: [0.1, 0.09, 0.08], mass: 8 },
10
+ spine: { halfExtents: [0.1, 0.1, 0.08], mass: 6 },
11
+ chest: { halfExtents: [0.13, 0.12, 0.09], mass: 10 },
12
+ head: { halfExtents: [0.09, 0.1, 0.09], mass: 5 },
13
+ "upperArm.l": { halfExtents: [0.05, 0.13, 0.05], mass: 2 },
14
+ "upperArm.r": { halfExtents: [0.05, 0.13, 0.05], mass: 2 },
15
+ "lowerArm.l": { halfExtents: [0.04, 0.12, 0.04], mass: 1.5 },
16
+ "lowerArm.r": { halfExtents: [0.04, 0.12, 0.04], mass: 1.5 },
17
+ "hand.l": { halfExtents: [0.04, 0.05, 0.03], mass: 0.5 },
18
+ "hand.r": { halfExtents: [0.04, 0.05, 0.03], mass: 0.5 },
19
+ "upperLeg.l": { halfExtents: [0.07, 0.18, 0.07], mass: 4 },
20
+ "upperLeg.r": { halfExtents: [0.07, 0.18, 0.07], mass: 4 },
21
+ "lowerLeg.l": { halfExtents: [0.06, 0.17, 0.06], mass: 3 },
22
+ "lowerLeg.r": { halfExtents: [0.06, 0.17, 0.06], mass: 3 },
23
+ "foot.l": { halfExtents: [0.06, 0.04, 0.1], mass: 1 },
24
+ "foot.r": { halfExtents: [0.06, 0.04, 0.1], mass: 1 },
25
+ "toes.l": { halfExtents: [0.06, 0.03, 0.06], mass: 0.3 },
26
+ "toes.r": { halfExtents: [0.06, 0.03, 0.06], mass: 0.3 },
27
+ };
28
+ /**
29
+ * World orientations for a ragdoll's bones from its bodies' current positions: each bone rotates
30
+ * from its rest direction-to-child onto the current one, so the aim follows the joints while every
31
+ * bone keeps the roll it had in the death pose. A bone with no posed child keeps its rest
32
+ * orientation. The physics bodies carry translation only, so this is what gives the ragdoll a
33
+ * skeleton instead of a pile of boxes.
34
+ *
35
+ * @capability animation orient a ragdoll's bones from its physics bodies' positions
36
+ */
37
+ export function ragdollBoneOrientations(rest, positions) {
38
+ const restBySlot = new Map(rest.map((pose) => [pose.slot, pose]));
39
+ const present = new Set(restBySlot.keys());
40
+ // The nearest posed descendant, walking down through slots the rig or shape table skipped.
41
+ const childOf = new Map();
42
+ for (const slot of present) {
43
+ for (const candidate of SKELETON_SLOTS) {
44
+ let parent = SKELETON_PARENT[candidate];
45
+ while (parent !== undefined && !present.has(parent))
46
+ parent = SKELETON_PARENT[parent];
47
+ if (parent === slot && !childOf.has(slot)) {
48
+ childOf.set(slot, candidate);
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ const out = {};
54
+ for (const slot of present) {
55
+ const pose = restBySlot.get(slot);
56
+ const current = positions[slot];
57
+ const child = childOf.get(slot);
58
+ const childPose = child === undefined ? undefined : restBySlot.get(child);
59
+ const childCurrent = child === undefined ? undefined : positions[child];
60
+ if (current === undefined || childPose === undefined || childCurrent === undefined) {
61
+ out[slot] = [pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]];
62
+ continue;
63
+ }
64
+ const restDirection = [
65
+ childPose.position[0] - pose.position[0],
66
+ childPose.position[1] - pose.position[1],
67
+ childPose.position[2] - pose.position[2],
68
+ ];
69
+ const currentDirection = [
70
+ childCurrent[0] - current[0],
71
+ childCurrent[1] - current[1],
72
+ childCurrent[2] - current[2],
73
+ ];
74
+ if (Math.hypot(...restDirection) <= 1e-6 || Math.hypot(...currentDirection) <= 1e-6) {
75
+ out[slot] = [pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]];
76
+ continue;
77
+ }
78
+ const aim = quaternionFromUnitVectors(restDirection, currentDirection);
79
+ const base = [pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]];
80
+ const composed = multiplyQuaternions(aim, base);
81
+ out[slot] = [composed[0], composed[1], composed[2], composed[3]];
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * A jointed ragdoll for a mapped rig from its bones' world poses at the moment of death: one box
87
+ * body per standard slot that has a shape, sized to the character, and one distance link per slot
88
+ * that has its parent present, holding the bones' own separation. The bodies start where the
89
+ * character actually was, so the collapse continues the death pose instead of snapping to a preset.
90
+ *
91
+ * `handSlot.l`/`handSlot.r` carry no body: they are prop sockets, not body parts. A slot whose bone
92
+ * the rig does not have is skipped, and its children link to the nearest present ancestor instead.
93
+ *
94
+ * @capability animation build a physics ragdoll from a mapped rig's death pose
95
+ */
96
+ export function ragdollConfigFromPoses(map, poses, options = {}) {
97
+ const scale = options.scale ?? 1;
98
+ const poseBySlot = new Map(poses.map((pose) => [pose.slot, pose.position]));
99
+ const present = [];
100
+ const bones = [];
101
+ for (const slot of SKELETON_SLOTS) {
102
+ if (map.slots[slot] === undefined)
103
+ continue;
104
+ const position = poseBySlot.get(slot);
105
+ if (position === undefined)
106
+ continue;
107
+ const shape = options.bodies?.[slot] ?? HUMANOID_RAGDOLL_BODIES[slot];
108
+ if (shape === undefined && options.includeUnshaped !== true)
109
+ continue;
110
+ const halfExtents = shape?.halfExtents ?? [0.1, 0.1, 0.1];
111
+ bones.push({
112
+ name: slot,
113
+ position: [position[0], position[1], position[2]],
114
+ halfExtents: [halfExtents[0] * scale, halfExtents[1] * scale, halfExtents[2] * scale],
115
+ mass: shape?.mass ?? 1,
116
+ });
117
+ present.push(slot);
118
+ }
119
+ const included = new Set(present);
120
+ const links = [];
121
+ for (const slot of present) {
122
+ let parent = SKELETON_PARENT[slot];
123
+ // Walk up past slots this rig or this shape table does not cover, so a legless rig still links
124
+ // its head to the chest rather than dropping the bone out of the joint chain.
125
+ while (parent !== undefined && !included.has(parent))
126
+ parent = SKELETON_PARENT[parent];
127
+ if (parent === undefined)
128
+ continue;
129
+ links.push({ a: parent, b: slot, kind: "distance" });
130
+ }
131
+ return { bones, links };
132
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The engine's one humanoid skeleton standard, and the path any other CC0 rig takes onto it.
3
+ *
4
+ * The standard is the **KayKit character rig**: one armature shared by every rigged character in
5
+ * `kaykit-adventurers` (Barbarian, Knight, Mage, Rogue, Rogue_Hooded) and `kaykit-skeletons`
6
+ * (Skeleton_Mage/Minion/Rogue/Warrior). Because they share it, all nine characters play the same
7
+ * clip set and one authored graph, foot-IK rig, attachment slot or ragdoll layout serves all of
8
+ * them. There is no second standard to keep in sync.
9
+ *
10
+ * A rig on a different scheme (Quaternius aliens, Mixamo exports) is not rewritten: its bones are
11
+ * *mapped* slot by slot onto the standard's names, and what it does not have is reported rather
12
+ * than guessed. A rig missing every leg slot can still be driven on its spine and head; a rig
13
+ * missing a slot never silently borrows a neighbour's bone.
14
+ */
15
+ /**
16
+ * One canonical bone on the standard. Names are the authored names in the KayKit armature; the
17
+ * loaded runtime name drops `.` (see {@link runtimeBoneName}), so `upperleg.l` is `upperlegl` in
18
+ * the scene graph and both spellings resolve through `resolveRigNode`.
19
+ */
20
+ export type SkeletonSlot = "root" | "hips" | "spine" | "chest" | "head" | "upperArm.l" | "upperArm.r" | "lowerArm.l" | "lowerArm.r" | "hand.l" | "hand.r" | "handSlot.l" | "handSlot.r" | "upperLeg.l" | "upperLeg.r" | "lowerLeg.l" | "lowerLeg.r" | "foot.l" | "foot.r" | "toes.l" | "toes.r";
21
+ /** Every standard slot, in parent-before-child order. */
22
+ export declare const SKELETON_SLOTS: readonly SkeletonSlot[];
23
+ /** The parent slot of each slot that has one; `root` has none. */
24
+ export declare const SKELETON_PARENT: Readonly<Partial<Record<SkeletonSlot, SkeletonSlot>>>;
25
+ /** A named skeleton standard: its id and the authored bone name for each slot it defines. */
26
+ export interface SkeletonStandard {
27
+ id: string;
28
+ label: string;
29
+ slots: Readonly<Partial<Record<SkeletonSlot, string>>>;
30
+ }
31
+ /**
32
+ * The one humanoid skeleton standard. Bone names are the authored KayKit names; the loaded
33
+ * hierarchy spells them without `.` and `resolveRigNode` accepts either.
34
+ *
35
+ * @capability skeleton-standard the shared humanoid bone standard every rigged character maps onto
36
+ */
37
+ export declare const SKELETON_STANDARD: SkeletonStandard;
38
+ /**
39
+ * A source naming scheme and the bone names on it that fill each standard slot. A slot lists every
40
+ * accepted spelling; matching is case- and punctuation-insensitive, so `mixamorigLeftUpLeg` and
41
+ * `LeftUpLeg` are the same candidate.
42
+ */
43
+ export interface RigScheme {
44
+ id: string;
45
+ label: string;
46
+ /** Where the scheme comes from, for credits and diagnostics. */
47
+ source: string;
48
+ slots: Readonly<Partial<Record<SkeletonSlot, readonly string[]>>>;
49
+ }
50
+ /**
51
+ * The standard itself, as a scheme: a KayKit rig already is the standard, so its own names are the
52
+ * candidates. Both authored (`upperleg.l`) and runtime (`upperlegl`) spellings are accepted.
53
+ *
54
+ * @capability skeleton-standard map a rig on the standard scheme onto the standard's slots
55
+ */
56
+ export declare const KAYKIT_SCHEME: RigScheme;
57
+ /**
58
+ * Mixamo exports (`mixamorig*` names, or the same names without the prefix). Mixamo has a neck and
59
+ * a shoulder per side, which the standard folds into `chest` and `upperArm`; its toe is
60
+ * `ToeBase`, which is the standard's `toes`. Mixamo's topmost bone is `Hips`, so `root` and `hips`
61
+ * both resolve to it — a rig on this scheme has no bone above the pelvis.
62
+ */
63
+ export declare const MIXAMO_SCHEME: RigScheme;
64
+ /**
65
+ * Quaternius sci-fi aliens: a `Root` plus a `Spine_1..Spine_6` chain, a `Neck` and a `Head`. The
66
+ * pack ships no arms and no legs, so those slots stay empty and the alien can only be driven on
67
+ * its spine and head. That is the honest mapping: the pack's own clip set is `Alien_Idle` alone.
68
+ */
69
+ export declare const QUATERNIUS_ALIEN_SCHEME: RigScheme;
70
+ /** Every scheme the engine ships a mapping for, keyed by {@link RigScheme.id}. */
71
+ export declare const RIG_SCHEMES: Readonly<Record<string, RigScheme>>;
72
+ /**
73
+ * The runtime name GLTFLoader gives an authored bone name: three's `PropertyBinding.sanitizeNodeName`
74
+ * strips `[ ] . : /`, so the authored `upperleg.l` loads as `upperlegl`.
75
+ *
76
+ * @capability skeleton-standard convert an authored bone name to its loaded runtime name
77
+ */
78
+ export declare function runtimeBoneName(authored: string): string;
79
+ /** The result of mapping a rig's bones onto {@link SKELETON_STANDARD}. */
80
+ export interface SkeletonMap {
81
+ /** Scheme the map was built from. */
82
+ scheme: string;
83
+ /** Standard slot → the bone name on this rig that fills it. */
84
+ slots: Partial<Record<SkeletonSlot, string>>;
85
+ /** Slots the standard defines that this rig has no bone for. */
86
+ missing: SkeletonSlot[];
87
+ /** Bones on the rig that fill no standard slot (helpers, props, IK targets). */
88
+ unmapped: string[];
89
+ }
90
+ /**
91
+ * Maps a rig's bone names onto {@link SKELETON_STANDARD} through one {@link RigScheme}. Each slot
92
+ * takes the first candidate that resolves; a candidate list is tried in order, so a scheme can name
93
+ * a preferred bone and fall back. Bones that fill no slot come back in `unmapped`, and slots the rig
94
+ * cannot fill in `missing` — the caller decides whether a partial map is usable.
95
+ *
96
+ * @capability skeleton-standard map any CC0 rig's bones onto the shared humanoid slots
97
+ */
98
+ export declare function mapRigToStandard(bones: readonly string[], scheme?: RigScheme | string): SkeletonMap;
99
+ /** A standard slot and the rest-pose length of the bone chain that reaches it. */
100
+ export interface SlotLength {
101
+ slot: SkeletonSlot;
102
+ /** Distance from the slot's bone to its parent bone, in the rig's own units. */
103
+ length: number;
104
+ }
105
+ /**
106
+ * Per-slot length ratios that stretch a source rig's bone chain onto the standard's proportions, so
107
+ * a retargeted clip moves both rigs the same distance rather than the same angle. Slots with no
108
+ * counterpart on either side are omitted; a source length of zero is skipped instead of dividing.
109
+ *
110
+ * @capability skeleton-standard scale a retargeted clip to the target rig's bone proportions
111
+ */
112
+ export declare function retargetBoneScales(source: readonly SlotLength[], target: readonly SlotLength[]): Partial<Record<SkeletonSlot, number>>;
113
+ /**
114
+ * Bone-name prefixes for a set of standard slots, for an {@link AnimLayer.mask}. A slot on one side
115
+ * contributes its side-less prefix (`upperLeg.l` → `upperleg`), which matches both sides' tracks; an
116
+ * unsided slot contributes its own runtime name. Masking by slot means a rig mapped onto the
117
+ * standard is masked without hand-typing its bone names.
118
+ *
119
+ * @capability skeleton-standard build a layer mask from the standard's own slots
120
+ */
121
+ export declare function standardBoneMask(slots: readonly SkeletonSlot[]): string[];
122
+ /** Standard slots an upper-body action layer drives: spine, chest, head and both arms. */
123
+ export declare const UPPER_BODY_SLOTS: readonly SkeletonSlot[];
124
+ /** Standard slots a lower-body layer drives: hips, both legs and both feet. */
125
+ export declare const LOWER_BODY_SLOTS: readonly SkeletonSlot[];
126
+ /** Layer mask for an upper-body action layer (aim, reload, hit reaction) on a standard rig. */
127
+ export declare const UPPER_BODY_MASK: readonly string[];
128
+ /** Layer mask for a lower-body layer on a standard rig. */
129
+ export declare const LOWER_BODY_MASK: readonly string[];
130
+ /**
131
+ * Renames a clip's tracks from a rig's own bone names onto the standard's, so one authored clip
132
+ * plays on a mapped rig. The emitted node is the **runtime** bone name (see {@link runtimeBoneName}),
133
+ * which is what both the loaded scene graph and the loaded clips spell. A track whose node fills no
134
+ * slot is dropped (`null` in the result) rather than left pointing at a bone the rig does not have;
135
+ * a track whose property is not one of the four three writes is dropped too, since renaming it
136
+ * would not make it resolvable.
137
+ *
138
+ * @capability skeleton-standard rename an animation clip's tracks onto a mapped rig
139
+ */
140
+ export declare function retargetTracks(tracks: readonly string[], map: SkeletonMap): (string | null)[];