@jgengine/core 0.11.0 → 0.13.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 (256) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat/effects.d.ts +11 -4
  9. package/dist/combat/effects.js +24 -17
  10. package/dist/combat/magazine.d.ts +13 -0
  11. package/dist/combat/magazine.js +27 -0
  12. package/dist/combat/vfxPresets.d.ts +176 -0
  13. package/dist/combat/vfxPresets.js +74 -0
  14. package/dist/combat/weaponFire.d.ts +114 -0
  15. package/dist/combat/weaponFire.js +96 -0
  16. package/dist/combat.d.ts +5 -2
  17. package/dist/combat.js +4 -1
  18. package/dist/devtools/controls.d.ts +38 -0
  19. package/dist/devtools/controls.js +181 -0
  20. package/dist/devtools/devtools.d.ts +5 -197
  21. package/dist/devtools/devtools.js +38 -774
  22. package/dist/devtools/discover.d.ts +32 -0
  23. package/dist/devtools/discover.js +240 -0
  24. package/dist/devtools/fallbackSeams.d.ts +50 -0
  25. package/dist/devtools/fallbackSeams.js +111 -0
  26. package/dist/devtools/frame.d.ts +15 -0
  27. package/dist/devtools/frame.js +247 -0
  28. package/dist/devtools/logs.d.ts +15 -0
  29. package/dist/devtools/logs.js +86 -0
  30. package/dist/devtools/overrides.d.ts +16 -0
  31. package/dist/devtools/overrides.js +66 -0
  32. package/dist/devtools/types.d.ts +212 -0
  33. package/dist/devtools/types.js +1 -0
  34. package/dist/devtools/urlFlags.d.ts +22 -0
  35. package/dist/devtools/urlFlags.js +62 -0
  36. package/dist/economy/auctionBook.d.ts +122 -0
  37. package/dist/economy/auctionBook.js +213 -0
  38. package/dist/economy/priceHistory.d.ts +44 -0
  39. package/dist/economy/priceHistory.js +89 -0
  40. package/dist/economy/unlockPoints.d.ts +92 -0
  41. package/dist/economy/unlockPoints.js +103 -0
  42. package/dist/editor/commandApply.d.ts +5 -0
  43. package/dist/editor/commandApply.js +755 -0
  44. package/dist/editor/commands.d.ts +48 -3
  45. package/dist/editor/commands.js +40 -754
  46. package/dist/editor/directives.d.ts +94 -0
  47. package/dist/editor/directives.js +206 -0
  48. package/dist/editor/document.d.ts +37 -2
  49. package/dist/editor/document.js +422 -6
  50. package/dist/editor/entityCatalog.d.ts +42 -0
  51. package/dist/editor/entityCatalog.js +101 -0
  52. package/dist/editor/environment.d.ts +74 -0
  53. package/dist/editor/environment.js +169 -0
  54. package/dist/editor/index.d.ts +26 -1
  55. package/dist/editor/index.js +8 -1
  56. package/dist/editor/minimap.d.ts +31 -0
  57. package/dist/editor/minimap.js +73 -0
  58. package/dist/editor/streamer.d.ts +66 -0
  59. package/dist/editor/streamer.js +68 -0
  60. package/dist/editor/types.d.ts +143 -0
  61. package/dist/editor/world.d.ts +136 -0
  62. package/dist/editor/world.js +303 -0
  63. package/dist/game/breeding.d.ts +161 -0
  64. package/dist/game/breeding.js +181 -0
  65. package/dist/game/cameraConfig.d.ts +451 -0
  66. package/dist/game/cameraConfig.js +1 -0
  67. package/dist/game/clipRoles.d.ts +51 -0
  68. package/dist/game/clipRoles.js +142 -0
  69. package/dist/game/defineGame.d.ts +16 -9
  70. package/dist/game/defineGame.js +14 -6
  71. package/dist/game/defineSystem.d.ts +2 -0
  72. package/dist/game/defineSystem.js +2 -0
  73. package/dist/game/events.d.ts +28 -0
  74. package/dist/game/partAnimation.d.ts +78 -0
  75. package/dist/game/partAnimation.js +147 -0
  76. package/dist/game/playableGame.d.ts +72 -440
  77. package/dist/game/playableGame.js +1 -1
  78. package/dist/game/progression.d.ts +35 -0
  79. package/dist/game/progression.js +22 -1
  80. package/dist/game/quest.js +1 -3
  81. package/dist/game/tribe.d.ts +184 -0
  82. package/dist/game/tribe.js +432 -0
  83. package/dist/gameplay.d.ts +3 -1
  84. package/dist/gameplay.js +3 -1
  85. package/dist/inventory/encumbrance.d.ts +69 -0
  86. package/dist/inventory/encumbrance.js +68 -0
  87. package/dist/inventory/inventoryModel.d.ts +24 -8
  88. package/dist/inventory/inventoryModel.js +47 -8
  89. package/dist/item/durability.js +1 -3
  90. package/dist/meta/changelog.d.ts +1 -1
  91. package/dist/meta/changelog.js +48 -1
  92. package/dist/movement/movementModel.d.ts +15 -1
  93. package/dist/movement/movementModel.js +33 -15
  94. package/dist/movement/playerMovement.js +102 -2
  95. package/dist/multiplayer/combatSnapshot.js +13 -8
  96. package/dist/multiplayer/presenceModel.d.ts +2 -0
  97. package/dist/multiplayer/presenceModel.js +8 -0
  98. package/dist/physics/drivableVehicle.d.ts +2 -0
  99. package/dist/physics/drivableVehicle.js +1 -1
  100. package/dist/physics/flightDynamics.d.ts +82 -0
  101. package/dist/physics/flightDynamics.js +165 -0
  102. package/dist/physics/gravityField.d.ts +25 -0
  103. package/dist/physics/gravityField.js +41 -0
  104. package/dist/physics/kinematicVehicle.d.ts +87 -0
  105. package/dist/physics/kinematicVehicle.js +193 -11
  106. package/dist/physics/physicsWorld.d.ts +26 -0
  107. package/dist/physics/physicsWorld.js +109 -117
  108. package/dist/physics/vehicleObstacles.d.ts +44 -0
  109. package/dist/physics/vehicleObstacles.js +115 -0
  110. package/dist/random/rng.d.ts +4 -1
  111. package/dist/random/rng.js +4 -1
  112. package/dist/runtime/adapter.d.ts +13 -0
  113. package/dist/runtime/adapter.js +13 -0
  114. package/dist/runtime/context/combat.d.ts +51 -0
  115. package/dist/runtime/context/combat.js +131 -0
  116. package/dist/runtime/context/combatFx.js +15 -5
  117. package/dist/runtime/context/deathLoot.d.ts +30 -0
  118. package/dist/runtime/context/deathLoot.js +39 -0
  119. package/dist/runtime/context/player.d.ts +48 -0
  120. package/dist/runtime/context/player.js +125 -0
  121. package/dist/runtime/context/scene.d.ts +60 -0
  122. package/dist/runtime/context/scene.js +329 -0
  123. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  124. package/dist/runtime/context/snapshotCodecs.js +26 -0
  125. package/dist/runtime/descriptors/baseline.js +26 -3
  126. package/dist/runtime/descriptors/features.d.ts +5 -0
  127. package/dist/runtime/descriptors/features.js +20 -2
  128. package/dist/runtime/gameContext.d.ts +2 -428
  129. package/dist/runtime/gameContext.js +133 -500
  130. package/dist/runtime/gameContextTypes.d.ts +500 -0
  131. package/dist/runtime/gameContextTypes.js +1 -0
  132. package/dist/runtime/headlessRunner.d.ts +17 -2
  133. package/dist/runtime/headlessRunner.js +15 -0
  134. package/dist/runtime/hostPolicy.d.ts +51 -0
  135. package/dist/runtime/hostPolicy.js +68 -0
  136. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  137. package/dist/runtime/hostedGameRunner.js +1 -0
  138. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  139. package/dist/runtime/hostedWorldSession.js +1 -0
  140. package/dist/runtime/worldSnapshot.d.ts +13 -1
  141. package/dist/runtime/worldSnapshot.js +15 -3
  142. package/dist/scene/assetCatalog.d.ts +7 -0
  143. package/dist/scene/assetSpace.js +1 -3
  144. package/dist/scene/authoredTriggers.d.ts +50 -0
  145. package/dist/scene/authoredTriggers.js +114 -0
  146. package/dist/scene/builtinSceneKinds.js +8 -3
  147. package/dist/scene/colliders.d.ts +79 -0
  148. package/dist/scene/colliders.js +170 -0
  149. package/dist/scene/collisionMesh.d.ts +94 -0
  150. package/dist/scene/collisionMesh.js +699 -0
  151. package/dist/scene/companion.d.ts +147 -0
  152. package/dist/scene/companion.js +193 -0
  153. package/dist/scene/entityStats.d.ts +7 -9
  154. package/dist/scene/entityStats.js +19 -15
  155. package/dist/scene/sceneKinds.d.ts +16 -0
  156. package/dist/scene/sceneKinds.js +4 -0
  157. package/dist/scene/sceneRaycast.js +15 -0
  158. package/dist/stats/spawnLevelStats.d.ts +110 -0
  159. package/dist/stats/spawnLevelStats.js +140 -0
  160. package/dist/stats/statModifiers.d.ts +16 -0
  161. package/dist/stats/statModifiers.js +30 -1
  162. package/dist/stats/statPool.d.ts +70 -0
  163. package/dist/stats/statPool.js +62 -0
  164. package/dist/store/changeSignal.d.ts +3 -0
  165. package/dist/store/changeSignal.js +2 -0
  166. package/dist/store/observableKeyedStore.d.ts +2 -0
  167. package/dist/store/observableKeyedStore.js +1 -0
  168. package/dist/survival/decayMeter.js +1 -3
  169. package/dist/survival/regionHealth.js +1 -3
  170. package/dist/ui/hudLayout.d.ts +5 -0
  171. package/dist/ui/hudLayout.js +17 -1
  172. package/dist/ui/panelModel.d.ts +120 -0
  173. package/dist/ui/panelModel.js +168 -0
  174. package/dist/ui.d.ts +1 -0
  175. package/dist/ui.js +1 -0
  176. package/dist/world/authoredEntities.d.ts +29 -0
  177. package/dist/world/authoredEntities.js +29 -0
  178. package/dist/world/authoredObjects.d.ts +15 -1
  179. package/dist/world/authoredObjects.js +13 -1
  180. package/dist/world/authoredSpawn.d.ts +39 -0
  181. package/dist/world/authoredSpawn.js +37 -0
  182. package/dist/world/buildSockets.d.ts +152 -0
  183. package/dist/world/buildSockets.js +188 -0
  184. package/dist/world/buildingLots.d.ts +84 -0
  185. package/dist/world/buildingLots.js +119 -0
  186. package/dist/world/buildings.d.ts +6 -0
  187. package/dist/world/cityBlocks.d.ts +152 -0
  188. package/dist/world/cityBlocks.js +670 -0
  189. package/dist/world/cityContent.d.ts +77 -0
  190. package/dist/world/cityContent.js +261 -0
  191. package/dist/world/cityGenerator.d.ts +41 -0
  192. package/dist/world/cityGenerator.js +58 -0
  193. package/dist/world/cityGeometry.d.ts +114 -0
  194. package/dist/world/cityGeometry.js +573 -0
  195. package/dist/world/cityKind.d.ts +314 -0
  196. package/dist/world/cityKind.js +1578 -0
  197. package/dist/world/environmentSummary.js +57 -1
  198. package/dist/world/features.d.ts +53 -5
  199. package/dist/world/features.js +15 -3
  200. package/dist/world/grassKind.js +2 -2
  201. package/dist/world/markers.d.ts +50 -4
  202. package/dist/world/markers.js +49 -0
  203. package/dist/world/massing.js +1 -1
  204. package/dist/world/minimapBake.d.ts +78 -0
  205. package/dist/world/minimapBake.js +208 -0
  206. package/dist/world/minimapTrack.d.ts +17 -0
  207. package/dist/world/minimapTrack.js +22 -0
  208. package/dist/world/place.d.ts +166 -0
  209. package/dist/world/place.js +96 -0
  210. package/dist/world/placeAsset.js +3 -1
  211. package/dist/world/poleLineKind.d.ts +40 -0
  212. package/dist/world/poleLineKind.js +97 -0
  213. package/dist/world/polyline.js +9 -2
  214. package/dist/world/resourceNode.d.ts +109 -0
  215. package/dist/world/resourceNode.js +140 -0
  216. package/dist/world/roads.d.ts +19 -2
  217. package/dist/world/roads.js +60 -3
  218. package/dist/world/scatter.js +2 -20
  219. package/dist/world/streetGenerator.d.ts +159 -0
  220. package/dist/world/streetGenerator.js +684 -0
  221. package/dist/world/streets.d.ts +103 -1
  222. package/dist/world/streets.js +273 -0
  223. package/dist/world/terraform.js +58 -88
  224. package/dist/world/terrain.d.ts +65 -1
  225. package/dist/world/terrain.js +119 -2
  226. package/dist/world.d.ts +14 -5
  227. package/dist/world.js +12 -3
  228. package/package.json +1 -1
  229. package/skills/game-design/SKILL.md +28 -0
  230. package/skills/game-design/agents/openai.yaml +4 -0
  231. package/skills/game-design/references/application-playbook.md +170 -0
  232. package/skills/game-design/references/field-guide.md +288 -0
  233. package/skills/jgengine/SKILL.md +8 -4
  234. package/skills/jgengine/api.md +182 -81
  235. package/skills/jgengine/capabilities.md +104 -2
  236. package/skills/jgengine/recipes/minimal-game.md +88 -0
  237. package/skills/jgengine-combat/SKILL.md +19 -1
  238. package/skills/jgengine-combat/api.md +75 -2
  239. package/skills/jgengine-combat/capabilities.md +61 -1
  240. package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
  241. package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
  242. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  243. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
  244. package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
  245. package/skills/jgengine-combat/reference.md +33 -4
  246. package/skills/jgengine-gameplay/SKILL.md +8 -0
  247. package/skills/jgengine-gameplay/api.md +182 -26
  248. package/skills/jgengine-gameplay/capabilities.md +96 -2
  249. package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
  250. package/skills/jgengine-gameplay/reference-systems.md +10 -5
  251. package/skills/jgengine-world/SKILL.md +13 -0
  252. package/skills/jgengine-world/api.md +453 -21
  253. package/skills/jgengine-world/capabilities.md +112 -2
  254. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  255. package/skills/jgengine-world/reference.md +43 -4
  256. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -13,7 +13,7 @@ import { type EntityStore } from "../scene/entityStore.js";
13
13
  import type { StoreHandle } from "../store/defineStore.js";
14
14
  import type { TimeConfig } from "../time/simClock.js";
15
15
  import type { WorldFeature } from "../world/features.js";
16
- /** Tunes offline whole-world save (`defineGame({ persist })`). Defaults: continuous `autosave` to `localStorage`, one slot, no version. */
16
+ /** Tunes offline whole-world save (`defineGameDefinition({ persist })`). Offline games already autosave to `localStorage` by default — pass this only to tune it (or `persist: false` to opt out). Defaults: continuous `autosave` to `localStorage`, one slot, no version. */
17
17
  export interface PersistConfig {
18
18
  /** `"autosave"` (default) writes on a debounce after any change; `"manual"` writes only on `ctx.game.save.checkpoint()` (save points / quest triggers). */
19
19
  mode?: RuntimeSaveMode;
@@ -140,7 +140,7 @@ export interface LifecycleConfig<TState = unknown> {
140
140
  restart?: string;
141
141
  };
142
142
  }
143
- /** Fully-resolved game description produced by {@link defineGame} — assets, scene, and opted-in subsystems. */
143
+ /** Fully-resolved game description produced by {@link defineGameDefinition} — assets, scene, and opted-in subsystems. */
144
144
  export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> {
145
145
  name: string;
146
146
  assets: AssetCatalog<TAssetRef>;
@@ -156,7 +156,13 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
156
156
  * Classic `loop` hooks still run for incremental migration (systems tick first, then `loop.onTick`).
157
157
  */
158
158
  systems?: readonly SystemDefinition[];
159
+ /**
160
+ * The place this game happens in — preferred shape is `world()` from `@jgengine/core/world/place`
161
+ * (ground + per-place laws; dressing lives in the editor's scene document). Omit entirely for
162
+ * pure UI/rules games that are not a spatial place.
163
+ */
159
164
  world?: WorldFeature;
165
+ /** Game-level default physics laws; a place world's own `physics` resolves over this. */
160
166
  physics?: PhysicsConfig;
161
167
  /** Simulation clock: real→game time scale, selectable speeds, calendar. Exposed as `ctx.time`; the shell feeds its scaled dt to `loop.onTick`. */
162
168
  time?: TimeConfig;
@@ -165,8 +171,9 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
165
171
  inventories?: Record<string, InventoryDeclaration>;
166
172
  input?: ActionCodesMap;
167
173
  server?: GameServerConfig;
174
+ /** Hosted per-player save policy, read only by an authoritative multiplayer host. Solo games leave this unset (nothing persists by default) and use `persist` when they want a save at all. */
168
175
  save?: SaveConfig;
169
- /** Offline/single-player whole-world save. `true` autosaves the entire game to `localStorage`; a config object tunes the mode/cadence/target. Binds `ctx.game.save` — the game drives save points and restore. Ignored for multiplayer worlds (the host persists). */
176
+ /** The one save knob for solo games: offline whole-world save. **On by default** — an offline game autosaves the entire world to `localStorage` and binds `ctx.game.save` with no config at all; the game only drives save points and restore (`ctx.game.save.load()` at boot). Pass `false` to opt out entirely, or a config object to tune the mode/cadence/target (`storage: "memory"` for in-session-only). Ignored for multiplayer worlds (the host persists via `save`). */
170
177
  persist?: boolean | PersistConfig;
171
178
  ui?: unknown;
172
179
  loop?: GameLoop<GameContext>;
@@ -180,14 +187,14 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
180
187
  */
181
188
  replication?: ReplicationPolicy;
182
189
  }
183
- /** Input to {@link defineGame} — a `GameDefinition` with `scene` derived and `assets` optional. */
190
+ /** Input to {@link defineGameDefinition} — a `GameDefinition` with `scene` derived and `assets` optional. */
184
191
  export type GameDefinitionConfig<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> = Omit<GameDefinition<TAssetRef, TMultiplayer>, "scene" | "assets"> & {
185
192
  assets?: AssetCatalog<TAssetRef>;
186
193
  };
187
194
  /**
188
- * Task-first entry point for authoring a game: fills in `scene` and default `assets`, validates `name`,
189
- * OR-merges `features` from installed systems, and composes `loop` from `systems` + any classic hooks.
190
- *
191
- * @capability define-game single public game-authoring path — compose systems, world, and loop in one definition
195
+ * Host-level constructor for a bare `GameDefinition`: fills in `scene` and default `assets`, validates
196
+ * `name`, OR-merges `features` from installed systems, and composes `loop` from `systems` + any classic
197
+ * hooks. Games author through `defineGame` from `@jgengine/shell/defineGame`, which calls this and adds
198
+ * the presentation layer; call this directly only from headless hosts that mount no shell.
192
199
  */
193
- export declare function defineGame<TAssetRef extends ModelAssetRef, TMultiplayer>(config: GameDefinitionConfig<TAssetRef, TMultiplayer>): GameDefinition<TAssetRef, TMultiplayer>;
200
+ export declare function defineGameDefinition<TAssetRef extends ModelAssetRef, TMultiplayer>(config: GameDefinitionConfig<TAssetRef, TMultiplayer>): GameDefinition<TAssetRef, TMultiplayer>;
@@ -2,6 +2,7 @@ import { mergeSystemFeatures } from "./defineSystem.js";
2
2
  import { composeGameLoop } from "./systemRuntime.js";
3
3
  import { createAssetCatalog } from "../scene/assetCatalog.js";
4
4
  import { createEntityStore } from "../scene/entityStore.js";
5
+ import { resolveWorldPhysics } from "../world/place.js";
5
6
  /** @internal */
6
7
  export function createDisposer() {
7
8
  const cleanups = [];
@@ -25,19 +26,26 @@ export function createDisposer() {
25
26
  };
26
27
  }
27
28
  /**
28
- * Task-first entry point for authoring a game: fills in `scene` and default `assets`, validates `name`,
29
- * OR-merges `features` from installed systems, and composes `loop` from `systems` + any classic hooks.
30
- *
31
- * @capability define-game single public game-authoring path — compose systems, world, and loop in one definition
29
+ * Host-level constructor for a bare `GameDefinition`: fills in `scene` and default `assets`, validates
30
+ * `name`, OR-merges `features` from installed systems, and composes `loop` from `systems` + any classic
31
+ * hooks. Games author through `defineGame` from `@jgengine/shell/defineGame`, which calls this and adds
32
+ * the presentation layer; call this directly only from headless hosts that mount no shell.
32
33
  */
33
- export function defineGame(config) {
34
+ export function defineGameDefinition(config) {
34
35
  if (config.name.trim().length === 0) {
35
- throw new Error("defineGame: name must be non-empty");
36
+ throw new Error("defineGameDefinition: name must be non-empty");
36
37
  }
38
+ // Boolean `features: { quest: true }` is sugar for the same activation map systems produce via
39
+ // `feature: "quest"`. Both OR-merge here; `createGameContext` only reads this final map through
40
+ // featureDescriptors.enabled — one enable path, no dual checks at install time.
37
41
  const features = mergeSystemFeatures(config.features, config.systems);
38
42
  const loop = composeGameLoop(config.systems, config.loop);
43
+ // A place world carries the laws of that place: its physics resolves over the game-level default,
44
+ // so every consumer of definition.physics (movement, combat, shell) sees the active world's laws.
45
+ const physics = resolveWorldPhysics(config.world, config.physics);
39
46
  return {
40
47
  ...config,
48
+ ...(physics === undefined ? {} : { physics }),
41
49
  features,
42
50
  loop,
43
51
  scene: createEntityStore(),
@@ -78,6 +78,8 @@ export interface SystemDefinition {
78
78
  /**
79
79
  * Declare a composable game system. Pure data + hooks — the engine compiles the schedule and
80
80
  * installs lifecycle when the game boots.
81
+ *
82
+ * @capability define-system composable game behavior — pure data + hooks the engine schedules; pass via `defineGame({ systems })`
81
83
  */
82
84
  export declare function defineSystem(definition: SystemDefinition): SystemDefinition;
83
85
  /** Collect feature flags implied by a system list (OR-merge; installing a system activates its capability).
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Declare a composable game system. Pure data + hooks — the engine compiles the schedule and
3
3
  * installs lifecycle when the game boots.
4
+ *
5
+ * @capability define-system composable game behavior — pure data + hooks the engine schedules; pass via `defineGame({ systems })`
4
6
  */
5
7
  export function defineSystem(definition) {
6
8
  if (definition.id.trim().length === 0) {
@@ -191,6 +191,31 @@ export interface AudioMusicEvent {
191
191
  theme: string | null;
192
192
  transpose?: number;
193
193
  }
194
+ /**
195
+ * Start (or idempotently keep) the retained, id-keyed audio loop `id` from catalog `sound`, optionally
196
+ * anchored at world `at`. Restarting with the same `sound` does not restart the source (no click); a
197
+ * different `sound` replaces it. Drives RPM-pitched engine loops and slip-scaled tire squeal (#1051).
198
+ */
199
+ export interface AudioLoopStartEvent {
200
+ id: string;
201
+ sound: string;
202
+ at?: readonly [number, number, number];
203
+ }
204
+ /**
205
+ * Live-update the retained loop `id`: `rate` re-pitches it (1 = authored, the shell clamps to 0.25–4),
206
+ * `gain` rescales its volume (0–1), and `at` repositions its emitter. Emitted every tick to track a
207
+ * live signal (RPM, tire slip); the shell smooths rate/gain and ignores an unknown `id` (#1051).
208
+ */
209
+ export interface AudioLoopSetEvent {
210
+ id: string;
211
+ rate?: number;
212
+ gain?: number;
213
+ at?: readonly [number, number, number];
214
+ }
215
+ /** Stop and dispose the retained loop `id`; an unknown `id` is ignored (#1051). */
216
+ export interface AudioLoopStopEvent {
217
+ id: string;
218
+ }
194
219
  export interface GameEventMap {
195
220
  "entity.died": EntityDiedEvent;
196
221
  "entity.floatText": EntityFloatTextEvent;
@@ -221,6 +246,9 @@ export interface GameEventMap {
221
246
  "audio.play": AudioPlayEvent;
222
247
  "audio.music": AudioMusicEvent;
223
248
  "audio.resume": AudioResumeEvent;
249
+ "audio.loopStart": AudioLoopStartEvent;
250
+ "audio.loopSet": AudioLoopSetEvent;
251
+ "audio.loopStop": AudioLoopStopEvent;
224
252
  "entity.animation": EntityAnimationEvent;
225
253
  }
226
254
  export type GameEventHandler<TPayload> = (payload: TPayload) => void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Procedural animation for part-composed (rig-less) models — characters kit-bashed from
3
+ * primitives/`ModelPart`s with no skeleton or clips. Pure, deterministic curve sampling:
4
+ * the same (time, speed, phase, flinch, death) input always yields the same pose, so
5
+ * replays and multi-client renders stay in lockstep and the hot path allocates nothing
6
+ * when an `out` pose is reused.
7
+ */
8
+ /** Semantic motion role of one `ModelPart` in a part-composed character. */
9
+ export type PartRole = "head" | "arm.l" | "arm.r" | "leg.l" | "leg.r" | "tail" | "wing.l" | "wing.r";
10
+ /** Tuning for {@link samplePartPose}/{@link sampleBodyPose}; every field has a sensible default. */
11
+ export interface PartMotionParams {
12
+ /** Full stride cycles per second at walking speed. Default 1.6. */
13
+ strideHz?: number;
14
+ /** Limb swing amplitude in radians at full walk. Default 0.7. */
15
+ swingRad?: number;
16
+ /** Body bob height in model units at full walk. Default 0.04. */
17
+ bobAmp?: number;
18
+ /** Idle sway/breathe amplitude in radians. Default 0.05. */
19
+ swayRad?: number;
20
+ /** Speed (world units/sec) treated as a full walk; swing/bob scale linearly below it. Default 2. */
21
+ walkSpeed?: number;
22
+ /** Backward flinch pitch in radians at flinch = 1. Default 0.35. */
23
+ flinchRad?: number;
24
+ /** Sideways topple roll in radians at death = 1. Default PI / 2. */
25
+ toppleRad?: number;
26
+ /**
27
+ * Squash-and-stretch amplitude for soft characters (blobs, slimes): footfall squash while
28
+ * walking, jelly breathe while idle, a squash pulse on flinch. Volume-conserving — the body
29
+ * bulges horizontally as it compresses. Default 0 (rigid; scale stays exactly 1).
30
+ */
31
+ squashAmp?: number;
32
+ /** How death reads: `"topple"` rolls sideways (default, hard characters); `"splat"` flattens the body out — right for blobs. */
33
+ deathStyle?: "topple" | "splat";
34
+ }
35
+ /** Sampled offsets to add onto a part's authored transform (or the character root). `scale` is multiplicative (1 = unchanged). */
36
+ export interface PartPose {
37
+ position: [number, number, number];
38
+ rotation: [number, number, number];
39
+ scale: [number, number, number];
40
+ }
41
+ /** Live signals a driver feeds the samplers each frame. All values are render-side only. */
42
+ export interface PartMotionInput {
43
+ /** Seconds since the driver started (any monotonic clock). */
44
+ timeSec: number;
45
+ /** Horizontal speed in world units/sec (smoothed by the driver). */
46
+ speed: number;
47
+ /** Per-character phase offset in [0, 1) so crowds don't stride in sync — see {@link partMotionPhase}. */
48
+ phase: number;
49
+ /** Hit-flinch envelope in [0, 1]; the driver sets 1 on a hit and decays it. */
50
+ flinch: number;
51
+ /** Death ramp in [0, 1]; the driver eases it to 1 after the entity dies. */
52
+ death: number;
53
+ }
54
+ /** A reusable identity pose (zero offsets, unit scale). */
55
+ export declare function createPartPose(): PartPose;
56
+ /**
57
+ * Deterministic per-character stride phase in [0, 1) from an id (e.g. the entity instance id),
58
+ * so a crowd of identical part-composed characters doesn't animate in lockstep.
59
+ */
60
+ export declare function partMotionPhase(id: string): number;
61
+ /**
62
+ * Sample the character-root pose: walk bob, idle breathe/sway, backward hit flinch, and death —
63
+ * a sideways topple by default, or a flatten-out splat with `deathStyle: "splat"`. With
64
+ * `squashAmp > 0` the body also squashes and stretches (footfall squash, jelly idle breathe,
65
+ * flinch squash pulse), volume-conserving. A driver applies this to the group wrapping the
66
+ * whole composition (base model plus parts) — limbs ride along, as they would on a real torso.
67
+ *
68
+ * @capability part-motion sample the root bob/sway/squash/flinch/death pose for a rig-less part-composed character
69
+ */
70
+ export declare function sampleBodyPose(input: PartMotionInput, params?: PartMotionParams, out?: PartPose): PartPose;
71
+ /**
72
+ * Sample one part's pose offset by role: counter-phase leg/arm swing driven by speed, a head
73
+ * counter-sway, tail wag, and wing flap. Offsets are added onto the part's authored
74
+ * position/rotation. Motion fades to zero as `death` ramps so the topple reads as one piece.
75
+ *
76
+ * @capability part-motion sample a limb/head/tail/wing pose offset for a role-tagged ModelPart
77
+ */
78
+ export declare function samplePartPose(role: PartRole, input: PartMotionInput, params?: PartMotionParams, out?: PartPose): PartPose;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Procedural animation for part-composed (rig-less) models — characters kit-bashed from
3
+ * primitives/`ModelPart`s with no skeleton or clips. Pure, deterministic curve sampling:
4
+ * the same (time, speed, phase, flinch, death) input always yields the same pose, so
5
+ * replays and multi-client renders stay in lockstep and the hot path allocates nothing
6
+ * when an `out` pose is reused.
7
+ */
8
+ const DEFAULT_STRIDE_HZ = 1.6;
9
+ const DEFAULT_SWING_RAD = 0.7;
10
+ const DEFAULT_BOB_AMP = 0.04;
11
+ const DEFAULT_SWAY_RAD = 0.05;
12
+ const DEFAULT_WALK_SPEED = 2;
13
+ const DEFAULT_FLINCH_RAD = 0.35;
14
+ const DEFAULT_TOPPLE_RAD = Math.PI / 2;
15
+ const IDLE_BREATHE_HZ = 0.45;
16
+ const TWO_PI = Math.PI * 2;
17
+ function clamp01(value) {
18
+ return value < 0 ? 0 : value > 1 ? 1 : value;
19
+ }
20
+ function zeroPose(out) {
21
+ out.position[0] = 0;
22
+ out.position[1] = 0;
23
+ out.position[2] = 0;
24
+ out.rotation[0] = 0;
25
+ out.rotation[1] = 0;
26
+ out.rotation[2] = 0;
27
+ out.scale[0] = 1;
28
+ out.scale[1] = 1;
29
+ out.scale[2] = 1;
30
+ return out;
31
+ }
32
+ /** Volume-conserving vertical squash: compress y by `amount`, bulge x/z by half of it. */
33
+ function applySquash(out, amount) {
34
+ out.scale[1] *= 1 - amount;
35
+ const bulge = 1 + amount * 0.5;
36
+ out.scale[0] *= bulge;
37
+ out.scale[2] *= bulge;
38
+ }
39
+ /** A reusable identity pose (zero offsets, unit scale). */
40
+ export function createPartPose() {
41
+ return { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] };
42
+ }
43
+ /**
44
+ * Deterministic per-character stride phase in [0, 1) from an id (e.g. the entity instance id),
45
+ * so a crowd of identical part-composed characters doesn't animate in lockstep.
46
+ */
47
+ export function partMotionPhase(id) {
48
+ let hash = 0;
49
+ for (let index = 0; index < id.length; index += 1)
50
+ hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
51
+ return (hash % 1000) / 1000;
52
+ }
53
+ const SPLAT_FLATTEN = 0.7;
54
+ /**
55
+ * Sample the character-root pose: walk bob, idle breathe/sway, backward hit flinch, and death —
56
+ * a sideways topple by default, or a flatten-out splat with `deathStyle: "splat"`. With
57
+ * `squashAmp > 0` the body also squashes and stretches (footfall squash, jelly idle breathe,
58
+ * flinch squash pulse), volume-conserving. A driver applies this to the group wrapping the
59
+ * whole composition (base model plus parts) — limbs ride along, as they would on a real torso.
60
+ *
61
+ * @capability part-motion sample the root bob/sway/squash/flinch/death pose for a rig-less part-composed character
62
+ */
63
+ export function sampleBodyPose(input, params, out = createPartPose()) {
64
+ zeroPose(out);
65
+ const walkSpeed = params?.walkSpeed ?? DEFAULT_WALK_SPEED;
66
+ const strideHz = params?.strideHz ?? DEFAULT_STRIDE_HZ;
67
+ const bobAmp = params?.bobAmp ?? DEFAULT_BOB_AMP;
68
+ const swayRad = params?.swayRad ?? DEFAULT_SWAY_RAD;
69
+ const squashAmp = params?.squashAmp ?? 0;
70
+ const death = clamp01(input.death);
71
+ const alive = 1 - death;
72
+ const walk = clamp01(input.speed / walkSpeed) * alive;
73
+ const idle = (1 - walk) * alive;
74
+ const stride = TWO_PI * (input.timeSec * strideHz + input.phase);
75
+ const breathe = TWO_PI * (input.timeSec * IDLE_BREATHE_HZ + input.phase);
76
+ const step = Math.abs(Math.sin(stride));
77
+ // Double-bounce bob: the body rises on each step, twice per stride cycle.
78
+ out.position[1] = step * bobAmp * walk + Math.sin(breathe) * bobAmp * 0.5 * idle;
79
+ out.rotation[2] = Math.sin(breathe) * swayRad * idle;
80
+ out.rotation[0] = -clamp01(input.flinch) * (params?.flinchRad ?? DEFAULT_FLINCH_RAD) * alive;
81
+ if (squashAmp > 0) {
82
+ // Footfall squash: deepest where the bob is lowest (step ≈ 0), released at the top of the step.
83
+ applySquash(out, (1 - step) * squashAmp * walk);
84
+ // Jelly breathe: idle scale oscillation, gentler than a footfall.
85
+ applySquash(out, Math.sin(breathe) * squashAmp * 0.4 * idle);
86
+ // Flinch reads as a squash pulse on a soft body.
87
+ applySquash(out, clamp01(input.flinch) * squashAmp * alive);
88
+ }
89
+ if ((params?.deathStyle ?? "topple") === "splat") {
90
+ applySquash(out, death * SPLAT_FLATTEN);
91
+ }
92
+ else {
93
+ out.rotation[2] += death * (params?.toppleRad ?? DEFAULT_TOPPLE_RAD);
94
+ }
95
+ return out;
96
+ }
97
+ /**
98
+ * Sample one part's pose offset by role: counter-phase leg/arm swing driven by speed, a head
99
+ * counter-sway, tail wag, and wing flap. Offsets are added onto the part's authored
100
+ * position/rotation. Motion fades to zero as `death` ramps so the topple reads as one piece.
101
+ *
102
+ * @capability part-motion sample a limb/head/tail/wing pose offset for a role-tagged ModelPart
103
+ */
104
+ export function samplePartPose(role, input, params, out = createPartPose()) {
105
+ zeroPose(out);
106
+ const walkSpeed = params?.walkSpeed ?? DEFAULT_WALK_SPEED;
107
+ const strideHz = params?.strideHz ?? DEFAULT_STRIDE_HZ;
108
+ const swingRad = params?.swingRad ?? DEFAULT_SWING_RAD;
109
+ const swayRad = params?.swayRad ?? DEFAULT_SWAY_RAD;
110
+ const alive = 1 - clamp01(input.death);
111
+ const walk = clamp01(input.speed / walkSpeed) * alive;
112
+ const idle = (1 - walk) * alive;
113
+ const stride = TWO_PI * (input.timeSec * strideHz + input.phase);
114
+ const breathe = TWO_PI * (input.timeSec * IDLE_BREATHE_HZ + input.phase);
115
+ switch (role) {
116
+ case "leg.l":
117
+ out.rotation[0] = Math.sin(stride) * swingRad * walk;
118
+ break;
119
+ case "leg.r":
120
+ out.rotation[0] = Math.sin(stride + Math.PI) * swingRad * walk;
121
+ break;
122
+ case "arm.l":
123
+ // Arms counter their same-side leg, at slightly lower amplitude.
124
+ out.rotation[0] = Math.sin(stride + Math.PI) * swingRad * 0.8 * walk;
125
+ out.rotation[2] = Math.sin(breathe) * swayRad * idle;
126
+ break;
127
+ case "arm.r":
128
+ out.rotation[0] = Math.sin(stride) * swingRad * 0.8 * walk;
129
+ out.rotation[2] = -Math.sin(breathe) * swayRad * idle;
130
+ break;
131
+ case "head":
132
+ out.rotation[2] = -Math.sin(breathe) * swayRad * idle;
133
+ out.rotation[1] = Math.sin(stride) * swayRad * 0.5 * walk;
134
+ out.rotation[0] = -clamp01(input.flinch) * 0.5 * (params?.flinchRad ?? DEFAULT_FLINCH_RAD) * alive;
135
+ break;
136
+ case "tail":
137
+ out.rotation[1] = Math.sin(stride) * swingRad * 0.5 * walk + Math.sin(breathe) * swayRad * 2 * idle;
138
+ break;
139
+ case "wing.l":
140
+ out.rotation[2] = Math.abs(Math.sin(stride)) * swingRad * walk + Math.sin(breathe) * swayRad * idle;
141
+ break;
142
+ case "wing.r":
143
+ out.rotation[2] = -Math.abs(Math.sin(stride)) * swingRad * walk - Math.sin(breathe) * swayRad * idle;
144
+ break;
145
+ }
146
+ return out;
147
+ }