@jgengine/core 0.11.0 → 0.12.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 (219) hide show
  1. package/CHANGELOG.md +26 -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.d.ts +1 -0
  9. package/dist/combat.js +1 -0
  10. package/dist/devtools/controls.d.ts +38 -0
  11. package/dist/devtools/controls.js +181 -0
  12. package/dist/devtools/devtools.d.ts +5 -197
  13. package/dist/devtools/devtools.js +38 -774
  14. package/dist/devtools/discover.d.ts +32 -0
  15. package/dist/devtools/discover.js +240 -0
  16. package/dist/devtools/fallbackSeams.d.ts +50 -0
  17. package/dist/devtools/fallbackSeams.js +111 -0
  18. package/dist/devtools/frame.d.ts +15 -0
  19. package/dist/devtools/frame.js +247 -0
  20. package/dist/devtools/logs.d.ts +15 -0
  21. package/dist/devtools/logs.js +86 -0
  22. package/dist/devtools/overrides.d.ts +16 -0
  23. package/dist/devtools/overrides.js +66 -0
  24. package/dist/devtools/types.d.ts +212 -0
  25. package/dist/devtools/types.js +1 -0
  26. package/dist/economy/auctionBook.d.ts +122 -0
  27. package/dist/economy/auctionBook.js +213 -0
  28. package/dist/economy/priceHistory.d.ts +44 -0
  29. package/dist/economy/priceHistory.js +89 -0
  30. package/dist/economy/unlockPoints.d.ts +92 -0
  31. package/dist/economy/unlockPoints.js +103 -0
  32. package/dist/editor/commandApply.d.ts +5 -0
  33. package/dist/editor/commandApply.js +755 -0
  34. package/dist/editor/commands.d.ts +48 -3
  35. package/dist/editor/commands.js +40 -754
  36. package/dist/editor/directives.d.ts +94 -0
  37. package/dist/editor/directives.js +206 -0
  38. package/dist/editor/document.d.ts +37 -2
  39. package/dist/editor/document.js +422 -6
  40. package/dist/editor/entityCatalog.d.ts +42 -0
  41. package/dist/editor/entityCatalog.js +101 -0
  42. package/dist/editor/environment.d.ts +74 -0
  43. package/dist/editor/environment.js +169 -0
  44. package/dist/editor/index.d.ts +26 -1
  45. package/dist/editor/index.js +8 -1
  46. package/dist/editor/minimap.d.ts +31 -0
  47. package/dist/editor/minimap.js +73 -0
  48. package/dist/editor/streamer.d.ts +66 -0
  49. package/dist/editor/streamer.js +68 -0
  50. package/dist/editor/types.d.ts +143 -0
  51. package/dist/editor/world.d.ts +136 -0
  52. package/dist/editor/world.js +303 -0
  53. package/dist/game/breeding.d.ts +161 -0
  54. package/dist/game/breeding.js +181 -0
  55. package/dist/game/cameraConfig.d.ts +451 -0
  56. package/dist/game/cameraConfig.js +1 -0
  57. package/dist/game/defineGame.d.ts +16 -9
  58. package/dist/game/defineGame.js +14 -6
  59. package/dist/game/defineSystem.d.ts +2 -0
  60. package/dist/game/defineSystem.js +2 -0
  61. package/dist/game/events.d.ts +28 -0
  62. package/dist/game/playableGame.d.ts +34 -435
  63. package/dist/game/playableGame.js +1 -1
  64. package/dist/game/progression.d.ts +35 -0
  65. package/dist/game/progression.js +19 -0
  66. package/dist/game/quest.js +1 -3
  67. package/dist/game/tribe.d.ts +184 -0
  68. package/dist/game/tribe.js +432 -0
  69. package/dist/gameplay.d.ts +3 -1
  70. package/dist/gameplay.js +3 -1
  71. package/dist/inventory/encumbrance.d.ts +69 -0
  72. package/dist/inventory/encumbrance.js +68 -0
  73. package/dist/item/durability.js +1 -3
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +24 -1
  76. package/dist/movement/movementModel.d.ts +15 -1
  77. package/dist/movement/movementModel.js +33 -15
  78. package/dist/movement/playerMovement.js +102 -2
  79. package/dist/multiplayer/combatSnapshot.js +13 -8
  80. package/dist/multiplayer/presenceModel.d.ts +2 -0
  81. package/dist/multiplayer/presenceModel.js +8 -0
  82. package/dist/physics/drivableVehicle.d.ts +2 -0
  83. package/dist/physics/drivableVehicle.js +1 -1
  84. package/dist/physics/flightDynamics.d.ts +82 -0
  85. package/dist/physics/flightDynamics.js +165 -0
  86. package/dist/physics/gravityField.d.ts +25 -0
  87. package/dist/physics/gravityField.js +41 -0
  88. package/dist/physics/kinematicVehicle.d.ts +87 -0
  89. package/dist/physics/kinematicVehicle.js +193 -11
  90. package/dist/physics/physicsWorld.d.ts +26 -0
  91. package/dist/physics/physicsWorld.js +109 -117
  92. package/dist/physics/vehicleObstacles.d.ts +44 -0
  93. package/dist/physics/vehicleObstacles.js +115 -0
  94. package/dist/random/rng.d.ts +4 -1
  95. package/dist/random/rng.js +4 -1
  96. package/dist/runtime/adapter.d.ts +13 -0
  97. package/dist/runtime/adapter.js +13 -0
  98. package/dist/runtime/context/combat.d.ts +51 -0
  99. package/dist/runtime/context/combat.js +130 -0
  100. package/dist/runtime/context/deathLoot.d.ts +30 -0
  101. package/dist/runtime/context/deathLoot.js +39 -0
  102. package/dist/runtime/context/player.d.ts +48 -0
  103. package/dist/runtime/context/player.js +125 -0
  104. package/dist/runtime/context/scene.d.ts +59 -0
  105. package/dist/runtime/context/scene.js +291 -0
  106. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  107. package/dist/runtime/context/snapshotCodecs.js +26 -0
  108. package/dist/runtime/descriptors/baseline.js +26 -3
  109. package/dist/runtime/descriptors/features.d.ts +5 -0
  110. package/dist/runtime/descriptors/features.js +20 -2
  111. package/dist/runtime/gameContext.d.ts +2 -428
  112. package/dist/runtime/gameContext.js +88 -501
  113. package/dist/runtime/gameContextTypes.d.ts +467 -0
  114. package/dist/runtime/gameContextTypes.js +1 -0
  115. package/dist/runtime/headlessRunner.d.ts +4 -2
  116. package/dist/runtime/headlessRunner.js +1 -0
  117. package/dist/runtime/hostPolicy.d.ts +51 -0
  118. package/dist/runtime/hostPolicy.js +68 -0
  119. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  120. package/dist/runtime/hostedGameRunner.js +1 -0
  121. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  122. package/dist/runtime/hostedWorldSession.js +1 -0
  123. package/dist/runtime/worldSnapshot.d.ts +13 -1
  124. package/dist/runtime/worldSnapshot.js +15 -3
  125. package/dist/scene/assetCatalog.d.ts +5 -0
  126. package/dist/scene/assetSpace.js +1 -3
  127. package/dist/scene/authoredTriggers.d.ts +50 -0
  128. package/dist/scene/authoredTriggers.js +114 -0
  129. package/dist/scene/builtinSceneKinds.js +8 -3
  130. package/dist/scene/colliders.d.ts +58 -0
  131. package/dist/scene/colliders.js +108 -0
  132. package/dist/scene/collisionMesh.d.ts +94 -0
  133. package/dist/scene/collisionMesh.js +699 -0
  134. package/dist/scene/companion.d.ts +147 -0
  135. package/dist/scene/companion.js +193 -0
  136. package/dist/scene/entityStats.d.ts +7 -9
  137. package/dist/scene/entityStats.js +10 -14
  138. package/dist/scene/sceneKinds.d.ts +16 -0
  139. package/dist/scene/sceneKinds.js +4 -0
  140. package/dist/scene/sceneRaycast.js +15 -0
  141. package/dist/stats/spawnLevelStats.d.ts +110 -0
  142. package/dist/stats/spawnLevelStats.js +140 -0
  143. package/dist/stats/statPool.d.ts +70 -0
  144. package/dist/stats/statPool.js +59 -0
  145. package/dist/store/changeSignal.d.ts +3 -0
  146. package/dist/store/changeSignal.js +2 -0
  147. package/dist/store/observableKeyedStore.d.ts +2 -0
  148. package/dist/store/observableKeyedStore.js +1 -0
  149. package/dist/survival/decayMeter.js +1 -3
  150. package/dist/survival/regionHealth.js +1 -3
  151. package/dist/ui/hudLayout.d.ts +5 -0
  152. package/dist/ui/hudLayout.js +17 -1
  153. package/dist/world/authoredEntities.d.ts +29 -0
  154. package/dist/world/authoredEntities.js +29 -0
  155. package/dist/world/authoredObjects.d.ts +15 -1
  156. package/dist/world/authoredObjects.js +13 -1
  157. package/dist/world/authoredSpawn.d.ts +39 -0
  158. package/dist/world/authoredSpawn.js +37 -0
  159. package/dist/world/buildSockets.d.ts +152 -0
  160. package/dist/world/buildSockets.js +188 -0
  161. package/dist/world/cityBlocks.d.ts +152 -0
  162. package/dist/world/cityBlocks.js +670 -0
  163. package/dist/world/cityContent.d.ts +77 -0
  164. package/dist/world/cityContent.js +261 -0
  165. package/dist/world/cityGeometry.d.ts +114 -0
  166. package/dist/world/cityGeometry.js +573 -0
  167. package/dist/world/cityKind.d.ts +314 -0
  168. package/dist/world/cityKind.js +1578 -0
  169. package/dist/world/features.d.ts +18 -3
  170. package/dist/world/features.js +10 -1
  171. package/dist/world/grassKind.js +2 -2
  172. package/dist/world/markers.d.ts +50 -4
  173. package/dist/world/markers.js +49 -0
  174. package/dist/world/massing.js +1 -1
  175. package/dist/world/minimapBake.d.ts +78 -0
  176. package/dist/world/minimapBake.js +208 -0
  177. package/dist/world/minimapTrack.d.ts +17 -0
  178. package/dist/world/minimapTrack.js +22 -0
  179. package/dist/world/pathNetwork.d.ts +159 -0
  180. package/dist/world/pathNetwork.js +684 -0
  181. package/dist/world/place.d.ts +166 -0
  182. package/dist/world/place.js +96 -0
  183. package/dist/world/placeAsset.js +3 -1
  184. package/dist/world/poleLineKind.d.ts +40 -0
  185. package/dist/world/poleLineKind.js +97 -0
  186. package/dist/world/polyline.js +9 -2
  187. package/dist/world/resourceNode.d.ts +109 -0
  188. package/dist/world/resourceNode.js +140 -0
  189. package/dist/world/scatter.js +2 -20
  190. package/dist/world/streets.d.ts +29 -0
  191. package/dist/world/streets.js +55 -0
  192. package/dist/world/terraform.js +58 -88
  193. package/dist/world/terrain.d.ts +45 -0
  194. package/dist/world/terrain.js +78 -0
  195. package/dist/world.d.ts +14 -5
  196. package/dist/world.js +12 -3
  197. package/package.json +1 -1
  198. package/skills/game-design/SKILL.md +28 -0
  199. package/skills/game-design/agents/openai.yaml +4 -0
  200. package/skills/game-design/references/application-playbook.md +170 -0
  201. package/skills/game-design/references/field-guide.md +288 -0
  202. package/skills/jgengine/SKILL.md +7 -4
  203. package/skills/jgengine/api.md +172 -79
  204. package/skills/jgengine/capabilities.md +104 -2
  205. package/skills/jgengine/recipes/minimal-game.md +88 -0
  206. package/skills/jgengine-combat/SKILL.md +5 -1
  207. package/skills/jgengine-combat/api.md +36 -0
  208. package/skills/jgengine-combat/capabilities.md +44 -0
  209. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  210. package/skills/jgengine-combat/reference.md +12 -2
  211. package/skills/jgengine-gameplay/api.md +152 -26
  212. package/skills/jgengine-gameplay/capabilities.md +78 -2
  213. package/skills/jgengine-gameplay/reference-systems.md +1 -5
  214. package/skills/jgengine-world/SKILL.md +13 -0
  215. package/skills/jgengine-world/api.md +419 -15
  216. package/skills/jgengine-world/capabilities.md +108 -2
  217. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  218. package/skills/jgengine-world/reference.md +43 -4
  219. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -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,11 +11,15 @@ 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;
20
25
  }
@@ -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,34 @@ 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;
52
110
  /** @internal */
53
111
  export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
54
112
  /** @internal */
@@ -1,3 +1,4 @@
1
+ import { prepareCollisionMesh } from "./collisionMesh.js";
1
2
  export const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
2
3
  export const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
3
4
  /** Matches the shell's fallback actor capsule (~0.7m wide, feet at y=0 to head at ~1.8m). */
@@ -80,6 +81,113 @@ export function scaledObjectColliders(scale) {
80
81
  },
81
82
  };
82
83
  }
84
+ /**
85
+ * The entity-local AABB the rendered model actually occupies, from measured dims + render config — the
86
+ * same scale/normalize/anchor math the shell uses to place the mesh, so box equals visual by
87
+ * construction. `null` when the model is unmeasured (`dims`/`maxY` absent) or degenerate.
88
+ */
89
+ function fittedModelBox(model) {
90
+ const dims = model.dims;
91
+ if (dims === undefined || dims.maxY === undefined)
92
+ return null;
93
+ const { w, d } = dims.footprint;
94
+ const height = dims.maxY - dims.minY;
95
+ if (!(w > 0) || !(d > 0) || !(height > 0))
96
+ return null;
97
+ if (!Number.isFinite(w) || !Number.isFinite(d) || !Number.isFinite(height))
98
+ return null;
99
+ const normalize = model.targetHeight !== undefined && model.targetHeight > 0 ? model.targetHeight / height : 1;
100
+ const scale = (model.scale ?? 1) * normalize;
101
+ if (!(scale > 0) || !Number.isFinite(scale))
102
+ return null;
103
+ const baseY = model.y ?? 0;
104
+ // The renderer centers/grounds whenever it normalizes to targetHeight or anchors "center" (the
105
+ // default); only a raw-origin model without targetHeight keeps its authored pivot.
106
+ const centered = model.targetHeight !== undefined || (model.anchor ?? "center") === "center";
107
+ const halfExtents = [(w / 2) * scale, (height / 2) * scale, (d / 2) * scale];
108
+ const offset = centered
109
+ ? [0, baseY + (height / 2) * scale, 0]
110
+ : [dims.center.x * scale, baseY + (dims.minY + height / 2) * scale, dims.center.z * scale];
111
+ // Maps the model point (center.x, minY, center.z) to the entity-local (0, baseY, 0) the box grounds
112
+ // on when centered; a raw-origin model keeps its authored pivot and only takes the render offset.
113
+ const meshTranslate = centered
114
+ ? [-dims.center.x * scale, baseY - dims.minY * scale, -dims.center.z * scale]
115
+ : [0, baseY, 0];
116
+ return { halfExtents, offset, scale, meshTranslate };
117
+ }
118
+ /** Map a model-space point onto entity-local space with the mesh shape's uniform `scale` and per-axis
119
+ * `translate` (`coord * scale + translate`) — the same composition applied to `meshScale`/`meshTranslate`,
120
+ * so a model-space AABB corner lands exactly where the placed mesh puts it. `scale > 0` keeps min<max. */
121
+ function scaleTranslatePoint(point, scale, translate) {
122
+ return [point[0] * scale + translate[0], point[1] * scale + translate[1], point[2] * scale + translate[2]];
123
+ }
124
+ /** The fitted collider shape for a rendered model: a mesh-accurate triangle shape when the model
125
+ * opts in with a decodable `collisionMesh`, otherwise the conservative fitted box. `null` when the
126
+ * model is unmeasured or degenerate.
127
+ */
128
+ function fittedBodyShape(model) {
129
+ const box = fittedModelBox(model);
130
+ if (box === null)
131
+ return null;
132
+ const mesh = model.collisionMesh !== undefined ? prepareCollisionMesh(model.collisionMesh) : null;
133
+ if (mesh !== null) {
134
+ const sourceBoxes = model.collisionMesh?.boxes;
135
+ const boxes = sourceBoxes !== undefined && sourceBoxes.length > 0
136
+ ? sourceBoxes.map((b) => ({
137
+ min: scaleTranslatePoint(b.min, box.scale, box.meshTranslate),
138
+ max: scaleTranslatePoint(b.max, box.scale, box.meshTranslate),
139
+ }))
140
+ : undefined;
141
+ return {
142
+ kind: "mesh",
143
+ mesh,
144
+ meshScale: box.scale,
145
+ meshTranslate: box.meshTranslate,
146
+ halfExtents: box.halfExtents,
147
+ ...(boxes !== undefined ? { boxes } : {}),
148
+ offset: box.offset,
149
+ };
150
+ }
151
+ return { kind: "aabb", halfExtents: box.halfExtents, offset: box.offset };
152
+ }
153
+ /** Damage hitbox fitted to the rendered model's measured bounds — the model-aware replacement for the
154
+ * fixed humanoid default. `null` when the model is unmeasured, keeping the caller's existing fallback.
155
+ * @internal
156
+ */
157
+ export function fittedEntityColliders(model) {
158
+ const shape = fittedBodyShape(model);
159
+ if (shape === null)
160
+ return null;
161
+ return {
162
+ hitboxes: [
163
+ {
164
+ name: "body",
165
+ purpose: "damage",
166
+ shape,
167
+ damageEligible: true,
168
+ blocks: false,
169
+ },
170
+ ],
171
+ };
172
+ }
173
+ /** Blocking physical body fitted to the rendered model's measured bounds — the model-aware replacement
174
+ * for the unit-cube-times-scale guess. `null` when the model is unmeasured.
175
+ * @internal
176
+ */
177
+ export function fittedObjectColliders(model) {
178
+ const shape = fittedBodyShape(model);
179
+ if (shape === null)
180
+ return null;
181
+ return {
182
+ body: {
183
+ name: "body",
184
+ purpose: "physical",
185
+ shape,
186
+ damageEligible: false,
187
+ blocks: true,
188
+ },
189
+ };
190
+ }
83
191
  /** @internal */
84
192
  export function resolveColliders(set) {
85
193
  if (set === null || set === undefined)
@@ -0,0 +1,94 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ /**
3
+ * Compact, serializable triangle collision mesh measured at asset reindex — the renderer-free source
4
+ * of mesh-accurate hitboxes. Positions are welded onto a 16-bit grid spanning `min`..`max` (model
5
+ * space) and stored base64-encoded, so an opted-in catalog asset ships its collision triangles inside
6
+ * the generated index without the runtime ever touching the rendered scene graph.
7
+ */
8
+ export interface CollisionMeshData {
9
+ /** Model-space bounds of the encoded vertices — the quantization frame. */
10
+ min: readonly [number, number, number];
11
+ max: readonly [number, number, number];
12
+ /** Welded vertex count; `positions` holds exactly `vertexCount * 3` u16 values. */
13
+ vertexCount: number;
14
+ /** Triangle count; `indices` holds exactly `triangleCount * 3` values. */
15
+ triangleCount: number;
16
+ /** Base64 little-endian u16 xyz triples, each axis normalized 0..65535 across `min`..`max`. */
17
+ positions: string;
18
+ /** Base64 little-endian triangle vertex indices — u16, or u32 when `vertexCount` exceeds 65536. */
19
+ indices: string;
20
+ /**
21
+ * Optional compact model-space solid decomposition from {@link voxelizeToBoxes} — a deterministic set
22
+ * of AABBs approximating the model's filled volume so movement obstruction can slide a capsule against
23
+ * the real pillars/lintel of a concave opening (walk THROUGH an arch) instead of one outer box. Absent
24
+ * for models whose voxelization is empty, keeping box-less assets byte-identical.
25
+ */
26
+ boxes?: readonly {
27
+ min: readonly [number, number, number];
28
+ max: readonly [number, number, number];
29
+ }[];
30
+ }
31
+ /** Raw triangle soup for {@link encodeCollisionMesh}: model-space xyz triples plus triangle indices. */
32
+ export interface CollisionMeshSource {
33
+ positions: ArrayLike<number>;
34
+ indices: ArrayLike<number>;
35
+ }
36
+ /**
37
+ * Decoded, BVH-indexed triangles ready for per-ray queries — build once per asset via
38
+ * {@link prepareCollisionMesh}. Treat the fields as opaque; they exist so the raycast hot path can
39
+ * run allocation-free over flat arrays.
40
+ */
41
+ export interface PreparedCollisionMesh {
42
+ /** Dequantized model-space vertex xyz triples. */
43
+ readonly positions: Float32Array;
44
+ /** Triangle vertex indices, 3 per triangle. */
45
+ readonly indices: Uint32Array;
46
+ /** Per-node AABBs, 6 floats (minXYZ, maxXYZ) per node. */
47
+ readonly nodeBounds: Float32Array;
48
+ /** Leaf: first slot in `triOrder`. Internal: right-child node index (left child is `node + 1`). */
49
+ readonly nodeStart: Int32Array;
50
+ /** Leaf: triangle count (> 0). Internal: 0. */
51
+ readonly nodeCount: Int32Array;
52
+ /** Triangle indices grouped by leaf ranges. */
53
+ readonly triOrder: Uint32Array;
54
+ }
55
+ /** Nearest triangle impact from {@link raycastCollisionMesh}: world distance along the ray and the world-space geometric normal facing the ray. */
56
+ export interface CollisionMeshHit {
57
+ distance: number;
58
+ normal: EntityPosition;
59
+ }
60
+ /**
61
+ * Quantize a triangle soup onto the 16-bit grid, weld coincident vertices, and drop triangles that
62
+ * collapse — the reindex-time compressor producing {@link CollisionMeshData}. Returns `null` when no
63
+ * finite, non-degenerate triangle survives.
64
+ */
65
+ export declare function encodeCollisionMesh(source: CollisionMeshSource): CollisionMeshData | null;
66
+ /**
67
+ * Decompose a triangle soup into a compact, deterministic set of model-space AABBs approximating its
68
+ * solid volume — the movement-obstruction counterpart to the raycast BVH. Rasterizes triangles onto a
69
+ * `cellSize` voxel grid, fills each vertical column between its lowest and highest occupied voxel (so a
70
+ * solid becomes solid and a concave opening — an archway — keeps its central gap empty while its pillars
71
+ * and lintel fill in), then greedy-merges occupied voxels into AABBs in a fixed z→y→x order. Pure and
72
+ * allocation-tolerant (runs once per asset at reindex). Caps the output at `maxBoxes`, folding any
73
+ * remainder into one enclosing AABB so obstruction stays conservative. Returns `[]` for degenerate input.
74
+ * @internal
75
+ */
76
+ export declare function voxelizeToBoxes(source: CollisionMeshSource, cellSize?: number, maxBoxes?: number): {
77
+ min: [number, number, number];
78
+ max: [number, number, number];
79
+ }[];
80
+ /**
81
+ * Decode a stored collision mesh and index it with a BVH. Memoized per data object, so the shell and
82
+ * a headless host preparing the same catalog entry share one build. Returns `null` (also memoized)
83
+ * when the payload is malformed.
84
+ */
85
+ export declare function prepareCollisionMesh(data: CollisionMeshData): PreparedCollisionMesh | null;
86
+ /**
87
+ * Nearest triangle hit for a world-space ray against a prepared mesh placed like its rendered model:
88
+ * model space scaled uniformly by `scale`, translated by entity-local `translate`, yaw-rotated by
89
+ * `rotationY`, and moved to `position` — the same composition {@link "scene/colliders".ModelBodySource}
90
+ * fitting applies to the AABB. Allocation-free until the returned hit. `distance` is world distance
91
+ * along `direction` (callers pass `direction` normalized); `normal` is the world-space geometric
92
+ * triangle normal facing the ray.
93
+ */
94
+ export declare function raycastCollisionMesh(mesh: PreparedCollisionMesh, origin: EntityPosition, direction: EntityPosition, maxDistance: number, position: EntityPosition, rotationY: number, scale: number, translate: EntityPosition): CollisionMeshHit | null;