@jgengine/core 0.18.0 → 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 (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. package/skills/jgengine-world/visibility.md +112 -0
@@ -18,8 +18,10 @@
18
18
  * (`cityContent`) — setback tower tiers, gabled houses, gambrel barns, domed silos. Blocks left
19
19
  * unbuilt become plazas/greens/meadows by band, or crop-row farm fields; slivers and block
20
20
  * interiors classify as buffers and courtyards, never leftover rectangles. Street
21
- * furniture (lights, species-mixed trees, estate hedges, driveways, parking lots) resolves as
22
- * bounded, seeded data hooked to the network. Child `cityzone` volumes override the band/mix
21
+ * furniture (lights, species-mixed trees, estate hedges, driveways, parking lots, parked vehicles,
22
+ * junction signals and sign posts) resolves as bounded, seeded data hooked to the network. Turn `race` on and the district also hands back a
23
+ * {@link ResolvedRace}: a closed lap lifted out of its OWN streets with every side street leaving it
24
+ * sealed off — the Monaco / open-world street-race model, not a separate track. Child `cityzone` volumes override the band/mix
23
25
  * locally, so hand-placed intent wins over the profile. Named presets (Manhattan, Los Angeles,
24
26
  * Beverly Hills, rural Ohio…) ship as plain slider bundles on the schema. Pure config + resolver
25
27
  * here; the instanced renderer lives in the `shell`. Everything is deterministic per seed and
@@ -31,7 +33,8 @@ import { seededStreams } from "../random/rng.js";
31
33
  import { parseParams, registerSceneKind, } from "../scene/sceneKinds.js";
32
34
  import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_STYLE } from "./buildings.js";
33
35
  import { buildLotPieces, pickClass, pickSpecies, rollClassPlacement, zoneBand, zoneMetric, } from "./cityContent.js";
34
- import { furnitureSpots } from "./streets.js";
36
+ import { circuitKerbs, extractCircuitRoute, } from "./raceCircuit.js";
37
+ import { furnitureSpots, parkingSpots } from "./streets.js";
35
38
  import { CITY_BUILDING_BUDGET, budgetWarning, placedCoverage } from "./scatterCoverage.js";
36
39
  import { generateStreets, streetNetworkMode, } from "./streetGenerator.js";
37
40
  import { buildablePolygon, carveCorridors, conformPolygonToRing, cutParcel, extractBlocks, isSliverBlock, pointsInPolygon, RingWalker, } from "./cityBlocks.js";
@@ -51,6 +54,11 @@ export const CITY_DEFAULTS = {
51
54
  minCurveRadius: 24,
52
55
  minTurnAngle: 0,
53
56
  maxTurnAngle: 150,
57
+ outline: 0,
58
+ residentialBranches: 0,
59
+ elevation: 0,
60
+ maxGrade: 0.07,
61
+ compactness: 0,
54
62
  fabric: true,
55
63
  tunnels: true,
56
64
  blockSize: 48,
@@ -92,6 +100,8 @@ export const CITY_DEFAULTS = {
92
100
  { item: "conifer", weight: 1 },
93
101
  ],
94
102
  lightDensity: 0.5,
103
+ parkedCars: 0.55,
104
+ signals: 0.7,
95
105
  hedges: true,
96
106
  driveways: true,
97
107
  parking: true,
@@ -102,6 +112,14 @@ export const CITY_DEFAULTS = {
102
112
  bridgeStyle: "arch",
103
113
  sidewalks: true,
104
114
  sidewalkWidth: 1.9,
115
+ race: false,
116
+ raceLapLength: 1200,
117
+ raceArterial: 0.7,
118
+ raceCheckpoints: 12,
119
+ raceLaps: 3,
120
+ raceSeals: true,
121
+ raceGridSlots: 12,
122
+ raceCornerRadius: 9,
105
123
  style: DEFAULT_BUILDING_STYLE,
106
124
  buildingKit: "",
107
125
  seed: "",
@@ -110,6 +128,7 @@ export const CITY_DEFAULTS = {
110
128
  export const CITY_SCHEMA = {
111
129
  groups: [
112
130
  { id: "layout", label: "Layout" },
131
+ { id: "race", label: "Race" },
113
132
  { id: "placement", label: "Placement" },
114
133
  { id: "zoning", label: "Zoning" },
115
134
  { id: "buildings", label: "Buildings" },
@@ -125,6 +144,11 @@ export const CITY_SCHEMA = {
125
144
  { type: "range", key: "minCurveRadius", label: "min curve radius", group: "layout", min: 6, max: 120, step: 1, default: CITY_DEFAULTS.minCurveRadius, unit: "m" },
126
145
  { type: "range", key: "minTurnAngle", label: "min turn angle", group: "layout", min: 0, max: 60, step: 1, default: CITY_DEFAULTS.minTurnAngle, unit: "°" },
127
146
  { type: "range", key: "maxTurnAngle", label: "max turn angle", group: "layout", min: 30, max: 180, step: 1, default: CITY_DEFAULTS.maxTurnAngle, unit: "°" },
147
+ { type: "range", key: "outline", label: "outline", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.outline },
148
+ { type: "range", key: "residentialBranches", label: "residential branches", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.residentialBranches },
149
+ { type: "range", key: "elevation", label: "road elevation", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.elevation },
150
+ { type: "range", key: "maxGrade", label: "max grade", group: "layout", min: 0.01, max: 0.3, step: 0.005, default: CITY_DEFAULTS.maxGrade },
151
+ { type: "range", key: "compactness", label: "circuit compactness", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.compactness },
128
152
  { type: "range", key: "blockSize", label: "block size", group: "layout", min: 20, max: 140, step: 1, default: CITY_DEFAULTS.blockSize, unit: "m" },
129
153
  { type: "range", key: "blockAspect", label: "block aspect", group: "layout", min: 1, max: 3, step: 0.05, default: CITY_DEFAULTS.blockAspect },
130
154
  { type: "range", key: "openSpace", label: "open space", group: "layout", min: 0, max: 0.9, step: 0.01, default: CITY_DEFAULTS.openSpace },
@@ -140,6 +164,7 @@ export const CITY_SCHEMA = {
140
164
  options: [{ value: "auto" }, { value: "gravel" }],
141
165
  },
142
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 },
143
168
  { type: "bool", key: "bridges", label: "bridges over water", group: "layout", default: CITY_DEFAULTS.bridges },
144
169
  { type: "bool", key: "tunnels", label: "tunnels through ridges", group: "layout", default: CITY_DEFAULTS.tunnels },
145
170
  {
@@ -153,6 +178,14 @@ export const CITY_SCHEMA = {
153
178
  { type: "bool", key: "sidewalks", label: "sidewalks", group: "layout", default: CITY_DEFAULTS.sidewalks },
154
179
  { type: "range", key: "sidewalkWidth", label: "sidewalk width", group: "layout", min: 1, max: 4, step: 0.1, default: CITY_DEFAULTS.sidewalkWidth, unit: "m" },
155
180
  { type: "action", key: "layoutRandomize", label: "randomize layout", group: "layout", action: "randomize" },
181
+ { type: "bool", key: "race", label: "street race", group: "race", default: CITY_DEFAULTS.race },
182
+ { type: "range", key: "raceLapLength", label: "lap length", group: "race", min: 200, max: 6000, step: 50, default: CITY_DEFAULTS.raceLapLength, unit: "m" },
183
+ { type: "range", key: "raceArterial", label: "arterial bias", group: "race", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.raceArterial },
184
+ { type: "range", key: "raceCheckpoints", label: "checkpoints", group: "race", min: 2, max: 48, step: 1, default: CITY_DEFAULTS.raceCheckpoints },
185
+ { type: "range", key: "raceLaps", label: "laps", group: "race", min: 1, max: 20, step: 1, default: CITY_DEFAULTS.raceLaps },
186
+ { type: "bool", key: "raceSeals", label: "seal side streets", group: "race", default: CITY_DEFAULTS.raceSeals },
187
+ { type: "range", key: "raceGridSlots", label: "grid slots", group: "race", min: 0, max: 40, step: 1, default: CITY_DEFAULTS.raceGridSlots },
188
+ { type: "range", key: "raceCornerRadius", label: "corner radius", group: "race", min: 3, max: 40, step: 1, default: CITY_DEFAULTS.raceCornerRadius, unit: "m" },
156
189
  {
157
190
  type: "select",
158
191
  key: "profile",
@@ -189,6 +222,8 @@ export const CITY_SCHEMA = {
189
222
  { type: "range", key: "treeDensity", label: "tree density", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.treeDensity },
190
223
  { type: "weightedList", key: "treeMix", label: "tree mix", group: "greenery", itemLabel: "species", default: CITY_DEFAULTS.treeMix },
191
224
  { type: "range", key: "lightDensity", label: "street lights", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.lightDensity },
225
+ { type: "range", key: "parkedCars", label: "parked cars", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.parkedCars },
226
+ { type: "range", key: "signals", label: "traffic signals", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.signals },
192
227
  { type: "bool", key: "hedges", label: "estate hedges", group: "greenery", default: CITY_DEFAULTS.hedges },
193
228
  { type: "bool", key: "driveways", label: "driveways", group: "greenery", default: CITY_DEFAULTS.driveways },
194
229
  { type: "bool", key: "parking", label: "parking lots", group: "greenery", default: CITY_DEFAULTS.parking },
@@ -208,8 +243,8 @@ export const CITY_SCHEMA = {
208
243
  openSpace: 0.05,
209
244
  roadsideOccupancy: 0.97,
210
245
  blockDensity: 0.85,
211
- buildingRoadSetback: 0.8,
212
- buildingSpacing: 0.8,
246
+ buildingRoadSetback: 0.55,
247
+ buildingSpacing: 0.55,
213
248
  clusterStrength: 0.25,
214
249
  streetWidth: 9,
215
250
  boulevards: 0.5,
@@ -235,6 +270,8 @@ export const CITY_SCHEMA = {
235
270
  treeDensity: 0.3,
236
271
  treeMix: [{ item: "broadleaf", weight: 1 }],
237
272
  lightDensity: 0.8,
273
+ parkedCars: 0.95,
274
+ signals: 1,
238
275
  hedges: false,
239
276
  driveways: false,
240
277
  parking: false,
@@ -288,6 +325,8 @@ export const CITY_SCHEMA = {
288
325
  { item: "cypress", weight: 1 },
289
326
  ],
290
327
  lightDensity: 0.6,
328
+ parkedCars: 0.7,
329
+ signals: 0.8,
291
330
  hedges: false,
292
331
  driveways: true,
293
332
  parking: true,
@@ -339,6 +378,8 @@ export const CITY_SCHEMA = {
339
378
  { item: "broadleaf", weight: 2 },
340
379
  ],
341
380
  lightDensity: 0.35,
381
+ parkedCars: 0.2,
382
+ signals: 0.12,
342
383
  hedges: true,
343
384
  driveways: true,
344
385
  parking: false,
@@ -392,6 +433,8 @@ export const CITY_SCHEMA = {
392
433
  { item: "conifer", weight: 2 },
393
434
  ],
394
435
  lightDensity: 0.1,
436
+ parkedCars: 0.04,
437
+ signals: 0,
395
438
  hedges: false,
396
439
  driveways: true,
397
440
  parking: false,
@@ -413,6 +456,9 @@ export const CITY_SCHEMA = {
413
456
  minCurveRadius: 34,
414
457
  minTurnAngle: 0,
415
458
  maxTurnAngle: 95,
459
+ compactness: 0.5,
460
+ elevation: 0.3,
461
+ maxGrade: 0.06,
416
462
  blockSize: 95,
417
463
  blockAspect: 1,
418
464
  openSpace: 0,
@@ -426,6 +472,8 @@ export const CITY_SCHEMA = {
426
472
  sidewalks: false,
427
473
  treeDensity: 0.2,
428
474
  lightDensity: 0.25,
475
+ parkedCars: 0,
476
+ signals: 0,
429
477
  hedges: false,
430
478
  driveways: false,
431
479
  parking: false,
@@ -446,6 +494,9 @@ export const CITY_SCHEMA = {
446
494
  minCurveRadius: 12,
447
495
  minTurnAngle: 0,
448
496
  maxTurnAngle: 150,
497
+ outline: 0.5,
498
+ elevation: 0.8,
499
+ maxGrade: 0.14,
449
500
  blockSize: 72,
450
501
  blockAspect: 1,
451
502
  openSpace: 0,
@@ -459,6 +510,8 @@ export const CITY_SCHEMA = {
459
510
  sidewalks: false,
460
511
  treeDensity: 0.65,
461
512
  lightDensity: 0.05,
513
+ parkedCars: 0,
514
+ signals: 0,
462
515
  hedges: false,
463
516
  driveways: false,
464
517
  parking: false,
@@ -490,6 +543,8 @@ export const CITY_SCHEMA = {
490
543
  floorsMax: 34,
491
544
  treeDensity: 0.55,
492
545
  lightDensity: 0.6,
546
+ parkedCars: 0.85,
547
+ signals: 0.9,
493
548
  style: "generic",
494
549
  },
495
550
  },
@@ -527,6 +582,11 @@ export function readCityRules(meta) {
527
582
  minCurveRadius: params["minCurveRadius"],
528
583
  minTurnAngle: params["minTurnAngle"],
529
584
  maxTurnAngle: params["maxTurnAngle"],
585
+ outline: params["outline"],
586
+ residentialBranches: params["residentialBranches"],
587
+ elevation: params["elevation"],
588
+ maxGrade: params["maxGrade"],
589
+ compactness: params["compactness"],
530
590
  fabric: params["fabric"],
531
591
  tunnels: params["tunnels"],
532
592
  blockSize: params["blockSize"],
@@ -554,6 +614,8 @@ export function readCityRules(meta) {
554
614
  treeDensity: params["treeDensity"],
555
615
  treeMix: params["treeMix"],
556
616
  lightDensity: params["lightDensity"],
617
+ parkedCars: params["parkedCars"],
618
+ signals: params["signals"],
557
619
  hedges: params["hedges"],
558
620
  driveways: params["driveways"],
559
621
  parking: params["parking"],
@@ -564,11 +626,68 @@ export function readCityRules(meta) {
564
626
  bridgeStyle: params["bridgeStyle"],
565
627
  sidewalks: params["sidewalks"],
566
628
  sidewalkWidth: params["sidewalkWidth"],
629
+ race: params["race"],
630
+ raceLapLength: params["raceLapLength"],
631
+ raceArterial: params["raceArterial"],
632
+ raceCheckpoints: params["raceCheckpoints"],
633
+ raceLaps: params["raceLaps"],
634
+ raceSeals: params["raceSeals"],
635
+ raceGridSlots: params["raceGridSlots"],
636
+ raceCornerRadius: params["raceCornerRadius"],
567
637
  style: params["style"],
568
638
  buildingKit: params["buildingKit"] ?? CITY_DEFAULTS.buildingKit,
569
639
  seed: params["seed"],
570
640
  };
571
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
+ }
572
691
  /** Bounded-work caps so a huge volume can never generate unbounded content. */
573
692
  const MAX_STREETS = 320;
574
693
  /** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
@@ -578,7 +697,19 @@ const MAX_TREES = 3200;
578
697
  const MAX_LIGHTS = 1200;
579
698
  const MAX_FIELD_ROWS = 2400;
580
699
  const MAX_PARKING = 380;
700
+ const MAX_VEHICLES = 900;
701
+ const MAX_SIGNALS = 640;
702
+ const MAX_RACE_SEALS = 400;
703
+ const MAX_RACE_KERBS = 320;
581
704
  const TAU = Math.PI * 2;
705
+ /** Body footprint per silhouette as `[half width across, half length along the heading]`. */
706
+ const VEHICLE_HALF_EXTENTS = {
707
+ car: [0.95, 2.25],
708
+ van: [1.05, 2.65],
709
+ truck: [1.2, 3.5],
710
+ };
711
+ /** Paint slots the resolver spreads vehicles across; the renderer owns the actual colors. */
712
+ const VEHICLE_PAINT_COUNT = 8;
582
713
  function axisValue(value) {
583
714
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
584
715
  }
@@ -818,7 +949,8 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
818
949
  if (prevClass !== null && stickRoll < 0.45 + rules.clusterStrength * 0.4 && mix.some((entry) => entry.item === prevClass && entry.weight > 0)) {
819
950
  cls = prevClass;
820
951
  }
821
- const placement = rollClassPlacement(cls, lotRng, rules.lotScale, rules.floorsMin, rules.floorsMax, rules.buildingRoadSetback, rules.buildingSpacing);
952
+ const stretchCommercial = rules.blockDensity > 0.75 && (cls === "tower" || cls === "slab" || cls === "shop" || cls === "rowhouse");
953
+ const placement = rollClassPlacement(cls, lotRng, rules.lotScale, rules.floorsMin, rules.floorsMax, rules.buildingRoadSetback, rules.buildingSpacing, stretchCommercial ? 1 + rules.blockDensity * 0.35 : 1);
822
954
  const frontW = placement.width + placement.gap;
823
955
  if (cursor + frontW > walker.total - 0.5)
824
956
  break;
@@ -904,18 +1036,21 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
904
1036
  prevClass = null;
905
1037
  continue;
906
1038
  }
907
- const sideGap = Math.max(0.3, placement.gap / 2);
908
- const buildable = buildablePolygon(poly, A, B, placement.setback, sideGap, 1.2, usedDepth);
1039
+ const commercial = cls === "tower" || cls === "slab" || cls === "shop" || cls === "rowhouse";
1040
+ const dense = rules.blockDensity > 0.75 && commercial;
1041
+ const sideGap = Math.max(dense ? (cls === "shop" || cls === "rowhouse" ? 0.08 : 0.15) : 0.3, (placement.gap / 2) * (dense ? 0.45 : 1));
1042
+ const frontSetback = Math.max(0.05, placement.setback * (dense ? 0.35 : 1));
1043
+ const buildable = buildablePolygon(poly, A, B, frontSetback, sideGap, 1.2, usedDepth);
909
1044
  parcel.buildable = buildable;
910
1045
  let placedLot = false;
911
1046
  if (buildable.length >= 3) {
912
1047
  const outDir = [-midS.normal[0], -midS.normal[1]];
913
1048
  const rotationY = Math.atan2(outDir[0], outDir[1]);
914
- const maxW = Math.max(3, Math.min(placement.width, frontW - placement.gap));
915
- const availDepth = usedDepth - placement.setback - 1.2;
916
- const maxD = Math.max(3, Math.min(placement.depth, availDepth));
917
- const cx0 = midS.p[0] + midS.normal[0] * (placement.setback + maxD / 2);
918
- const cz0 = midS.p[1] + midS.normal[1] * (placement.setback + maxD / 2);
1049
+ const maxW = Math.max(3, Math.min(dense ? Math.max(placement.width, frontW * 0.9) : placement.width, frontW - Math.max(0.15, placement.gap * (dense ? 0.35 : 1))));
1050
+ const availDepth = usedDepth - frontSetback - 1.2;
1051
+ const maxD = Math.max(3, Math.min(dense ? Math.max(placement.depth, availDepth * 0.85) : placement.depth, availDepth));
1052
+ const cx0 = midS.p[0] + midS.normal[0] * (frontSetback + maxD / 2);
1053
+ const cz0 = midS.p[1] + midS.normal[1] * (frontSetback + maxD / 2);
919
1054
  const fit = fitRectInPolygon(buildable, cx0, cz0, maxW, maxD, rotationY, 0.45);
920
1055
  if (fit !== null && fit.w >= 3 && fit.d >= 2.6) {
921
1056
  const slopeRoll = rng();
@@ -1022,8 +1157,8 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
1022
1157
  // elsewhere) when it is big enough to read as a place rather than back yards.
1023
1158
  if (maxDepthUsed > 0) {
1024
1159
  const core = insetRingUniform(land, maxDepthUsed + 0.5);
1025
- if (core.length >= 3 && polygonArea(core) > 240) {
1026
- const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.5 ? "courtyard" : "green") : "meadow";
1160
+ if (core.length >= 3 && polygonArea(core) > 120) {
1161
+ const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.55 ? "courtyard" : "green") : "meadow";
1027
1162
  result.parks.push(parkFromRing(`court:${bi}`, type, core, parkJitter));
1028
1163
  }
1029
1164
  }
@@ -1071,6 +1206,89 @@ function buildFieldRows(parks, placed, streams) {
1071
1206
  park.rows = rows;
1072
1207
  }
1073
1208
  }
1209
+ /** Ids of the through-streets carrying a lap's graph edges, in lap order and without repeats. */
1210
+ function lapStreetIds(network, edges, streetCount) {
1211
+ const pairKey = (a, b) => (a < b ? `${a}:${b}` : `${b}:${a}`);
1212
+ const owner = new Map();
1213
+ for (let index = 0; index < Math.min(network.streets.length, streetCount); index += 1) {
1214
+ const nodes = network.streets[index].nodes;
1215
+ for (let i = 0; i + 1 < nodes.length; i += 1)
1216
+ owner.set(pairKey(nodes[i], nodes[i + 1]), index);
1217
+ }
1218
+ const byId = new Map(network.edges.map((edge) => [edge.id, edge]));
1219
+ const ids = [];
1220
+ const seen = new Set();
1221
+ for (const id of edges) {
1222
+ const edge = byId.get(id);
1223
+ if (edge === undefined)
1224
+ continue;
1225
+ const index = owner.get(pairKey(edge.a, edge.b));
1226
+ if (index === undefined || seen.has(index))
1227
+ continue;
1228
+ seen.add(index);
1229
+ ids.push(`street:${index}`);
1230
+ }
1231
+ return ids;
1232
+ }
1233
+ /** Rotate and translate a volume-local circuit route into the world frame the rest of the plan uses. */
1234
+ function raceToWorld(route, streets, seals, toWorld, rotationY) {
1235
+ // `toWorld` rotates a local direction BY `rotationY`, so a heading turns with it rather than against.
1236
+ const heading = (local) => local + rotationY;
1237
+ // A checkpoint is an AABB with no yaw of its own; grow it to the bounds of the rotated gate.
1238
+ const gateX = Math.abs(Math.cos(rotationY));
1239
+ const gateZ = Math.abs(Math.sin(rotationY));
1240
+ const [sx, sz] = toWorld(route.start.x, route.start.z);
1241
+ return {
1242
+ centerline: route.centerline.map(([x, z]) => toWorld(x, z)),
1243
+ widths: [...route.widths],
1244
+ ...(route.heights === undefined ? {} : { heights: [...route.heights] }),
1245
+ length: route.length,
1246
+ streets,
1247
+ seals: seals
1248
+ ? route.seals.slice(0, MAX_RACE_SEALS).map((seal) => {
1249
+ const [x, z] = toWorld(seal.x, seal.z);
1250
+ return { x, z, heading: heading(seal.heading), width: seal.width, kind: seal.kind };
1251
+ })
1252
+ : [],
1253
+ corners: route.corners.map((corner) => ({ ...corner })),
1254
+ kerbs: circuitKerbs(route)
1255
+ .slice(0, MAX_RACE_KERBS)
1256
+ .map((kerb) => ({
1257
+ points: kerb.points.map(([x, z]) => toWorld(x, z)),
1258
+ width: kerb.width,
1259
+ side: kerb.side,
1260
+ stripe: kerb.stripe,
1261
+ corner: kerb.corner,
1262
+ })),
1263
+ checkpoints: route.checkpoints.map((gate) => {
1264
+ const [x, z] = toWorld(gate.center[0], gate.center[2]);
1265
+ return {
1266
+ id: gate.id,
1267
+ center: [x, gate.center[1], z],
1268
+ half: [
1269
+ gate.half[0] * gateX + gate.half[2] * gateZ,
1270
+ gate.half[1],
1271
+ gate.half[0] * gateZ + gate.half[2] * gateX,
1272
+ ],
1273
+ };
1274
+ }),
1275
+ laps: route.laps,
1276
+ start: {
1277
+ x: sx,
1278
+ z: sz,
1279
+ heading: heading(route.start.heading),
1280
+ line: [toWorld(route.start.line[0][0], route.start.line[0][1]), toWorld(route.start.line[1][0], route.start.line[1][1])],
1281
+ distance: route.start.distance,
1282
+ width: route.start.width,
1283
+ },
1284
+ grid: route.grid.map((slot) => {
1285
+ const [x, z] = toWorld(slot.x, slot.z);
1286
+ return { x, z, heading: heading(slot.heading) };
1287
+ }),
1288
+ sectors: [...route.sectors],
1289
+ tightestRadius: route.tightestRadius,
1290
+ };
1291
+ }
1074
1292
  /**
1075
1293
  * Synthesize the deterministic city plan for one `city` volume: streets → bridges → parks → zoned
1076
1294
  * frontage lots with massing pieces → furniture, all in the volume's local frame and then
@@ -1109,6 +1327,8 @@ export function resolveCityObject(object, context) {
1109
1327
  hedges: [],
1110
1328
  driveways: [],
1111
1329
  parkingLots: [],
1330
+ vehicles: [],
1331
+ signals: [],
1112
1332
  };
1113
1333
  if (hx < rules.blockSize * 0.75 || hz < rules.blockSize * 0.75)
1114
1334
  return empty;
@@ -1132,7 +1352,7 @@ export function resolveCityObject(object, context) {
1132
1352
  return Math.hypot(dx, dz) / (2 * d);
1133
1353
  };
1134
1354
  // Child `cityzone` volumes override the band (and optionally the class mix) for lots inside them.
1135
- 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);
1136
1356
  const overrideAt = overrides.length === 0
1137
1357
  ? null
1138
1358
  : (x, z) => {
@@ -1163,10 +1383,12 @@ export function resolveCityObject(object, context) {
1163
1383
  // net and a closed race circuit are the SAME generator at opposite slider extremes.
1164
1384
  const netRules = {
1165
1385
  seed: rules.seed.length > 0 ? rules.seed : object.id,
1386
+ outline: rules.outline,
1166
1387
  gridness: rules.gridness,
1167
1388
  loopiness: rules.loopiness,
1168
1389
  connectivity: rules.connectivity,
1169
1390
  branching: rules.branching,
1391
+ residentialBranches: rules.residentialBranches,
1170
1392
  deadEnds: rules.deadEnds,
1171
1393
  segmentLength: rules.blockSize,
1172
1394
  aspect: rules.blockAspect,
@@ -1176,6 +1398,10 @@ export function resolveCityObject(object, context) {
1176
1398
  maxTurnAngle: rules.maxTurnAngle,
1177
1399
  width: rules.streetWidth,
1178
1400
  boulevards: rules.boulevards,
1401
+ sidewalkWidth: rules.sidewalkWidth,
1402
+ elevation: rules.elevation,
1403
+ maxGrade: rules.maxGrade,
1404
+ compactness: rules.compactness,
1179
1405
  };
1180
1406
  const network = generateStreets(netRules, hx, hz, {
1181
1407
  heightAt: heightAt ?? undefined,
@@ -1193,6 +1419,7 @@ export function resolveCityObject(object, context) {
1193
1419
  level: street.level,
1194
1420
  surface,
1195
1421
  sidewalk: rules.sidewalks && surface === "asphalt",
1422
+ ...(street.heights === undefined ? {} : { heights: [...street.heights] }),
1196
1423
  ...(street.bulb === undefined ? {} : { bulb: [street.bulb[0], street.bulb[1]] }),
1197
1424
  ...(street.features.length === 0 ? {} : { features: street.features }),
1198
1425
  };
@@ -1212,6 +1439,19 @@ export function resolveCityObject(object, context) {
1212
1439
  width: t.width,
1213
1440
  bankHeight: t.bankHeight,
1214
1441
  }));
1442
+ // The race is a SECTION OF THIS CITY blocked off: the same streets the district already grew,
1443
+ // with a closed lap lifted out of them. A district with no drivable cycle simply has no race.
1444
+ const route = rules.race
1445
+ ? extractCircuitRoute(network, {
1446
+ seed: netRules.seed,
1447
+ lapLength: rules.raceLapLength,
1448
+ arterialBias: rules.raceArterial,
1449
+ minCornerRadius: rules.raceCornerRadius,
1450
+ checkpoints: rules.raceCheckpoints,
1451
+ laps: rules.raceLaps,
1452
+ gridSlots: rules.raceGridSlots,
1453
+ })
1454
+ : null;
1215
1455
  // Fabric (block/parcel/building subdivision) is optional: a bare road or race track leaves it off,
1216
1456
  // so a circuit never carries forced-on plots — parcels and buildings appear only when you want it.
1217
1457
  const emptyFabric = { blocks: [], parcels: [], parks: [], lots: [], placed: new LotIndex(), hedges: [], driveways: [] };
@@ -1372,6 +1612,99 @@ export function resolveCityObject(object, context) {
1372
1612
  parking.push({ center: [x, z], size: [lot.size[0] * 1.05, depth], rotationY: lot.rotationY });
1373
1613
  }
1374
1614
  }
1615
+ // Parked vehicles: curbside anchors come from the shared street helper, pad stalls from the
1616
+ // pads laid above. Every candidate clears other streets, footprints, and the cars already parked.
1617
+ const vehicles = [];
1618
+ if (rules.parkedCars > 0) {
1619
+ const carRng = streams("vehicles");
1620
+ const carIndex = new LotIndex();
1621
+ const park = (x, z, heading, mix, stall, street) => {
1622
+ const kind = mix < 0.68 ? "car" : mix < 0.9 ? "van" : "truck";
1623
+ const half = VEHICLE_HALF_EXTENTS[kind];
1624
+ const rect = { x, z, hw: half[0], hd: half[1], angle: heading };
1625
+ if (!insideBounds(x, z))
1626
+ return;
1627
+ if (index.clearance(x, z, stall === "curb" ? 0.6 : 0.5, street) < 0)
1628
+ return;
1629
+ if (lotResult.placed.overlapsAny(rect, 0.4))
1630
+ return;
1631
+ if (carIndex.overlapsAny(rect, 0.35))
1632
+ return;
1633
+ carIndex.add(rect);
1634
+ vehicles.push({ x, z, rotationY: heading, kind, paint: Math.floor(mix * 9973) % VEHICLE_PAINT_COUNT, stall });
1635
+ };
1636
+ const spacing = Math.max(7, 26 - 19 * rules.parkedCars);
1637
+ for (let si = 0; si < local.length && vehicles.length < MAX_VEHICLES; si += 1) {
1638
+ const street = local[si];
1639
+ // Gravel shoulders and narrow lanes have no curb lane: `parkingSpots` would seat the body
1640
+ // over the centreline of anything under ~6.5 m.
1641
+ if (street.surface === "gravel" || street.width < 6.5)
1642
+ continue;
1643
+ for (const spot of parkingSpots(asDescriptor(street), { spacing })) {
1644
+ if (vehicles.length >= MAX_VEHICLES)
1645
+ break;
1646
+ const mix = carRng();
1647
+ if (carRng() > rules.parkedCars)
1648
+ continue;
1649
+ const [x, z] = spot.position;
1650
+ if (nearIntersection(x, z, 4))
1651
+ continue;
1652
+ park(x, z, spot.heading, mix, "curb", si);
1653
+ }
1654
+ }
1655
+ for (const pad of parking) {
1656
+ if (vehicles.length >= MAX_VEHICLES)
1657
+ break;
1658
+ const ca = Math.cos(pad.rotationY);
1659
+ const sa = Math.sin(pad.rotationY);
1660
+ const rows = pad.size[1] >= 11 ? 2 : 1;
1661
+ const stalls = Math.max(1, Math.floor(pad.size[0] / 2.9));
1662
+ for (let row = 0; row < rows; row += 1) {
1663
+ const lz = rows === 1 ? 0 : (row === 0 ? -1 : 1) * pad.size[1] * 0.25;
1664
+ for (let s = 0; s < stalls; s += 1) {
1665
+ if (vehicles.length >= MAX_VEHICLES)
1666
+ break;
1667
+ const mix = carRng();
1668
+ if (carRng() > rules.parkedCars * 0.92)
1669
+ continue;
1670
+ const lx = ((s + 0.5) / stalls - 0.5) * pad.size[0];
1671
+ // Facing rows nose in on the aisle between them.
1672
+ park(pad.center[0] + lx * ca + lz * sa, pad.center[1] - lx * sa + lz * ca, pad.rotationY + row * Math.PI, mix, "lot", -1);
1673
+ }
1674
+ }
1675
+ }
1676
+ }
1677
+ // Junction control, one device per approach arm: arterial crossings get a signal on a mast arm,
1678
+ // everything above lane level gets a sign post instead.
1679
+ const signals = [];
1680
+ if (rules.signals > 0) {
1681
+ const signalRng = streams("signals");
1682
+ for (const cross of intersections) {
1683
+ if (signals.length >= MAX_SIGNALS)
1684
+ break;
1685
+ if (cross.arms.length < 3 || cross.level === "lane")
1686
+ continue;
1687
+ const arterial = LEVEL_RANK[cross.level] >= LEVEL_RANK.avenue;
1688
+ if (signalRng() > rules.signals * (arterial ? 1 : 0.75))
1689
+ continue;
1690
+ for (const arm of cross.arms) {
1691
+ if (signals.length >= MAX_SIGNALS)
1692
+ break;
1693
+ const dir = [Math.sin(arm.angle), Math.cos(arm.angle)];
1694
+ // Right-hand traffic: the pole stands on the near-right corner of the approach.
1695
+ const perp = [-dir[1], dir[0]];
1696
+ const out = cross.radius + 1.8;
1697
+ const side = arm.width / 2 + 1;
1698
+ const x = cross.x + dir[0] * out + perp[0] * side;
1699
+ const z = cross.z + dir[1] * out + perp[1] * side;
1700
+ if (!insideBounds(x, z))
1701
+ continue;
1702
+ if (index.clearance(x, z, 0.3) < 0)
1703
+ continue;
1704
+ signals.push({ x, z, heading: arm.angle, kind: arterial ? "signal" : "sign", reach: arm.width / 2 + 0.6 });
1705
+ }
1706
+ }
1707
+ }
1375
1708
  return {
1376
1709
  center: [center.x, center.y, center.z],
1377
1710
  size: [hx * 2, hz * 2],
@@ -1384,6 +1717,7 @@ export function resolveCityObject(object, context) {
1384
1717
  level: street.level,
1385
1718
  surface: street.surface,
1386
1719
  sidewalk: street.sidewalk,
1720
+ ...(street.heights === undefined ? {} : { heights: [...street.heights] }),
1387
1721
  ...(street.bulb === undefined ? {} : { bulb: toWorld(street.bulb[0], street.bulb[1]) }),
1388
1722
  ...(street.features === undefined ? {} : { features: street.features.map((f) => ({ ...f })) }),
1389
1723
  })),
@@ -1481,6 +1815,17 @@ export function resolveCityObject(object, context) {
1481
1815
  const [x, z] = toWorld(pad.center[0], pad.center[1]);
1482
1816
  return { center: [x, z], size: pad.size, rotationY: pad.rotationY - rotationY };
1483
1817
  }),
1818
+ vehicles: vehicles.map((vehicle) => {
1819
+ const [x, z] = toWorld(vehicle.x, vehicle.z);
1820
+ return { x, z, rotationY: vehicle.rotationY + rotationY, kind: vehicle.kind, paint: vehicle.paint, stall: vehicle.stall };
1821
+ }),
1822
+ signals: signals.map((signal) => {
1823
+ const [x, z] = toWorld(signal.x, signal.z);
1824
+ return { x, z, heading: signal.heading + rotationY, kind: signal.kind, reach: signal.reach };
1825
+ }),
1826
+ ...(route === null
1827
+ ? {}
1828
+ : { race: raceToWorld(route, lapStreetIds(network, route.edges, local.length), rules.raceSeals, toWorld, rotationY) }),
1484
1829
  };
1485
1830
  }
1486
1831
  /** Registers the `city` + `cityzone` scene kinds (schema + resolver). Called by {@link registerBuiltinSceneKinds}. @internal */
@@ -1494,6 +1839,7 @@ export function registerCityKind() {
1494
1839
  schema: CITY_SCHEMA,
1495
1840
  coverage: { spec: "city", densityKey: "roadsideOccupancy" },
1496
1841
  resolve: (object, _params, context) => resolveCityObject(object, context),
1842
+ solids: (resolved, _object, params, context) => resolved === null || params["solid"] === false ? [] : citySolids(resolved, context.sampleHeight),
1497
1843
  note: (object) => {
1498
1844
  const resolved = resolveCityObject(object);
1499
1845
  if (resolved === null)
@@ -1519,6 +1865,11 @@ export function registerCityKind() {
1519
1865
  const feats = [
1520
1866
  resolved.bridges.length > 0 ? `${resolved.bridges.length} bridges` : "",
1521
1867
  resolved.tunnels.length > 0 ? `${resolved.tunnels.length} tunnels` : "",
1868
+ resolved.rules.race
1869
+ ? resolved.race === undefined
1870
+ ? "no raceable lap in these streets"
1871
+ : `${Math.round(resolved.race.length)} m lap · ${resolved.race.seals.length} seals`
1872
+ : "",
1522
1873
  ].filter((s) => s.length > 0);
1523
1874
  const head = mode === "circuit" ? `circuit · ${resolved.streets.length} streets` : `${resolved.streets.length} streets`;
1524
1875
  // Shared clamp-and-warn: when the block/parcel pipeline hits the building budget, surface it in