@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
@@ -0,0 +1,68 @@
1
+ import { normalizeJoinCode } from "../multiplayer/matchmaking.js";
2
+ /**
3
+ * True when a server's `visibility` should surface in public listings / browse results.
4
+ * @capability host-listing-filter exclude private sessions from public browse results
5
+ */
6
+ export function isListablePublicly(visibility) {
7
+ return (visibility ?? "public") !== "private";
8
+ }
9
+ /**
10
+ * Private-server join-code gate. Existing members always pass; non-members must present a
11
+ * matching `joinCode` (loose-normalized via {@link normalizeJoinCode}). Callers still decide
12
+ * whether the server is private — this only answers the code/membership half.
13
+ * @capability host-join-code-gate membership-or-code gate for private hosted servers
14
+ */
15
+ export function canJoinPrivateServer(args) {
16
+ if (args.isMember)
17
+ return true;
18
+ if (args.joinCode === undefined || args.suppliedCode === undefined)
19
+ return false;
20
+ return normalizeJoinCode(args.suppliedCode) === normalizeJoinCode(args.joinCode);
21
+ }
22
+ /** Whether `userId` is already on the server's member roster. */
23
+ export function isServerMember(memberUserIds, userId) {
24
+ return memberUserIds.includes(userId);
25
+ }
26
+ /**
27
+ * True when the server has no free slots for a non-member. Existing members never count as
28
+ * "full" so rejoin/leave cycles keep working.
29
+ */
30
+ export function isServerFull(memberUserIds, slotsPerServer, userId) {
31
+ return memberUserIds.length >= slotsPerServer && !memberUserIds.includes(userId);
32
+ }
33
+ /**
34
+ * Whether a private-visibility server blocks this join (non-member without a matching code).
35
+ * Public / undefined visibility never blocks.
36
+ */
37
+ export function isPrivateJoinBlocked(args) {
38
+ if ((args.visibility ?? "public") !== "private")
39
+ return false;
40
+ return !canJoinPrivateServer({
41
+ isMember: isServerMember(args.memberUserIds, args.userId),
42
+ joinCode: args.joinCode,
43
+ suppliedCode: args.suppliedCode,
44
+ });
45
+ }
46
+ /** Roster after a successful join: unchanged when already a member, else appended. */
47
+ export function withJoinedMember(memberUserIds, userId) {
48
+ return memberUserIds.includes(userId) ? [...memberUserIds] : [...memberUserIds, userId];
49
+ }
50
+ /** Roster after a leave: `userId` removed; order of remaining members preserved. */
51
+ export function withoutMember(memberUserIds, userId) {
52
+ return memberUserIds.filter((id) => id !== userId);
53
+ }
54
+ /** Status after a leave: empty rooms reopen; non-empty rooms keep their current status. */
55
+ export function statusAfterLeave(remainingMemberCount, currentStatus) {
56
+ return remainingMemberCount === 0 ? "open" : currentStatus;
57
+ }
58
+ /**
59
+ * Auto-match candidate when no `serverId` is supplied: already a member, or a public room with
60
+ * free capacity. Private rooms are never auto-picked (join-by-code / direct id only).
61
+ */
62
+ export function isAutoJoinCandidate(args) {
63
+ if (isServerMember(args.memberUserIds, args.userId))
64
+ return true;
65
+ if (args.memberUserIds.length >= args.slotsPerServer)
66
+ return false;
67
+ return isListablePublicly(args.visibility);
68
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CommandResult } from "../commands/commandRegistry.js";
2
2
  import type { GameDefinition, LoopPlayer } from "../game/defineGame.js";
3
3
  import type { ModelAssetRef } from "../scene/assetCatalog.js";
4
- import { type GameContext, type GameContextContent } from "./gameContext.js";
4
+ import { type GameContext, type GameContextContent, type GameContextModels } from "./gameContext.js";
5
5
  import { type InputFrame } from "./inputSnapshot.js";
6
6
  import { type WorldDiff } from "./worldReplication.js";
7
7
  import type { SnapshotViewer, WorldSnapshot } from "./worldSnapshot.js";
@@ -21,6 +21,8 @@ export interface HostedGameRunnerOptions<TAssetRef extends ModelAssetRef, TMulti
21
21
  * snapshot overlays the world state it seeded. Omit for a long-lived stateful host (ws) that keeps one runner.
22
22
  */
23
23
  restore?: WorldSnapshot;
24
+ /** Render-model lookup for collider auto-fit — pass the same lookup the shell derives from `entityModels`/`objectModels` so host and clients resolve identical hitboxes. */
25
+ models?: GameContextModels;
24
26
  }
25
27
  /**
26
28
  * The GameContext-loop equivalent of the pure-reducer `createGameHost`: one authoritative `createGameContext`
@@ -16,6 +16,7 @@ export function createHostedGameRunner(options) {
16
16
  player: host ?? { userId: "host", isNew: true },
17
17
  ...(now === undefined ? {} : { now }),
18
18
  ...(definition.replication === undefined ? {} : { replication: definition.replication }),
19
+ ...(options.models === undefined ? {} : { models: options.models }),
19
20
  });
20
21
  const loop = definition.loop ?? {};
21
22
  const replicator = createWorldReplicator(() => ctx.snapshot(), {
@@ -1,6 +1,6 @@
1
1
  import type { CommandResult } from "../commands/commandRegistry.js";
2
2
  import type { ModelAssetRef } from "../scene/assetCatalog.js";
3
- import type { GameContext, GameContextContent } from "./gameContext.js";
3
+ import type { GameContext, GameContextContent, GameContextModels } from "./gameContext.js";
4
4
  import type { GameDefinition, LoopPlayer } from "../game/defineGame.js";
5
5
  import { type HostedGameRunner, type InputFrame } from "./hostedGameRunner.js";
6
6
  import type { WorldDiff } from "./worldReplication.js";
@@ -42,6 +42,8 @@ export interface HostedWorldSessionOptions<TAssetRef extends ModelAssetRef, TMul
42
42
  store?: HostedWorldStore;
43
43
  /** Minimum elapsed ms between auto-saves on tick, measured against `now` (defaults to `Date.now` when omitted). Default `0` — persist on every revision-changing tick. */
44
44
  saveIntervalMs?: number;
45
+ /** Render-model lookup for collider auto-fit, forwarded to the runner — see {@link HostedGameRunnerOptions.models}. */
46
+ models?: GameContextModels;
45
47
  }
46
48
  /**
47
49
  * The stateful host substrate a long-lived backend binds (ws server, browser P2P host): a live
@@ -25,6 +25,7 @@ export function createHostedWorldSession(options) {
25
25
  ...(host === undefined ? {} : { host }),
26
26
  ...(now === undefined ? {} : { now }),
27
27
  ...(loaded === null ? {} : { restore: loaded.snapshot }),
28
+ ...(options.models === undefined ? {} : { models: options.models }),
28
29
  });
29
30
  let savedRevision = loaded?.revision ?? 0;
30
31
  let lastSaveAt = clock();
@@ -13,11 +13,20 @@ export interface SnapshotViewer {
13
13
  * - {@link project} narrows this module's snapshot to what one viewer may see (private state, area of
14
14
  * interest). Absent → the module is world-public and every client receives it verbatim (the default).
15
15
  * - {@link version} is a monotone counter the host reads to skip re-serializing an unchanged module.
16
+ * - {@link decode} validates/coerces wire `unknown` into `T` before hydrate; returning `null` skips
17
+ * that module (fail-soft) instead of poisoning live state with a bare cast.
16
18
  */
17
19
  export interface SnapshotModule<T = unknown> {
18
20
  readonly key: string;
19
21
  snapshot(): T;
20
22
  hydrate(data: T): void;
23
+ /**
24
+ * Optional wire codec: turn the raw snapshot value into a typed `T`, or `null` to skip this module.
25
+ * When present, {@link applyWorldSnapshot} runs it before {@link hydrate} and never calls hydrate on
26
+ * a failed decode — a bad host/client payload leaves the prior module state intact rather than
27
+ * throwing or applying garbage.
28
+ */
29
+ decode?(raw: unknown): T | null;
21
30
  /**
22
31
  * Narrow this module's full snapshot to what `viewer` is allowed to see — return the same shape
23
32
  * carrying only the visible subset (drop other players' private state, cull entities outside the
@@ -38,7 +47,10 @@ export type WorldSnapshot = Record<string, unknown>;
38
47
  * @internal
39
48
  */
40
49
  export declare function composeWorldSnapshot(modules: readonly SnapshotModule[], viewer?: SnapshotViewer): WorldSnapshot;
41
- /** Hydrate every registered module whose key is present in `snapshot`; keys absent from it are left untouched.
50
+ /**
51
+ * Hydrate every registered module whose key is present in `snapshot`; keys absent from it are left
52
+ * untouched. When a module provides {@link SnapshotModule.decode}, a `null` decode skips that module
53
+ * (fail-soft) instead of calling hydrate with unvalidated wire data.
42
54
  * @internal
43
55
  */
44
56
  export declare function applyWorldSnapshot(modules: readonly SnapshotModule[], snapshot: WorldSnapshot): void;
@@ -18,13 +18,25 @@ export function composeWorldSnapshot(modules, viewer) {
18
18
  }
19
19
  return snapshot;
20
20
  }
21
- /** Hydrate every registered module whose key is present in `snapshot`; keys absent from it are left untouched.
21
+ /**
22
+ * Hydrate every registered module whose key is present in `snapshot`; keys absent from it are left
23
+ * untouched. When a module provides {@link SnapshotModule.decode}, a `null` decode skips that module
24
+ * (fail-soft) instead of calling hydrate with unvalidated wire data.
22
25
  * @internal
23
26
  */
24
27
  export function applyWorldSnapshot(modules, snapshot) {
25
28
  for (const module of modules) {
26
- if (Object.prototype.hasOwnProperty.call(snapshot, module.key)) {
27
- module.hydrate(snapshot[module.key]);
29
+ if (!Object.prototype.hasOwnProperty.call(snapshot, module.key))
30
+ continue;
31
+ const raw = snapshot[module.key];
32
+ if (module.decode !== undefined) {
33
+ const decoded = module.decode(raw);
34
+ if (decoded === null)
35
+ continue;
36
+ module.hydrate(decoded);
37
+ }
38
+ else {
39
+ module.hydrate(raw);
28
40
  }
29
41
  }
30
42
  }
@@ -1,4 +1,5 @@
1
1
  import type { AssetSpace } from "./assetSpace.js";
2
+ import type { CollisionMeshData } from "./collisionMesh.js";
2
3
  /** Measured horizontal footprint, footprint center, and lowest Y of a model in model space. */
3
4
  export interface ModelDims {
4
5
  footprint: {
@@ -10,13 +11,19 @@ export interface ModelDims {
10
11
  z: number;
11
12
  };
12
13
  minY: number;
14
+ /** Highest Y in model space (pre-scale). `maxY - minY` is the measured model height; absent in indexes generated before it was measured. */
15
+ maxY?: number;
13
16
  }
14
17
  export interface ModelAssetRef {
15
18
  url: string;
16
19
  /** Measured at asset reindex: horizontal footprint, footprint center, and lowest Y in model space (pre-scale). Lets the shell auto-center and ground-snap corner-pivot kit models. */
17
20
  dims?: ModelDims;
21
+ /** Opt-in compact triangle mesh extracted at asset reindex (see {@link "scene/collisionMesh".CollisionMeshData}) — feeds mesh-accurate hitboxes for concave models. */
22
+ collisionMesh?: CollisionMeshData;
18
23
  /** Authored asset-space metadata — canonical facing (degrees), source-unit scale, footprint, anchor, bounds, and rotation policy. The upstream owner of placement corrections; see {@link "scene/assetSpace".AssetSpace}. */
19
24
  space?: AssetSpace;
25
+ /** Animation clip names read from the GLB at asset reindex. Lets the shell auto-animate rigged catalog assets (semantic clip roles → default idle/walk/run + one-shots) with no game-side clip strings. */
26
+ clips?: readonly string[];
20
27
  }
21
28
  /**
22
29
  * A catalog entry backed by a registered {@link registerAssetGenerator} instead of a GLB URL — a
@@ -1,3 +1,4 @@
1
+ import { clamp } from "../math/scalar.js";
1
2
  /**
2
3
  * Asset-space metadata + placement rotation policy: the catalog-owned description of how a model sits
3
4
  * in its source space and how a placement tool may rotate it. Games author scenes against engine north
@@ -11,9 +12,6 @@
11
12
  const DEG2RAD = Math.PI / 180;
12
13
  const RAD2DEG = 180 / Math.PI;
13
14
  const FULL_TURN = 360;
14
- function clamp(value, min, max) {
15
- return value < min ? min : value > max ? max : value;
16
- }
17
15
  /**
18
16
  * Wrap `degrees` into `[0, 360)` — the canonical heading range every policy and conversion here works
19
17
  * in. Non-finite input collapses to `0` so bad metadata never leaks a `NaN` heading downstream.
@@ -71,6 +71,23 @@ export declare function getTriggerAction(id: string): TriggerActionDefinition |
71
71
  export declare function listTriggerActions(target?: TriggerSourceKind): TriggerActionDefinition[];
72
72
  /** Clears the action registry — tests only. @internal */
73
73
  export declare function clearTriggerActions(): void;
74
+ /**
75
+ * The engine's built-in trigger actions — the shared `announce` / `win` / `advance` vocabulary every
76
+ * game gets for free, so "enter this zone → win" is authored entirely in the editor with no bespoke
77
+ * action code. Their params land flat on the object's `meta` like any registered action; drive them
78
+ * at runtime with the handlers from {@link createTriggerOutcome}.
79
+ *
80
+ * @capability authored-triggers built-in announce/win/advance actions authorable on any marker or volume
81
+ */
82
+ export declare const BUILTIN_TRIGGER_ACTIONS: readonly TriggerActionDefinition[];
83
+ /**
84
+ * Register the engine's built-in {@link BUILTIN_TRIGGER_ACTIONS} so they appear in the editor's
85
+ * trigger inspector. Idempotent; call at module load (a game may still register its own actions or
86
+ * re-register a built-in id to override it).
87
+ *
88
+ * @capability authored-triggers register the shared announce/win/advance actions
89
+ */
90
+ export declare function registerBuiltinTriggerActions(): void;
74
91
  /** Resolve the proximity radius for a marker trigger from meta. @internal */
75
92
  export declare function triggerRadiusOf(meta: Record<string, unknown> | undefined, fallback?: number): number;
76
93
  /**
@@ -156,6 +173,39 @@ export declare function createAuthoredTriggerRuntime(options: {
156
173
  /** Override the collected trigger list (tests / hot-reload). Default: {@link collectAuthoredTriggers}. */
157
174
  triggers?: readonly AuthoredTrigger[];
158
175
  }): AuthoredTriggerRuntime;
176
+ /** Serializable read-model the built-in trigger actions write — the HUD/game reads it each frame. */
177
+ export interface TriggerOutcome {
178
+ /** True once a `win` action fired. */
179
+ won: boolean;
180
+ /** Latest announce/win/advance message to surface, or null before any fires. */
181
+ message: string | null;
182
+ /** Tone for the latest message: `"info" | "warn" | "good"`. */
183
+ tone: string;
184
+ /** Current objective id from the latest `advance`, or null. */
185
+ objective: string | null;
186
+ /** Bumped on every change — a cheap key for `useSyncExternalStore`. */
187
+ revision: number;
188
+ }
189
+ /** Outcome store: the built-in action handlers plus a subscribable snapshot of what they produced. */
190
+ export interface TriggerOutcomeStore {
191
+ /** Current outcome snapshot (stable reference until the next change). */
192
+ get(): TriggerOutcome;
193
+ /** Subscribe to outcome changes; returns an unsubscribe fn. */
194
+ subscribe(listener: () => void): () => void;
195
+ /** Handlers for `createAuthoredTriggerRuntime({ handlers })` — wires announce/win/advance. */
196
+ handlers: TriggerHandlers;
197
+ /** Reset to the initial (un-won) outcome — new run / retry. */
198
+ reset(): void;
199
+ }
200
+ /**
201
+ * Default runtime for the built-in {@link BUILTIN_TRIGGER_ACTIONS}: a small serializable outcome the
202
+ * HUD renders, with no engine-side UI. Pass {@link TriggerOutcomeStore.handlers} to
203
+ * {@link createAuthoredTriggerRuntime} and read {@link TriggerOutcomeStore.get} from the HUD via
204
+ * `useSyncExternalStore` — that is the whole zero-code "reach the zone to win" wiring.
205
+ *
206
+ * @capability authored-triggers headless win/announce/objective outcome for the built-in actions
207
+ */
208
+ export declare function createTriggerOutcome(): TriggerOutcomeStore;
159
209
  /**
160
210
  * Build a meta patch that sets a single flat trigger (`on` + `action` + optional param fields) and
161
211
  * clears a multi-trigger list if present. Used by the editor inspector.
@@ -41,6 +41,64 @@ export function listTriggerActions(target) {
41
41
  export function clearTriggerActions() {
42
42
  actionRegistry.clear();
43
43
  }
44
+ /**
45
+ * The engine's built-in trigger actions — the shared `announce` / `win` / `advance` vocabulary every
46
+ * game gets for free, so "enter this zone → win" is authored entirely in the editor with no bespoke
47
+ * action code. Their params land flat on the object's `meta` like any registered action; drive them
48
+ * at runtime with the handlers from {@link createTriggerOutcome}.
49
+ *
50
+ * @capability authored-triggers built-in announce/win/advance actions authorable on any marker or volume
51
+ */
52
+ export const BUILTIN_TRIGGER_ACTIONS = [
53
+ {
54
+ id: "announce",
55
+ label: "Announce",
56
+ schema: {
57
+ fields: [
58
+ { type: "text", key: "message", label: "Message", default: "Entered zone" },
59
+ {
60
+ type: "select",
61
+ key: "tone",
62
+ label: "Tone",
63
+ options: [
64
+ { value: "info", label: "Info" },
65
+ { value: "warn", label: "Warn" },
66
+ { value: "good", label: "Good" },
67
+ ],
68
+ default: "info",
69
+ },
70
+ ],
71
+ },
72
+ },
73
+ {
74
+ id: "win",
75
+ label: "Win the game",
76
+ schema: {
77
+ fields: [{ type: "text", key: "message", label: "Message", default: "You win!" }],
78
+ },
79
+ },
80
+ {
81
+ id: "advance",
82
+ label: "Advance objective",
83
+ schema: {
84
+ fields: [
85
+ { type: "text", key: "objective", label: "Next objective", default: "" },
86
+ { type: "text", key: "message", label: "Message", default: "" },
87
+ ],
88
+ },
89
+ },
90
+ ];
91
+ /**
92
+ * Register the engine's built-in {@link BUILTIN_TRIGGER_ACTIONS} so they appear in the editor's
93
+ * trigger inspector. Idempotent; call at module load (a game may still register its own actions or
94
+ * re-register a built-in id to override it).
95
+ *
96
+ * @capability authored-triggers register the shared announce/win/advance actions
97
+ */
98
+ export function registerBuiltinTriggerActions() {
99
+ for (const definition of BUILTIN_TRIGGER_ACTIONS)
100
+ registerTriggerAction(definition);
101
+ }
44
102
  function isTriggerEvent(value) {
45
103
  return typeof value === "string" && TRIGGER_EVENTS.includes(value);
46
104
  }
@@ -311,6 +369,62 @@ export function createAuthoredTriggerRuntime(options) {
311
369
  },
312
370
  };
313
371
  }
372
+ const INITIAL_OUTCOME = { won: false, message: null, tone: "info", objective: null, revision: 0 };
373
+ /**
374
+ * Default runtime for the built-in {@link BUILTIN_TRIGGER_ACTIONS}: a small serializable outcome the
375
+ * HUD renders, with no engine-side UI. Pass {@link TriggerOutcomeStore.handlers} to
376
+ * {@link createAuthoredTriggerRuntime} and read {@link TriggerOutcomeStore.get} from the HUD via
377
+ * `useSyncExternalStore` — that is the whole zero-code "reach the zone to win" wiring.
378
+ *
379
+ * @capability authored-triggers headless win/announce/objective outcome for the built-in actions
380
+ */
381
+ export function createTriggerOutcome() {
382
+ let outcome = INITIAL_OUTCOME;
383
+ const listeners = new Set();
384
+ const commit = (next) => {
385
+ outcome = { ...next, revision: outcome.revision + 1 };
386
+ for (const listener of listeners)
387
+ listener();
388
+ };
389
+ const messageOf = (event, fallback) => {
390
+ const value = event.params.message;
391
+ return typeof value === "string" && value.length > 0 ? value : fallback;
392
+ };
393
+ const stringParam = (event, key) => {
394
+ const value = event.params[key];
395
+ return typeof value === "string" && value.length > 0 ? value : undefined;
396
+ };
397
+ return {
398
+ get: () => outcome,
399
+ subscribe(listener) {
400
+ listeners.add(listener);
401
+ return () => {
402
+ listeners.delete(listener);
403
+ };
404
+ },
405
+ handlers: {
406
+ announce: (event) => {
407
+ commit({ won: outcome.won, message: messageOf(event, "Entered zone"), tone: stringParam(event, "tone") ?? "info", objective: outcome.objective });
408
+ },
409
+ win: (event) => {
410
+ commit({ won: true, message: messageOf(event, "You win!"), tone: "good", objective: outcome.objective });
411
+ },
412
+ advance: (event) => {
413
+ commit({
414
+ won: outcome.won,
415
+ message: stringParam(event, "message") ?? outcome.message,
416
+ tone: outcome.tone,
417
+ objective: stringParam(event, "objective") ?? outcome.objective,
418
+ });
419
+ },
420
+ },
421
+ reset() {
422
+ outcome = INITIAL_OUTCOME;
423
+ for (const listener of listeners)
424
+ listener();
425
+ },
426
+ };
427
+ }
314
428
  /**
315
429
  * Build a meta patch that sets a single flat trigger (`on` + `action` + optional param fields) and
316
430
  * clears a multi-trigger list if present. Used by the editor inspector.
@@ -2,16 +2,19 @@
2
2
  * Registers the engine's built-in *environment* scene kinds against the {@link registerSceneKind}
3
3
  * seam: `scatter` (the proof adopter — its inspector, `+ Add` entry, and parse now come from the
4
4
  * registry), `water` (a reusable parametric water surface), `grass_field` (GPU vertex-wind grass), and
5
- * `soil` (crack/moss ground-variation patch). These are genre-agnostic engine primitives, like terrain.
6
- * Domain studios (poles, bookcases, …) are NOT built in — they register themselves from example/game
7
- * code via the same public seam, needing zero engine edits (see `examples/studios`). Call
5
+ * `soil` (crack/moss ground-variation patch), and `pole_line` (poles + sagging cables along a path).
6
+ * These are genre-agnostic engine primitives, like terrain. Domain studios (bookcases, …) are NOT
7
+ * built in — they register themselves from example/game code via the same public seam, needing zero
8
+ * engine edits (see `examples/studios`). Call
8
9
  * {@link registerBuiltinSceneKinds} once at startup (idempotent).
9
10
  */
10
11
  import { SCATTER_PATH_KIND } from "../world/scatterRegion.js";
11
12
  import { registerWaterKind } from "../world/waterKind.js";
12
13
  import { registerGrassKind } from "../world/grassKind.js";
13
14
  import { registerSoilKind } from "../world/soilKind.js";
15
+ import { registerPoleLineKind } from "../world/poleLineKind.js";
14
16
  import { registerBuildingGenerator } from "../world/buildingGenerator.js";
17
+ import { registerCityKind } from "../world/cityKind.js";
15
18
  import { registerSceneKind } from "./sceneKinds.js";
16
19
  /** The scatter/foliage region schema — the fields the inspector exposed as hand-written `ScatterFields`. */
17
20
  export const SCATTER_SCHEMA = {
@@ -58,5 +61,7 @@ export function registerBuiltinSceneKinds() {
58
61
  registerWaterKind();
59
62
  registerGrassKind();
60
63
  registerSoilKind();
64
+ registerCityKind();
65
+ registerPoleLineKind();
61
66
  registerBuildingGenerator();
62
67
  }
@@ -1,5 +1,14 @@
1
+ import type { ModelDims } from "./assetCatalog.js";
2
+ import { type CollisionMeshData, type PreparedCollisionMesh } from "./collisionMesh.js";
1
3
  import type { EntityPosition } from "./entityStore.js";
2
4
  export type ColliderPurpose = "physical" | "damage";
5
+ /**
6
+ * Collision geometry in entity-local space. `sphere` and `aabb` are analytic; `mesh` carries a
7
+ * prepared triangle mesh so opted-in concave models raycast their real surface while bounds and
8
+ * broadphase keep reading the conservative `halfExtents`.
9
+ * @capability mesh-hitboxes shots pass through holes in concave models — opted-in catalog assets raycast their actual triangles instead of the fitted box.
10
+ * @capability mesh-movement the walking player slides against a mesh collider's compound `boxes` — a capsule walks THROUGH an opted-in archway opening and is stopped by its pillars/lintel instead of one outer box.
11
+ */
3
12
  export type ColliderShape = {
4
13
  kind: "sphere";
5
14
  radius: number;
@@ -8,6 +17,27 @@ export type ColliderShape = {
8
17
  kind: "aabb";
9
18
  halfExtents: EntityPosition;
10
19
  offset?: EntityPosition;
20
+ } | {
21
+ kind: "mesh";
22
+ /** Prepared triangle mesh in model space — engine-derived from an opted-in catalog asset, never hand-authored. */
23
+ mesh: PreparedCollisionMesh;
24
+ /** Uniform model→entity-local scale (same composition as the fitted box). */
25
+ meshScale: number;
26
+ /** Entity-local translation applied after `meshScale`. */
27
+ meshTranslate: EntityPosition;
28
+ /** Conservative entity-local AABB of the placed mesh — bounds/broadphase read this exactly like an `aabb`. */
29
+ halfExtents: EntityPosition;
30
+ /**
31
+ * Optional compact entity-local solid decomposition (from the model's `collisionMesh.boxes`, scaled
32
+ * and translated onto the placement exactly like `mesh`). Movement obstruction slides against these
33
+ * sub-boxes so a capsule walks THROUGH a concave opening; raycasts still use `mesh`. Absent when the
34
+ * model shipped no `boxes`.
35
+ */
36
+ boxes?: readonly {
37
+ min: EntityPosition;
38
+ max: EntityPosition;
39
+ }[];
40
+ offset?: EntityPosition;
11
41
  };
12
42
  export interface ColliderDef {
13
43
  name: string;
@@ -49,6 +79,55 @@ export declare function defaultObjectColliders(halfExtents?: EntityPosition): En
49
79
  * @internal
50
80
  */
51
81
  export declare function scaledObjectColliders(scale: readonly [number, number, number]): EntityColliderSet;
82
+ /**
83
+ * The render-config subset collider fitting reads — structurally satisfied by a resolved `ModelConfig`,
84
+ * so the shell can hand its render config straight to the fitting math without a conversion step.
85
+ */
86
+ export interface ModelBodySource {
87
+ /** Measured model-space bounds from the asset index; fitting requires `dims` with `maxY`. */
88
+ dims?: ModelDims;
89
+ /** Uniform render scale. Default `1`. */
90
+ scale?: number;
91
+ /** Normalize-to-height in world units — fitting composes it exactly like the renderer (`targetHeight / measured height`, multiplied by `scale`). */
92
+ targetHeight?: number;
93
+ /** Vertical render offset added to the placement Y. Default `0`. */
94
+ y?: number;
95
+ /** Placement registration. `"center"` (default) centers the footprint and grounds `minY` on the placement point; `"origin"` renders at the raw model origin. */
96
+ anchor?: "center" | "origin";
97
+ /** Opt-in triangle collision mesh extracted at asset reindex; when present, fitting emits a mesh-accurate shape (rays pass through holes in concave models) instead of the fitted box. */
98
+ collisionMesh?: CollisionMeshData;
99
+ }
100
+ /** Damage hitbox fitted to the rendered model's measured bounds — the model-aware replacement for the
101
+ * fixed humanoid default. `null` when the model is unmeasured, keeping the caller's existing fallback.
102
+ * @internal
103
+ */
104
+ export declare function fittedEntityColliders(model: ModelBodySource): EntityColliderSet | null;
105
+ /** Blocking physical body fitted to the rendered model's measured bounds — the model-aware replacement
106
+ * for the unit-cube-times-scale guess. `null` when the model is unmeasured.
107
+ * @internal
108
+ */
109
+ export declare function fittedObjectColliders(model: ModelBodySource): EntityColliderSet | null;
110
+ /**
111
+ * Entity-local AABB of what the renderer actually mounted for a kind — the runtime counterpart to
112
+ * index-measured `dims`. Reported through `ctx.scene.entity.reportBounds` /
113
+ * `ctx.scene.object.reportBounds` (the shell does this automatically for custom
114
+ * `renderEntity`/`renderObject` content and models without index dims) so collider resolution can
115
+ * wrap the real rendered shape instead of a fixed-size guess.
116
+ */
117
+ export interface MeasuredBounds {
118
+ min: EntityPosition;
119
+ max: EntityPosition;
120
+ }
121
+ /** Damage hitbox exactly wrapping runtime-measured render bounds — what `reportBounds` stores for an
122
+ * entity kind. `null` when the bounds are degenerate, keeping the caller's existing fallback.
123
+ * @internal
124
+ */
125
+ export declare function measuredEntityColliders(bounds: MeasuredBounds): EntityColliderSet | null;
126
+ /** Blocking physical body exactly wrapping runtime-measured render bounds — what `reportBounds`
127
+ * stores for an object catalog id. `null` when the bounds are degenerate.
128
+ * @internal
129
+ */
130
+ export declare function measuredObjectColliders(bounds: MeasuredBounds): EntityColliderSet | null;
52
131
  /** @internal */
53
132
  export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
54
133
  /** @internal */