@jgengine/core 0.18.1 → 0.19.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 (220) hide show
  1. package/CHANGELOG.md +158 -7
  2. package/dist/ai/decisionGraph.d.ts +48 -4
  3. package/dist/ai/decisionGraph.js +153 -23
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/pursuit.d.ts +1 -1
  7. package/dist/ai/pursuit.js +1 -1
  8. package/dist/ai/threat.d.ts +8 -0
  9. package/dist/ai/threat.js +10 -0
  10. package/dist/anim/animGraph.d.ts +11 -0
  11. package/dist/anim/animGraph.js +128 -0
  12. package/dist/anim/boneTexture.d.ts +55 -0
  13. package/dist/anim/boneTexture.js +43 -0
  14. package/dist/anim/footPlacement.d.ts +57 -0
  15. package/dist/anim/footPlacement.js +67 -0
  16. package/dist/anim/locomotionGraph.d.ts +9 -0
  17. package/dist/anim/locomotionGraph.js +29 -0
  18. package/dist/audio/audioFalloff.d.ts +2 -0
  19. package/dist/audio/doppler.d.ts +21 -0
  20. package/dist/audio/doppler.js +32 -0
  21. package/dist/audio/engineLayers.d.ts +83 -0
  22. package/dist/audio/engineLayers.js +115 -0
  23. package/dist/combat/abilityKit.d.ts +1 -0
  24. package/dist/combat/abilityKit.js +1 -0
  25. package/dist/combat/death.d.ts +7 -0
  26. package/dist/combat/shotOrigin.d.ts +6 -0
  27. package/dist/combat/shotOrigin.js +6 -0
  28. package/dist/combat/weaponFeedback.d.ts +36 -0
  29. package/dist/combat/weaponFeedback.js +42 -0
  30. package/dist/combat/weaponHandling.d.ts +161 -0
  31. package/dist/combat/weaponHandling.js +202 -0
  32. package/dist/combat/weaponPresentation.d.ts +115 -0
  33. package/dist/combat/weaponPresentation.js +96 -0
  34. package/dist/devtools/warnOnce.d.ts +8 -0
  35. package/dist/devtools/warnOnce.js +18 -0
  36. package/dist/editor/document.js +3 -1
  37. package/dist/editor/environment.js +8 -0
  38. package/dist/editor/types.d.ts +4 -0
  39. package/dist/faction/reputation.d.ts +6 -0
  40. package/dist/faction/reputation.js +11 -0
  41. package/dist/game/cameraConfig.d.ts +51 -1
  42. package/dist/game/events.d.ts +6 -0
  43. package/dist/game/lootTable.d.ts +4 -0
  44. package/dist/game/lootTable.js +16 -0
  45. package/dist/game/playableGame.d.ts +84 -17
  46. package/dist/game/toasts.d.ts +8 -0
  47. package/dist/game/toasts.js +7 -0
  48. package/dist/game/vfxInstance.d.ts +9 -0
  49. package/dist/game/vfxInstance.js +12 -1
  50. package/dist/game/viewports.d.ts +43 -0
  51. package/dist/game/viewports.js +46 -0
  52. package/dist/game/worldItem.d.ts +5 -1
  53. package/dist/game/worldItem.js +0 -1
  54. package/dist/input/actionBindings.d.ts +15 -1
  55. package/dist/input/actionBindings.js +14 -3
  56. package/dist/input/actionContexts.d.ts +25 -1
  57. package/dist/input/actionContexts.js +82 -25
  58. package/dist/input/axisInput.d.ts +4 -0
  59. package/dist/input/axisInput.js +4 -0
  60. package/dist/input/axisShaper.d.ts +73 -0
  61. package/dist/input/axisShaper.js +105 -0
  62. package/dist/input/gamepadModel.d.ts +36 -2
  63. package/dist/input/gamepadModel.js +69 -30
  64. package/dist/input/haptics.d.ts +50 -0
  65. package/dist/input/haptics.js +74 -0
  66. package/dist/input/inputBuffer.d.ts +40 -0
  67. package/dist/input/inputBuffer.js +59 -0
  68. package/dist/input/lookChannel.d.ts +10 -0
  69. package/dist/input/lookChannel.js +6 -0
  70. package/dist/input/pointer.d.ts +2 -0
  71. package/dist/input/pointer.js +4 -0
  72. package/dist/item/affix.d.ts +6 -0
  73. package/dist/item/affix.js +6 -0
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +112 -1
  76. package/dist/movement/glideModel.d.ts +3 -0
  77. package/dist/movement/glideModel.js +6 -0
  78. package/dist/movement/grappleSwing.d.ts +2 -0
  79. package/dist/movement/grappleSwing.js +5 -1
  80. package/dist/movement/movementModel.d.ts +39 -0
  81. package/dist/movement/movementModel.js +62 -19
  82. package/dist/movement/movementProbe.d.ts +51 -0
  83. package/dist/movement/movementProbe.js +119 -0
  84. package/dist/movement/playerMovement.d.ts +21 -2
  85. package/dist/movement/playerMovement.js +89 -6
  86. package/dist/movement/solidObstacles.d.ts +9 -1
  87. package/dist/movement/solidObstacles.js +44 -2
  88. package/dist/movement/voxelController.js +7 -7
  89. package/dist/nav/navFromEnvironment.d.ts +9 -3
  90. package/dist/nav/navFromEnvironment.js +18 -12
  91. package/dist/nav/navMesh.d.ts +43 -2
  92. package/dist/nav/navMesh.js +534 -128
  93. package/dist/physics/aircraftDynamics.d.ts +390 -0
  94. package/dist/physics/aircraftDynamics.js +704 -0
  95. package/dist/physics/boatDynamics.d.ts +116 -0
  96. package/dist/physics/boatDynamics.js +160 -0
  97. package/dist/physics/drivableVehicle.d.ts +22 -8
  98. package/dist/physics/drivableVehicle.js +4 -2
  99. package/dist/physics/flightDynamics.d.ts +21 -4
  100. package/dist/physics/flightDynamics.js +28 -5
  101. package/dist/physics/handlingProbe.d.ts +249 -0
  102. package/dist/physics/handlingProbe.js +606 -0
  103. package/dist/physics/kinematicVehicle.d.ts +32 -2
  104. package/dist/physics/kinematicVehicle.js +15 -0
  105. package/dist/physics/physicsBackend.d.ts +12 -0
  106. package/dist/physics/physicsBackendConformance.js +34 -0
  107. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  108. package/dist/physics/physicsWorldBackend.js +33 -0
  109. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  110. package/dist/physics/vehicleBackendLink.js +102 -0
  111. package/dist/physics/vehicleDynamics.d.ts +374 -0
  112. package/dist/physics/vehicleDynamics.js +737 -0
  113. package/dist/physics/worldColliders.d.ts +1 -1
  114. package/dist/physics/worldColliders.js +35 -3
  115. package/dist/puzzle/cellGrid.d.ts +15 -9
  116. package/dist/puzzle/cellGrid.js +13 -9
  117. package/dist/render/lookPreset.d.ts +9 -2
  118. package/dist/render/lookPreset.js +29 -2
  119. package/dist/render/postProcessing.d.ts +30 -0
  120. package/dist/render/postProcessing.js +1 -1
  121. package/dist/runtime/cameraDirector.d.ts +17 -3
  122. package/dist/runtime/cameraDirector.js +25 -0
  123. package/dist/runtime/commandInput.d.ts +37 -0
  124. package/dist/runtime/commandInput.js +58 -0
  125. package/dist/runtime/commandRunner.d.ts +27 -0
  126. package/dist/runtime/commandRunner.js +31 -3
  127. package/dist/runtime/context/combat.d.ts +1 -0
  128. package/dist/runtime/context/combat.js +2 -1
  129. package/dist/runtime/context/deathLoot.d.ts +2 -0
  130. package/dist/runtime/context/deathLoot.js +1 -0
  131. package/dist/runtime/gameContext.js +7 -0
  132. package/dist/runtime/gameContextTypes.d.ts +19 -3
  133. package/dist/runtime/gameRuntime.d.ts +3 -1
  134. package/dist/runtime/gameRuntime.js +4 -0
  135. package/dist/runtime/inputSnapshot.d.ts +6 -0
  136. package/dist/runtime/inputSnapshot.js +10 -0
  137. package/dist/runtime/localPlayers.d.ts +73 -0
  138. package/dist/runtime/localPlayers.js +119 -0
  139. package/dist/runtime/transport.d.ts +9 -0
  140. package/dist/runtime/transport.js +7 -1
  141. package/dist/scene/behaviorRuntime.d.ts +12 -2
  142. package/dist/scene/behaviorRuntime.js +30 -14
  143. package/dist/scene/behaviors.d.ts +5 -1
  144. package/dist/scene/sceneKinds.d.ts +9 -0
  145. package/dist/scene/vehicleSeat.d.ts +5 -1
  146. package/dist/scene/vehicleSeat.js +5 -1
  147. package/dist/sensor/concealment.d.ts +6 -0
  148. package/dist/sensor/concealment.js +8 -0
  149. package/dist/sensor/freezeMonitor.d.ts +6 -0
  150. package/dist/sensor/freezeMonitor.js +8 -0
  151. package/dist/sensor/perception.d.ts +11 -1
  152. package/dist/sensor/perception.js +57 -10
  153. package/dist/sensor/recordingBuffer.d.ts +3 -0
  154. package/dist/sensor/recordingBuffer.js +8 -0
  155. package/dist/session/roundState.d.ts +4 -0
  156. package/dist/session/roundState.js +11 -0
  157. package/dist/settings/frameRateLimit.d.ts +16 -0
  158. package/dist/settings/frameRateLimit.js +37 -0
  159. package/dist/settings/graphicsProfile.d.ts +2 -0
  160. package/dist/settings/graphicsProfile.js +12 -0
  161. package/dist/settings/graphicsSettings.d.ts +40 -0
  162. package/dist/settings/graphicsSettings.js +81 -0
  163. package/dist/settings/settingsModel.d.ts +9 -1
  164. package/dist/settings/settingsModel.js +10 -0
  165. package/dist/stats/eventMeter.d.ts +1 -1
  166. package/dist/stats/eventMeter.js +1 -1
  167. package/dist/vfx/feedbackMixer.d.ts +61 -0
  168. package/dist/vfx/feedbackMixer.js +106 -0
  169. package/dist/visibility/simulationCulling.d.ts +8 -1
  170. package/dist/visibility/simulationCulling.js +7 -0
  171. package/dist/visibility/spatialIndex.d.ts +21 -0
  172. package/dist/visibility/spatialIndex.js +16 -0
  173. package/dist/world/authoredSolids.d.ts +46 -0
  174. package/dist/world/authoredSolids.js +75 -0
  175. package/dist/world/buildingGenerator.js +2 -2
  176. package/dist/world/buildingKit.d.ts +3 -1
  177. package/dist/world/buildings.d.ts +34 -2
  178. package/dist/world/buildings.js +11 -1
  179. package/dist/world/cityKind.d.ts +7 -0
  180. package/dist/world/cityKind.js +52 -1
  181. package/dist/world/features.d.ts +29 -2
  182. package/dist/world/features.js +6 -0
  183. package/dist/world/footprintGrid.d.ts +7 -0
  184. package/dist/world/footprintGrid.js +16 -0
  185. package/dist/world/interiors.d.ts +1 -0
  186. package/dist/world/interiors.js +5 -3
  187. package/dist/world/lod.d.ts +6 -0
  188. package/dist/world/lod.js +8 -0
  189. package/dist/world/terraform.d.ts +8 -0
  190. package/dist/world/terraform.js +7 -0
  191. package/dist/world/walls.d.ts +7 -0
  192. package/dist/world/walls.js +7 -0
  193. package/dist/world/worldSolids.d.ts +84 -0
  194. package/dist/world/worldSolids.js +240 -0
  195. package/dist/world.d.ts +2 -2
  196. package/dist/world.js +1 -1
  197. package/package.json +1 -1
  198. package/skills/ce-handoff/SKILL.md +1 -1
  199. package/skills/game-design/SKILL.md +2 -2
  200. package/skills/jgengine/SKILL.md +7 -5
  201. package/skills/jgengine/api.md +118 -89
  202. package/skills/jgengine/capabilities.md +28 -0
  203. package/skills/jgengine/recipes/minimal-game.md +3 -0
  204. package/skills/jgengine-combat/SKILL.md +5 -0
  205. package/skills/jgengine-combat/api.md +138 -103
  206. package/skills/jgengine-combat/capabilities.md +29 -1
  207. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  208. package/skills/jgengine-gameplay/api.md +462 -436
  209. package/skills/jgengine-gameplay/capabilities.md +24 -0
  210. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  211. package/skills/jgengine-world/SKILL.md +15 -3
  212. package/skills/jgengine-world/api.md +873 -626
  213. package/skills/jgengine-world/capabilities.md +130 -6
  214. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  215. package/skills/jgengine-world/recipes/controls.md +44 -0
  216. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  217. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  218. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  219. package/skills/jgengine-world/reference.md +31 -2
  220. package/skills/jgengine-world/visibility.md +112 -0
@@ -0,0 +1,106 @@
1
+ /** Sample a {@link FeedbackCurve}; an empty curve is the identity. */
2
+ export function sampleFeedbackCurve(curve, x) {
3
+ if (curve === undefined || curve.length === 0)
4
+ return x;
5
+ const first = curve[0];
6
+ if (x <= first[0])
7
+ return first[1];
8
+ for (let i = 1; i < curve.length; i += 1) {
9
+ const [x1, y1] = curve[i];
10
+ if (x <= x1) {
11
+ const [x0, y0] = curve[i - 1];
12
+ const span = x1 - x0;
13
+ return span <= 0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / span;
14
+ }
15
+ }
16
+ return curve[curve.length - 1][1];
17
+ }
18
+ function approach(current, target, attack, release, dt) {
19
+ const rate = target > current ? attack : release;
20
+ if (!Number.isFinite(rate))
21
+ return target;
22
+ const step = Math.max(0, rate) * dt;
23
+ return Math.abs(target - current) <= step ? target : current + Math.sign(target - current) * step;
24
+ }
25
+ /**
26
+ * Creates a {@link FeedbackMixer}: the one place a game declares how sim telemetry drives presentation —
27
+ * rpm to engine pitch, tire saturation to squeal and rumble, speed to FOV, landing speed to a thud — instead
28
+ * of hand-writing that glue per vehicle, aircraft or weapon. Deterministic, allocation-free per update.
29
+ * @capability feedback-mixer map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
30
+ */
31
+ export function createFeedbackMixer(initial) {
32
+ let config = initial;
33
+ let routeValues = [];
34
+ let eventSignals = [];
35
+ let cooldowns = [];
36
+ let firedNow = new Set();
37
+ const output = {};
38
+ const touched = new Set();
39
+ function shapeState() {
40
+ routeValues = config.routes.map((_, i) => routeValues[i] ?? 0);
41
+ eventSignals = (config.events ?? []).map((_, i) => eventSignals[i] ?? 0);
42
+ cooldowns = (config.events ?? []).map((_, i) => cooldowns[i] ?? 0);
43
+ for (const route of config.routes)
44
+ output[route.target] ??= 0;
45
+ for (const key of Object.keys(config.base ?? {}))
46
+ output[key] ??= config.base?.[key] ?? 0;
47
+ }
48
+ shapeState();
49
+ return {
50
+ update(dt, signals) {
51
+ const step = Math.max(0, dt);
52
+ for (const key of Object.keys(output))
53
+ output[key] = config.base?.[key] ?? 0;
54
+ touched.clear();
55
+ for (let i = 0; i < config.routes.length; i += 1) {
56
+ const route = config.routes[i];
57
+ const target = sampleFeedbackCurve(route.curve, signals[route.signal] ?? 0);
58
+ const attack = route.attack ?? Number.POSITIVE_INFINITY;
59
+ const value = approach(routeValues[i], target, attack, route.release ?? attack, step);
60
+ routeValues[i] = value;
61
+ const mode = config.combine?.[route.target] ?? "sum";
62
+ if (mode === "max") {
63
+ const current = touched.has(route.target) ? output[route.target] : Number.NEGATIVE_INFINITY;
64
+ output[route.target] = Math.max(current, value);
65
+ }
66
+ else
67
+ output[route.target] += value;
68
+ touched.add(route.target);
69
+ }
70
+ firedNow.clear();
71
+ const events = config.events ?? [];
72
+ for (let i = 0; i < events.length; i += 1) {
73
+ const event = events[i];
74
+ const value = signals[event.signal] ?? 0;
75
+ cooldowns[i] = Math.max(0, cooldowns[i] - step);
76
+ if (value >= event.threshold && eventSignals[i] < event.threshold && cooldowns[i] === 0) {
77
+ firedNow.add(event.id);
78
+ cooldowns[i] = event.cooldown ?? 0;
79
+ }
80
+ eventSignals[i] = value;
81
+ }
82
+ return output;
83
+ },
84
+ fired: (id) => firedNow.has(id),
85
+ value: () => output,
86
+ retune(next) {
87
+ config = next;
88
+ shapeState();
89
+ },
90
+ snapshot: () => ({ routes: [...routeValues], eventSignals: [...eventSignals], cooldowns: [...cooldowns] }),
91
+ restore(state) {
92
+ routeValues = [...state.routes];
93
+ eventSignals = [...state.eventSignals];
94
+ cooldowns = [...state.cooldowns];
95
+ shapeState();
96
+ },
97
+ reset() {
98
+ routeValues = routeValues.map(() => 0);
99
+ eventSignals = eventSignals.map(() => 0);
100
+ cooldowns = cooldowns.map(() => 0);
101
+ firedNow = new Set();
102
+ for (const key of Object.keys(output))
103
+ output[key] = config.base?.[key] ?? 0;
104
+ },
105
+ };
106
+ }
@@ -1,4 +1,4 @@
1
- import type { LodBand } from "../world/lod.js";
1
+ import type { LodBand, LodSchedulerState } from "../world/lod.js";
2
2
  /**
3
3
  * Simulation culling is a SEPARATE, opt-in system from render culling. Render culling only
4
4
  * decides what is drawn; this decides whether a low-priority off-screen entity updates this
@@ -23,6 +23,11 @@ export interface SimulationDecision {
23
23
  /** Seconds of accumulated time delivered when `update` is true (for catch-up integration). */
24
24
  elapsed: number;
25
25
  }
26
+ /** Plain JSON state of a {@link SimulationCuller}: the enabled flag and each id's throttle accumulator. */
27
+ export interface SimulationCullerState {
28
+ enabled: boolean;
29
+ scheduler: LodSchedulerState;
30
+ }
26
31
  export interface SimulationCuller {
27
32
  enabled(): boolean;
28
33
  setEnabled(value: boolean): void;
@@ -30,5 +35,7 @@ export interface SimulationCuller {
30
35
  step(id: string, distance: number, dt: number): SimulationDecision;
31
36
  forget(id: string): void;
32
37
  clear(): void;
38
+ snapshot(): SimulationCullerState;
39
+ restore(next: SimulationCullerState): void;
33
40
  }
34
41
  export declare function createSimulationCuller(options?: SimulationCullingOptions): SimulationCuller;
@@ -32,5 +32,12 @@ export function createSimulationCuller(options = {}) {
32
32
  clear() {
33
33
  scheduler.clear();
34
34
  },
35
+ snapshot() {
36
+ return { enabled, scheduler: scheduler.snapshot() };
37
+ },
38
+ restore(next) {
39
+ enabled = next.enabled;
40
+ scheduler.restore(next.scheduler);
41
+ },
35
42
  };
36
43
  }
@@ -10,6 +10,25 @@ export interface SpatialIndexOptions {
10
10
  */
11
11
  readonly maxCellSpan?: number;
12
12
  }
13
+ /** Integer cell bounds an object covers, inclusive on both ends. */
14
+ export interface SpatialCellRange {
15
+ x0: number;
16
+ y0: number;
17
+ z0: number;
18
+ x1: number;
19
+ y1: number;
20
+ z1: number;
21
+ }
22
+ /**
23
+ * Plain JSON state of a {@link SpatialIndex}. Cells and their ids keep insertion order, so a restored
24
+ * index returns query results in the same order.
25
+ */
26
+ export interface SpatialIndexState {
27
+ /** Per object: its cell range (`null` when oversized) and whether it was inserted as dynamic. */
28
+ entries: [id: string, range: SpatialCellRange | null, dynamic: boolean][];
29
+ cells: [x: number, y: number, z: number, ids: string[]][];
30
+ oversized: string[];
31
+ }
13
32
  /**
14
33
  * A uniform 3D spatial hash the renderer and streaming system query for potentially-visible
15
34
  * objects instead of scanning the whole scene. Objects are keyed by their world AABB into
@@ -39,6 +58,8 @@ export interface SpatialIndex {
39
58
  }[];
40
59
  cellSize(): number;
41
60
  clear(): void;
61
+ snapshot(): SpatialIndexState;
62
+ restore(next: SpatialIndexState): void;
42
63
  }
43
64
  export declare function createSpatialIndex(options?: SpatialIndexOptions): SpatialIndex;
44
65
  /** Precise AABB-vs-AABB test re-exported for callers refining a broad-phase result. */
@@ -206,6 +206,22 @@ export function createSpatialIndex(options = {}) {
206
206
  entries.clear();
207
207
  stamps.clear();
208
208
  },
209
+ snapshot() {
210
+ return {
211
+ entries: [...entries].map(([id, entry]) => [id, entry.range === null ? null : { ...entry.range }, entry.dynamic]),
212
+ cells: [...cells.values()].map((bucket) => [bucket.x, bucket.y, bucket.z, [...bucket.ids]]),
213
+ oversized: [...oversized],
214
+ };
215
+ },
216
+ restore(next) {
217
+ this.clear();
218
+ for (const [id, range, dynamic] of next.entries)
219
+ entries.set(id, { range: range === null ? null : { ...range }, dynamic });
220
+ for (const [x, y, z, ids] of next.cells)
221
+ cells.set(packCell(x, y, z), { x, y, z, ids: new Set(ids) });
222
+ for (const id of next.oversized)
223
+ oversized.add(id);
224
+ },
209
225
  };
210
226
  }
211
227
  /** Precise AABB-vs-AABB test re-exported for callers refining a broad-phase result. */
@@ -0,0 +1,46 @@
1
+ import type { WorldSolid, WorldSolids } from "./worldSolids.js";
2
+ type Vec3Like = {
3
+ x: number;
4
+ y: number;
5
+ z: number;
6
+ };
7
+ /** Minimal editor document shape {@link resolveAuthoredSolids} walks; any `EditorDocument` satisfies it. */
8
+ export interface AuthoredSolidsDocumentLike {
9
+ markers: readonly {
10
+ id: string;
11
+ kind: string;
12
+ position: Vec3Like;
13
+ rotationY?: number;
14
+ meta?: Record<string, unknown>;
15
+ }[];
16
+ volumes: readonly {
17
+ id: string;
18
+ kind: string;
19
+ center: Vec3Like;
20
+ halfExtents?: Vec3Like;
21
+ radius?: number;
22
+ meta?: Record<string, unknown>;
23
+ }[];
24
+ paths: readonly {
25
+ id: string;
26
+ kind: string;
27
+ points: readonly Vec3Like[];
28
+ meta?: Record<string, unknown>;
29
+ }[];
30
+ }
31
+ /** Layer prefix `syncAuthoredSolids` owns in `ctx.world.solids`; one layer per document object. */
32
+ export declare const AUTHORED_SOLID_LAYER_PREFIX = "authored:";
33
+ /**
34
+ * Solids for every document object whose scene kind declares `solids`, keyed by object id. Objects
35
+ * whose kind has no `solids` hook, or whose hook returns none, are left out.
36
+ * @capability authored-solids collision for studio-authored world content such as city volumes
37
+ */
38
+ export declare function resolveAuthoredSolids(document: AuthoredSolidsDocumentLike, sampleHeight?: (x: number, z: number) => number): Map<string, readonly WorldSolid[]>;
39
+ /**
40
+ * Write a document's studio solids into `solids` (usually `ctx.world.solids`), one
41
+ * `authored:<objectId>` layer per object, dropping authored layers the document no longer has.
42
+ * Call again whenever the document changes.
43
+ * @capability authored-solids collision for studio-authored world content such as city volumes
44
+ */
45
+ export declare function syncAuthoredSolids(solids: WorldSolids, document: AuthoredSolidsDocumentLike, sampleHeight?: (x: number, z: number) => number): void;
46
+ export {};
@@ -0,0 +1,75 @@
1
+ import { registerBuiltinSceneKinds } from "../scene/builtinSceneKinds.js";
2
+ import { getSceneKind, parseParams } from "../scene/sceneKinds.js";
3
+ /** Layer prefix `syncAuthoredSolids` owns in `ctx.world.solids`; one layer per document object. */
4
+ export const AUTHORED_SOLID_LAYER_PREFIX = "authored:";
5
+ function kindObjects(document) {
6
+ const objects = [];
7
+ for (const marker of document.markers) {
8
+ objects.push({
9
+ id: marker.id,
10
+ kind: marker.kind,
11
+ position: marker.position,
12
+ ...(marker.rotationY === undefined ? {} : { rotationY: marker.rotationY }),
13
+ ...(marker.meta === undefined ? {} : { meta: marker.meta }),
14
+ });
15
+ }
16
+ for (const volume of document.volumes) {
17
+ objects.push({
18
+ id: volume.id,
19
+ kind: volume.kind,
20
+ center: volume.center,
21
+ ...(volume.halfExtents === undefined ? {} : { halfExtents: volume.halfExtents }),
22
+ ...(volume.radius === undefined ? {} : { radius: volume.radius }),
23
+ ...(volume.meta === undefined ? {} : { meta: volume.meta }),
24
+ });
25
+ }
26
+ for (const path of document.paths) {
27
+ objects.push({
28
+ id: path.id,
29
+ kind: path.kind,
30
+ points: path.points,
31
+ ...(path.meta === undefined ? {} : { meta: path.meta }),
32
+ });
33
+ }
34
+ return objects;
35
+ }
36
+ /**
37
+ * Solids for every document object whose scene kind declares `solids`, keyed by object id. Objects
38
+ * whose kind has no `solids` hook, or whose hook returns none, are left out.
39
+ * @capability authored-solids collision for studio-authored world content such as city volumes
40
+ */
41
+ export function resolveAuthoredSolids(document, sampleHeight) {
42
+ registerBuiltinSceneKinds();
43
+ const out = new Map();
44
+ const objects = kindObjects(document);
45
+ const context = { ...(sampleHeight === undefined ? {} : { sampleHeight }), objects };
46
+ for (const object of objects) {
47
+ const definition = getSceneKind(object.kind);
48
+ if (definition?.solids === undefined || definition.resolve === undefined)
49
+ continue;
50
+ const params = parseParams(definition.schema, object.meta);
51
+ const solids = definition.solids(definition.resolve(object, params, context), object, params, context);
52
+ if (solids.length > 0)
53
+ out.set(object.id, solids);
54
+ }
55
+ return out;
56
+ }
57
+ /**
58
+ * Write a document's studio solids into `solids` (usually `ctx.world.solids`), one
59
+ * `authored:<objectId>` layer per object, dropping authored layers the document no longer has.
60
+ * Call again whenever the document changes.
61
+ * @capability authored-solids collision for studio-authored world content such as city volumes
62
+ */
63
+ export function syncAuthoredSolids(solids, document, sampleHeight) {
64
+ const resolved = resolveAuthoredSolids(document, sampleHeight);
65
+ solids.batch(() => {
66
+ for (const layer of solids.layers()) {
67
+ if (!layer.startsWith(AUTHORED_SOLID_LAYER_PREFIX))
68
+ continue;
69
+ if (!resolved.has(layer.slice(AUTHORED_SOLID_LAYER_PREFIX.length)))
70
+ solids.remove(layer);
71
+ }
72
+ for (const [id, set] of resolved)
73
+ solids.set(`${AUTHORED_SOLID_LAYER_PREFIX}${id}`, set);
74
+ });
75
+ }
@@ -7,7 +7,7 @@
7
7
  * @capability building-generator seeded building as a placeable parametric asset
8
8
  */
9
9
  import { registerAssetGenerator, partsBounds } from "../scene/assetGenerator.js";
10
- import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, generateBuilding, resolveBuildingPalette } from "./buildings.js";
10
+ import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, buildingSurfaceColor, generateBuilding, resolveBuildingPalette } from "./buildings.js";
11
11
  /** The generator id a catalog entry / placed marker references. */
12
12
  export const BUILDING_GENERATOR_ID = "building";
13
13
  /** The building generator's slider schema — drives the inspector via #809. */
@@ -47,7 +47,7 @@ export function generateBuildingAsset(params, seed) {
47
47
  position: part.position,
48
48
  size: part.scale,
49
49
  rotationY: part.rotationY,
50
- color: palette[part.kind],
50
+ color: buildingSurfaceColor(palette[part.kind]),
51
51
  }));
52
52
  return { parts, bounds: partsBounds(parts) };
53
53
  }
@@ -3,7 +3,7 @@
3
3
  * as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
4
4
  * bindings, the renderer loads them.
5
5
  */
6
- import type { BuildingKitSlot, BuildingPartKind, BuildingVariantCounts, Vec3 } from "./buildings.js";
6
+ import type { BuildingKitSlot, BuildingPartKind, BuildingSurfaceMaterial, BuildingVariantCounts, Vec3 } from "./buildings.js";
7
7
  /**
8
8
  * How a kit model's native bounds are mapped onto the slot box `BuildingPartPlacement.scale`
9
9
  * describes.
@@ -32,6 +32,8 @@ export interface BuildingKitPart {
32
32
  /** Multiplier applied after `fit`. */
33
33
  scale?: Vec3;
34
34
  tint?: string;
35
+ /** Tiled PBR maps applied over the model's own materials; `color` tints, `repeat` sets the tiling. */
36
+ material?: BuildingSurfaceMaterial;
35
37
  /**
36
38
  * Quarter-turn the model when its long horizontal axis disagrees with the slot's, so a panel
37
39
  * authored running along Z still tiles a bay that runs along X. Default true — modular kits do not
@@ -103,11 +103,43 @@ export interface BuildingGridConfig {
103
103
  floorRange?: readonly [number, number];
104
104
  }
105
105
  export declare const DEFAULT_BUILDING_CONFIG: BuildingConfig;
106
- export type BuildingPalette = Record<BuildingPartKind, string>;
106
+ /** URLs of the PBR maps a building surface tiles; `buildMaterialCatalog(...).resolve(id)!.maps` fits directly. */
107
+ export interface BuildingSurfaceMaps {
108
+ color?: string;
109
+ normal?: string;
110
+ roughness?: string;
111
+ ao?: string;
112
+ metalness?: string;
113
+ }
114
+ /**
115
+ * A textured building surface: a base colour plus optional tiled PBR maps. With `maps`, `color`
116
+ * multiplies the albedo (leave it light to show the map as authored); without, it is the flat
117
+ * colour the part has always had.
118
+ */
119
+ export interface BuildingSurfaceMaterial {
120
+ /** Base colour; the style's colour for that part kind when unset. */
121
+ color?: string;
122
+ maps?: BuildingSurfaceMaps;
123
+ /**
124
+ * Texture tiles per part slot along [x, y]; a number tiles both. Slots within one building share
125
+ * a size, so one repeat per kind is uniform there; it drifts across buildings with a different
126
+ * `bayWidth`. Default 1.
127
+ */
128
+ repeat?: number | readonly [number, number];
129
+ roughness?: number;
130
+ metalness?: number;
131
+ }
132
+ /** What a building part kind is painted with: a hex colour, or a colour plus tiled PBR maps. */
133
+ export type BuildingSurface = string | BuildingSurfaceMaterial;
134
+ export type BuildingPalette = Record<BuildingPartKind, BuildingSurface>;
107
135
  export type BuildingPaletteOverrides = Partial<BuildingPalette>;
136
+ /** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
137
+ export declare function buildingSurfaceColor(surface: BuildingSurface | undefined, fallback?: string): string;
138
+ /** Normalises a surface to its material form so a renderer reads one shape. */
139
+ export declare function resolveBuildingSurface(surface: BuildingSurface): BuildingSurfaceMaterial;
108
140
  export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
109
141
  export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
110
- export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, BuildingPalette>;
142
+ export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>>;
111
143
  /**
112
144
  * Per-style facade-tone family: the wall colours a district spreads across its buildings so
113
145
  * neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
@@ -38,6 +38,16 @@ export const DEFAULT_BUILDING_CONFIG = {
38
38
  probabilities: DEFAULT_PROBABILITIES,
39
39
  variants: DEFAULT_VARIANTS,
40
40
  };
41
+ /** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
42
+ export function buildingSurfaceColor(surface, fallback = "#808080") {
43
+ if (surface === undefined)
44
+ return fallback;
45
+ return typeof surface === "string" ? surface : (surface.color ?? fallback);
46
+ }
47
+ /** Normalises a surface to its material form so a renderer reads one shape. */
48
+ export function resolveBuildingSurface(surface) {
49
+ return typeof surface === "string" ? { color: surface } : surface;
50
+ }
41
51
  export const DEFAULT_BUILDING_STYLE = "generic";
42
52
  export const BUILDING_STYLE_PALETTES = {
43
53
  generic: {
@@ -207,7 +217,7 @@ export function resolveBuildingWallTones(style = DEFAULT_BUILDING_STYLE) {
207
217
  const tones = BUILDING_STYLE_WALL_TONES[style];
208
218
  if (tones !== undefined && tones.length > 0)
209
219
  return tones;
210
- return [resolveBuildingPalette(style).wall];
220
+ return [buildingSurfaceColor(resolveBuildingPalette(style).wall)];
211
221
  }
212
222
  /** @internal */
213
223
  export function resolveBuildingPalette(style = DEFAULT_BUILDING_STYLE, overrides) {
@@ -2,6 +2,7 @@ import { type ParamSchema, type SceneKindObject, type SceneKindResolveContext, t
2
2
  import { type BuildingStyle } from "./buildings.js";
3
3
  import { type CityLotClass, type CityLotPiece, type CityTreeSpecies, type CityZoneBand, type CityZoneProfile } from "./cityContent.js";
4
4
  import { type CircuitCorner, type CircuitGate, type CircuitSealKind } from "./raceCircuit.js";
5
+ import type { WorldSolid } from "./worldSolids.js";
5
6
  import type { RoadPoint } from "./roads.js";
6
7
  import { type StreetFeatureKind } from "./streetGenerator.js";
7
8
  import { type CityBlockKind } from "./cityBlocks.js";
@@ -432,6 +433,12 @@ export interface ResolvedCity {
432
433
  /** The instanced street race through these streets, when `race` is on and a lap could be found. */
433
434
  race?: ResolvedRace;
434
435
  }
436
+ /**
437
+ * One solid per wall and roof piece of every lot, grounded the way the city renderer grounds it:
438
+ * grade at the highest lot corner, foundations down to half a metre under the lowest.
439
+ * @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
440
+ */
441
+ export declare function citySolids(city: ResolvedCity, sampleHeight?: (x: number, z: number) => number): WorldSolid[];
435
442
  /** Extended resolve context: sibling `cityzone` volumes that override the band/mix locally. */
436
443
  export interface CityResolveContext extends SceneKindResolveContext {
437
444
  zoneOverrides?: readonly SceneKindObject[];
@@ -164,6 +164,7 @@ export const CITY_SCHEMA = {
164
164
  options: [{ value: "auto" }, { value: "gravel" }],
165
165
  },
166
166
  { type: "bool", key: "fabric", label: "buildings & parcels", group: "layout", default: CITY_DEFAULTS.fabric },
167
+ { type: "bool", key: "solid", label: "solid buildings", group: "layout", default: true },
167
168
  { type: "bool", key: "bridges", label: "bridges over water", group: "layout", default: CITY_DEFAULTS.bridges },
168
169
  { type: "bool", key: "tunnels", label: "tunnels through ridges", group: "layout", default: CITY_DEFAULTS.tunnels },
169
170
  {
@@ -638,6 +639,55 @@ export function readCityRules(meta) {
638
639
  seed: params["seed"],
639
640
  };
640
641
  }
642
+ /** Lot pieces that block: walls and roofs. Trim and accents (cornices, awnings) stay passable. */
643
+ const SOLID_PIECE_ROLES = new Set(["wall", "roof"]);
644
+ /**
645
+ * One solid per wall and roof piece of every lot, grounded the way the city renderer grounds it:
646
+ * grade at the highest lot corner, foundations down to half a metre under the lowest.
647
+ * @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
648
+ */
649
+ export function citySolids(city, sampleHeight = () => 0) {
650
+ const solids = [];
651
+ for (const lot of city.lots) {
652
+ const c = Math.cos(lot.rotationY);
653
+ const s = Math.sin(lot.rotationY);
654
+ const hw = lot.size[0] / 2;
655
+ const hd = lot.size[1] / 2;
656
+ let minY = Infinity;
657
+ let maxY = -Infinity;
658
+ for (const [dx, dz] of [
659
+ [hw, hd],
660
+ [hw, -hd],
661
+ [-hw, hd],
662
+ [-hw, -hd],
663
+ ]) {
664
+ const y = sampleHeight(lot.center[0] + dx * c + dz * s, lot.center[1] - dx * s + dz * c);
665
+ minY = Math.min(minY, y);
666
+ maxY = Math.max(maxY, y);
667
+ }
668
+ const grade = maxY;
669
+ const foundationBase = minY - 0.5;
670
+ for (const piece of lot.pieces) {
671
+ if (!SOLID_PIECE_ROLES.has(piece.role))
672
+ continue;
673
+ const bottom = piece.grounded ? foundationBase : grade + piece.offset[1];
674
+ const top = piece.grounded ? grade + piece.size[1] : grade + piece.offset[1] + piece.size[1];
675
+ if (top - bottom <= 0 || piece.size[0] <= 0 || piece.size[2] <= 0)
676
+ continue;
677
+ const yaw = lot.rotationY + piece.rotationY;
678
+ solids.push({
679
+ center: [
680
+ lot.center[0] + piece.offset[0] * c + piece.offset[2] * s,
681
+ (bottom + top) / 2,
682
+ lot.center[1] - piece.offset[0] * s + piece.offset[2] * c,
683
+ ],
684
+ halfExtents: [piece.size[0] / 2, (top - bottom) / 2, piece.size[2] / 2],
685
+ ...(yaw === 0 ? {} : { rotationY: yaw }),
686
+ });
687
+ }
688
+ }
689
+ return solids;
690
+ }
641
691
  /** Bounded-work caps so a huge volume can never generate unbounded content. */
642
692
  const MAX_STREETS = 320;
643
693
  /** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
@@ -1302,7 +1352,7 @@ export function resolveCityObject(object, context) {
1302
1352
  return Math.hypot(dx, dz) / (2 * d);
1303
1353
  };
1304
1354
  // Child `cityzone` volumes override the band (and optionally the class mix) for lots inside them.
1305
- const overrides = (context?.zoneOverrides ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
1355
+ const overrides = (context?.zoneOverrides ?? context?.objects ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
1306
1356
  const overrideAt = overrides.length === 0
1307
1357
  ? null
1308
1358
  : (x, z) => {
@@ -1789,6 +1839,7 @@ export function registerCityKind() {
1789
1839
  schema: CITY_SCHEMA,
1790
1840
  coverage: { spec: "city", densityKey: "roadsideOccupancy" },
1791
1841
  resolve: (object, _params, context) => resolveCityObject(object, context),
1842
+ solids: (resolved, _object, params, context) => resolved === null || params["solid"] === false ? [] : citySolids(resolved, context.sampleHeight),
1792
1843
  note: (object) => {
1793
1844
  const resolved = resolveCityObject(object);
1794
1845
  if (resolved === null)
@@ -296,16 +296,38 @@ export interface OceanEnvironmentConfig {
296
296
  waveSpeed?: number;
297
297
  color?: string;
298
298
  }
299
+ /**
300
+ * Where the sun sits, as a compass bearing and a height above the horizon, both in degrees.
301
+ * `azimuth` 0 points toward -Z (north), 90 toward +X (east); `elevation` 90 is straight overhead.
302
+ */
303
+ export interface SkySunConfig {
304
+ /** Compass bearing of the sun in degrees; 0 = -Z, 90 = +X. Default 0. */
305
+ azimuth?: number;
306
+ /** Height above the horizon in degrees; 0 sits on the horizon, 90 is overhead. Default 68. */
307
+ elevation?: number;
308
+ }
299
309
  export interface SkyEnvironmentConfig {
300
310
  /** Fixed look used when `timeOfDay` is off (or no clock is available); default "day". */
301
311
  preset?: "day" | "dusk" | "night";
302
312
  /** Drive sun/sky from the world clock's `calendar().dayFraction` instead of the fixed `preset`. */
303
313
  timeOfDay?: boolean;
314
+ /**
315
+ * Horizon tint at noon. Under `timeOfDay` this only sets the noon keyframe; dawn, dusk, and night
316
+ * still crossfade through the engine presets, so an authored tint is visible for part of the day.
317
+ */
304
318
  horizonColor?: string;
319
+ /** Zenith tint at noon; same `timeOfDay` crossfade caveat as `horizonColor`. */
305
320
  zenithColor?: string;
306
321
  sunIntensity?: number;
307
322
  ambientIntensity?: number;
308
- /** Sky-dome sphere radius in world units. Enlarge for a playfield sited far from the origin so the viewer never exits the dome. Default 260. */
323
+ /**
324
+ * Sun placement for the dome glow and the sky-owned sun light. With a fixed `preset` this is the
325
+ * sun's exact direction; under `timeOfDay` it is where the sun stands at noon and the day arc
326
+ * swings around that bearing. Unset with an authored `lighting.directional` light, the shell takes
327
+ * the first directional light's direction so the dome sun agrees with the shadows.
328
+ */
329
+ sun?: SkySunConfig;
330
+ /** Sky-dome sphere radius in world units. Enlarge for a playfield sited far from the origin so the viewer never exits the dome. Must stay below the camera far plane or the dome depth-clips to black. Default 260. */
309
331
  radius?: number;
310
332
  /** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
311
333
  hazeStrength?: number;
@@ -377,6 +399,11 @@ export interface BuildingEnvironmentConfig {
377
399
  * untextured block; without a kit the whole facade stays blocks, as before.
378
400
  */
379
401
  kit?: BuildingKit;
402
+ /**
403
+ * Whether the buildings block movement and physics through `ctx.world.solids` (default `true`).
404
+ * `false` keeps them render-only, e.g. a distant skyline nobody can reach.
405
+ */
406
+ solid?: boolean;
380
407
  }
381
408
  export type PadSize = readonly [number, number] | {
382
409
  radius: number;
@@ -418,7 +445,7 @@ export type OceanEnvironmentDescriptor = {
418
445
  } & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position" | "levelAt">;
419
446
  export type BuildingEnvironmentDescriptor = {
420
447
  kind: "building";
421
- } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit">;
448
+ } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit" | "solid">;
422
449
  export type PadEnvironmentDescriptor = {
423
450
  kind: "pad";
424
451
  } & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY" | "elevation">;
@@ -1,3 +1,4 @@
1
+ import { warnOnce } from "../devtools/warnOnce.js";
1
2
  function list(value) {
2
3
  if (value === undefined) {
3
4
  return undefined;
@@ -110,6 +111,9 @@ export function terrain(config = {}) {
110
111
  });
111
112
  }
112
113
  export function sky(config = {}) {
114
+ if (config.timeOfDay === true && (config.zenithColor !== undefined || config.horizonColor !== undefined)) {
115
+ warnOnce("sky-timeofday-tint", "[jgengine] sky(): zenithColor/horizonColor only set the noon keyframe under timeOfDay; dawn, dusk, and night crossfade through the engine presets. Drop timeOfDay for a fixed tint.");
116
+ }
113
117
  return withOptional({
114
118
  kind: "sky",
115
119
  preset: config.preset ?? "day",
@@ -119,6 +123,7 @@ export function sky(config = {}) {
119
123
  ...(config.zenithColor === undefined ? {} : { zenithColor: config.zenithColor }),
120
124
  ...(config.sunIntensity === undefined ? {} : { sunIntensity: config.sunIntensity }),
121
125
  ...(config.ambientIntensity === undefined ? {} : { ambientIntensity: config.ambientIntensity }),
126
+ ...(config.sun === undefined ? {} : { sun: { ...config.sun } }),
122
127
  ...(config.radius === undefined ? {} : { radius: config.radius }),
123
128
  ...(config.hazeStrength === undefined ? {} : { hazeStrength: config.hazeStrength }),
124
129
  ...(config.sunGlowStrength === undefined ? {} : { sunGlowStrength: config.sunGlowStrength }),
@@ -253,6 +258,7 @@ export function building(config = {}) {
253
258
  ...(config.palette === undefined ? {} : { palette: config.palette }),
254
259
  ...(config.along === undefined ? {} : { along: config.along }),
255
260
  ...(config.kit === undefined ? {} : { kit: config.kit }),
261
+ ...(config.solid === undefined ? {} : { solid: config.solid }),
256
262
  });
257
263
  }
258
264
  export function pad(config) {
@@ -16,6 +16,10 @@ export interface FootprintReservation {
16
16
  kind: string;
17
17
  cells: readonly GridCell[];
18
18
  }
19
+ /** Plain JSON state of a {@link FootprintGrid}: live reservations in claim order. */
20
+ export interface FootprintGridState {
21
+ reservations: FootprintReservation[];
22
+ }
19
23
  /** Handle returned by {@link createFootprintGrid}. */
20
24
  export interface FootprintGrid {
21
25
  readonly cellSize: number;
@@ -30,6 +34,9 @@ export interface FootprintGrid {
30
34
  reservationOf(id: string): FootprintReservation | null;
31
35
  list(): readonly FootprintReservation[];
32
36
  clear(): void;
37
+ snapshot(): FootprintGridState;
38
+ /** Replaces every reservation with `next`'s and rebuilds cell occupancy from them. */
39
+ restore(next: FootprintGridState): void;
33
40
  }
34
41
  /**
35
42
  * Multi-cell footprint occupancy/reservation on a shared build grid — `world/placementController`