@jgengine/core 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (727) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/ai/crowd.d.ts +55 -0
  5. package/dist/ai/crowd.js +114 -0
  6. package/dist/ai/flock.d.ts +5 -2
  7. package/dist/ai/flock.js +5 -2
  8. package/dist/ai/groupAssist.d.ts +1 -0
  9. package/dist/ai/groupAssist.js +1 -0
  10. package/dist/ai/heatSystem.d.ts +76 -0
  11. package/dist/ai/heatSystem.js +97 -0
  12. package/dist/ai/interestScheduler.d.ts +107 -0
  13. package/dist/ai/interestScheduler.js +118 -0
  14. package/dist/ai/jobBoard.d.ts +1 -0
  15. package/dist/ai/jobBoard.js +1 -0
  16. package/dist/ai/laneSelect.d.ts +5 -2
  17. package/dist/ai/laneSelect.js +5 -2
  18. package/dist/ai/mobBrain.d.ts +1 -0
  19. package/dist/ai/mobBrain.js +1 -0
  20. package/dist/ai/populationDirector.d.ts +103 -0
  21. package/dist/ai/populationDirector.js +190 -0
  22. package/dist/ai/pursuit.d.ts +56 -0
  23. package/dist/ai/pursuit.js +53 -0
  24. package/dist/ai/spawnDirector.d.ts +7 -2
  25. package/dist/ai/spawnDirector.js +18 -15
  26. package/dist/ai/spawnPoint.d.ts +3 -1
  27. package/dist/ai/spawnPoint.js +3 -1
  28. package/dist/ai/targetAcquisition.d.ts +84 -0
  29. package/dist/ai/targetAcquisition.js +92 -0
  30. package/dist/anim/easing.d.ts +19 -0
  31. package/dist/anim/easing.js +19 -0
  32. package/dist/anim/oscillator.d.ts +12 -0
  33. package/dist/anim/oscillator.js +25 -0
  34. package/dist/area/areaEffectField.d.ts +127 -0
  35. package/dist/area/areaEffectField.js +175 -0
  36. package/dist/area/stackPolicy.d.ts +47 -0
  37. package/dist/area/stackPolicy.js +99 -0
  38. package/dist/authoring.d.ts +2 -1
  39. package/dist/authoring.js +2 -1
  40. package/dist/board/laneBoard.d.ts +13 -0
  41. package/dist/board/laneBoard.js +13 -0
  42. package/dist/board/timelineBoard.d.ts +10 -0
  43. package/dist/board/timelineBoard.js +10 -0
  44. package/dist/combat/abilityKit.d.ts +18 -0
  45. package/dist/combat/abilityKit.js +24 -9
  46. package/dist/combat/animationState.d.ts +5 -0
  47. package/dist/combat/animationState.js +5 -0
  48. package/dist/combat/breakMeters.d.ts +10 -0
  49. package/dist/combat/breakMeters.js +10 -0
  50. package/dist/combat/castRunner.d.ts +5 -0
  51. package/dist/combat/castRunner.js +5 -0
  52. package/dist/combat/comboPoints.d.ts +5 -0
  53. package/dist/combat/comboPoints.js +5 -0
  54. package/dist/combat/comboString.d.ts +5 -0
  55. package/dist/combat/comboString.js +5 -0
  56. package/dist/combat/damageInterceptors.d.ts +192 -0
  57. package/dist/combat/damageInterceptors.js +251 -0
  58. package/dist/combat/damageMatchup.d.ts +68 -0
  59. package/dist/combat/damageMatchup.js +42 -0
  60. package/dist/combat/damageResolution.d.ts +63 -0
  61. package/dist/combat/damageResolution.js +50 -0
  62. package/dist/combat/death.d.ts +7 -1
  63. package/dist/combat/death.js +5 -0
  64. package/dist/combat/deathReason.d.ts +14 -0
  65. package/dist/combat/deathReason.js +1 -0
  66. package/dist/combat/defensiveWindow.d.ts +5 -0
  67. package/dist/combat/defensiveWindow.js +5 -0
  68. package/dist/combat/dotField.d.ts +5 -1
  69. package/dist/combat/dotField.js +5 -1
  70. package/dist/combat/downed.d.ts +5 -0
  71. package/dist/combat/downed.js +5 -0
  72. package/dist/combat/effects.d.ts +5 -0
  73. package/dist/combat/effects.js +5 -0
  74. package/dist/combat/encounterSequence.d.ts +258 -0
  75. package/dist/combat/encounterSequence.js +426 -0
  76. package/dist/combat/hitReaction.d.ts +57 -1
  77. package/dist/combat/hitReaction.js +45 -5
  78. package/dist/combat/magazine.d.ts +54 -0
  79. package/dist/combat/magazine.js +96 -0
  80. package/dist/combat/projectiles.d.ts +5 -0
  81. package/dist/combat/projectiles.js +32 -14
  82. package/dist/combat/receivedDamage.d.ts +117 -0
  83. package/dist/combat/receivedDamage.js +92 -0
  84. package/dist/combat/regenShield.d.ts +5 -1
  85. package/dist/combat/regenShield.js +5 -1
  86. package/dist/combat/renderCues.d.ts +51 -0
  87. package/dist/combat/renderCues.js +44 -0
  88. package/dist/combat/resourcePool.d.ts +5 -0
  89. package/dist/combat/resourcePool.js +5 -0
  90. package/dist/combat/rewardAllocation.d.ts +121 -0
  91. package/dist/combat/rewardAllocation.js +251 -0
  92. package/dist/combat/shotOrigin.d.ts +18 -1
  93. package/dist/combat/shotOrigin.js +39 -0
  94. package/dist/combat/statusApplication.d.ts +96 -0
  95. package/dist/combat/statusApplication.js +81 -0
  96. package/dist/combat/telegraph.d.ts +11 -2
  97. package/dist/combat/telegraph.js +11 -2
  98. package/dist/combat/thresholdCrossings.d.ts +85 -0
  99. package/dist/combat/thresholdCrossings.js +116 -0
  100. package/dist/combat.d.ts +35 -0
  101. package/dist/combat.js +35 -0
  102. package/dist/commands/commandRegistry.d.ts +11 -0
  103. package/dist/commands/commandRegistry.js +9 -2
  104. package/dist/crafting/crop.d.ts +18 -0
  105. package/dist/crafting/crop.js +18 -0
  106. package/dist/crafting/production.d.ts +5 -0
  107. package/dist/crafting/production.js +5 -0
  108. package/dist/devtools/controls.d.ts +38 -0
  109. package/dist/devtools/controls.js +181 -0
  110. package/dist/devtools/devtools.d.ts +5 -197
  111. package/dist/devtools/devtools.js +38 -774
  112. package/dist/devtools/discover.d.ts +32 -0
  113. package/dist/devtools/discover.js +240 -0
  114. package/dist/devtools/fallbackSeams.d.ts +50 -0
  115. package/dist/devtools/fallbackSeams.js +111 -0
  116. package/dist/devtools/frame.d.ts +15 -0
  117. package/dist/devtools/frame.js +247 -0
  118. package/dist/devtools/logs.d.ts +15 -0
  119. package/dist/devtools/logs.js +86 -0
  120. package/dist/devtools/overrides.d.ts +16 -0
  121. package/dist/devtools/overrides.js +66 -0
  122. package/dist/devtools/rewriteTunables.d.ts +17 -0
  123. package/dist/devtools/rewriteTunables.js +276 -0
  124. package/dist/devtools/saveEndpoint.d.ts +46 -0
  125. package/dist/devtools/saveEndpoint.js +48 -0
  126. package/dist/devtools/transformTunables.d.ts +3 -0
  127. package/dist/devtools/transformTunables.js +3 -0
  128. package/dist/devtools/tunableSchema.d.ts +31 -0
  129. package/dist/devtools/tunableSchema.js +31 -0
  130. package/dist/devtools/types.d.ts +212 -0
  131. package/dist/devtools/types.js +1 -0
  132. package/dist/economy/auctionBook.d.ts +122 -0
  133. package/dist/economy/auctionBook.js +213 -0
  134. package/dist/economy/currency.d.ts +12 -1
  135. package/dist/economy/currency.js +12 -1
  136. package/dist/economy/listingBook.d.ts +91 -0
  137. package/dist/economy/listingBook.js +144 -0
  138. package/dist/economy/priceHistory.d.ts +44 -0
  139. package/dist/economy/priceHistory.js +89 -0
  140. package/dist/economy/resourceLedger.d.ts +222 -0
  141. package/dist/economy/resourceLedger.js +353 -0
  142. package/dist/economy/sharedWallet.d.ts +14 -0
  143. package/dist/economy/sharedWallet.js +14 -0
  144. package/dist/economy/techTree.d.ts +12 -0
  145. package/dist/economy/techTree.js +12 -0
  146. package/dist/economy/unlockPoints.d.ts +92 -0
  147. package/dist/economy/unlockPoints.js +103 -0
  148. package/dist/economy/wallet.d.ts +26 -2
  149. package/dist/economy/wallet.js +35 -6
  150. package/dist/editor/commandApply.d.ts +5 -0
  151. package/dist/editor/commandApply.js +755 -0
  152. package/dist/editor/commands.d.ts +235 -7
  153. package/dist/editor/commands.js +82 -124
  154. package/dist/editor/directives.d.ts +94 -0
  155. package/dist/editor/directives.js +206 -0
  156. package/dist/editor/document.d.ts +194 -13
  157. package/dist/editor/document.js +1166 -19
  158. package/dist/editor/entityCatalog.d.ts +42 -0
  159. package/dist/editor/entityCatalog.js +101 -0
  160. package/dist/editor/environment.d.ts +74 -0
  161. package/dist/editor/environment.js +169 -0
  162. package/dist/editor/grid.d.ts +229 -0
  163. package/dist/editor/grid.js +366 -0
  164. package/dist/editor/gridAdapters.d.ts +80 -0
  165. package/dist/editor/gridAdapters.js +134 -0
  166. package/dist/editor/index.d.ts +98 -6
  167. package/dist/editor/index.js +13 -2
  168. package/dist/editor/liveSync.d.ts +127 -0
  169. package/dist/editor/liveSync.js +249 -0
  170. package/dist/editor/minimap.d.ts +31 -0
  171. package/dist/editor/minimap.js +73 -0
  172. package/dist/editor/runtimeInspector.d.ts +88 -0
  173. package/dist/editor/runtimeInspector.js +239 -0
  174. package/dist/editor/streamer.d.ts +66 -0
  175. package/dist/editor/streamer.js +68 -0
  176. package/dist/editor/types.d.ts +250 -1
  177. package/dist/editor/world.d.ts +136 -0
  178. package/dist/editor/world.js +303 -0
  179. package/dist/faction/reputation.d.ts +5 -0
  180. package/dist/faction/reputation.js +5 -0
  181. package/dist/format/distance.d.ts +17 -0
  182. package/dist/format/distance.js +18 -0
  183. package/dist/format/duration.d.ts +27 -0
  184. package/dist/format/duration.js +45 -0
  185. package/dist/format/speed.d.ts +17 -0
  186. package/dist/format/speed.js +25 -0
  187. package/dist/game/breeding.d.ts +161 -0
  188. package/dist/game/breeding.js +181 -0
  189. package/dist/game/cameraConfig.d.ts +451 -0
  190. package/dist/game/cameraConfig.js +1 -0
  191. package/dist/game/connectedPlayers.d.ts +6 -4
  192. package/dist/game/connectedPlayers.js +2 -2
  193. package/dist/game/cosmetics.d.ts +9 -0
  194. package/dist/game/cosmetics.js +16 -0
  195. package/dist/game/defineGame.d.ts +110 -5
  196. package/dist/game/defineGame.js +49 -4
  197. package/dist/game/defineSystem.d.ts +90 -0
  198. package/dist/game/defineSystem.js +47 -0
  199. package/dist/game/dialogue.d.ts +33 -0
  200. package/dist/game/dialogue.js +21 -0
  201. package/dist/game/events.d.ts +63 -13
  202. package/dist/game/events.js +5 -0
  203. package/dist/game/feed.d.ts +33 -0
  204. package/dist/game/feed.js +35 -2
  205. package/dist/game/gamePhase.d.ts +11 -0
  206. package/dist/game/gamePhase.js +16 -0
  207. package/dist/game/leaderboard.d.ts +5 -0
  208. package/dist/game/leaderboard.js +5 -0
  209. package/dist/game/loadout.d.ts +5 -0
  210. package/dist/game/loadout.js +5 -0
  211. package/dist/game/lootFilter.d.ts +2 -0
  212. package/dist/game/lootFilter.js +2 -0
  213. package/dist/game/lootPipeline.d.ts +164 -0
  214. package/dist/game/lootPipeline.js +216 -0
  215. package/dist/game/lootTable.d.ts +19 -2
  216. package/dist/game/lootTable.js +15 -5
  217. package/dist/game/objectives.d.ts +43 -0
  218. package/dist/game/objectives.js +36 -0
  219. package/dist/game/paramLayers.d.ts +148 -0
  220. package/dist/game/paramLayers.js +185 -0
  221. package/dist/game/ping.d.ts +5 -0
  222. package/dist/game/ping.js +5 -0
  223. package/dist/game/playableGame.d.ts +116 -432
  224. package/dist/game/playableGame.js +1 -1
  225. package/dist/game/progression.d.ts +35 -0
  226. package/dist/game/progression.js +19 -0
  227. package/dist/game/quest.d.ts +8 -0
  228. package/dist/game/quest.js +30 -3
  229. package/dist/game/race.d.ts +141 -0
  230. package/dist/game/race.js +169 -0
  231. package/dist/game/recordBook.d.ts +3 -1
  232. package/dist/game/recordBook.js +4 -1
  233. package/dist/game/ruleSelection.d.ts +75 -0
  234. package/dist/game/ruleSelection.js +127 -0
  235. package/dist/game/runDraft.d.ts +5 -0
  236. package/dist/game/runDraft.js +5 -0
  237. package/dist/game/saveStore.d.ts +111 -0
  238. package/dist/game/saveStore.js +312 -0
  239. package/dist/game/snapshotHistory.d.ts +67 -0
  240. package/dist/game/snapshotHistory.js +95 -0
  241. package/dist/game/social.d.ts +38 -1
  242. package/dist/game/social.js +71 -0
  243. package/dist/game/spawnPoints.d.ts +5 -0
  244. package/dist/game/spawnPoints.js +5 -0
  245. package/dist/game/systemRuntime.d.ts +50 -0
  246. package/dist/game/systemRuntime.js +157 -0
  247. package/dist/game/systemSchedule.d.ts +72 -0
  248. package/dist/game/systemSchedule.js +242 -0
  249. package/dist/game/toasts.d.ts +43 -0
  250. package/dist/game/toasts.js +44 -0
  251. package/dist/game/trade.d.ts +10 -0
  252. package/dist/game/trade.js +10 -0
  253. package/dist/game/tribe.d.ts +184 -0
  254. package/dist/game/tribe.js +432 -0
  255. package/dist/game/unlocks.d.ts +8 -0
  256. package/dist/game/unlocks.js +16 -0
  257. package/dist/game/vfxInstance.d.ts +138 -0
  258. package/dist/game/vfxInstance.js +96 -0
  259. package/dist/game/worldItem.d.ts +17 -4
  260. package/dist/game/worldItem.js +17 -4
  261. package/dist/gameplay.d.ts +93 -0
  262. package/dist/gameplay.js +93 -0
  263. package/dist/index.d.ts +5 -14
  264. package/dist/index.js +5 -14
  265. package/dist/input/actionBindings.d.ts +23 -6
  266. package/dist/input/actionBindings.js +23 -6
  267. package/dist/input/axisInput.d.ts +7 -2
  268. package/dist/input/axisInput.js +7 -2
  269. package/dist/input/controlGroups.d.ts +67 -0
  270. package/dist/input/controlGroups.js +41 -0
  271. package/dist/input/touchScheme.d.ts +82 -6
  272. package/dist/input/touchScheme.js +123 -10
  273. package/dist/interaction/lockpick.d.ts +61 -0
  274. package/dist/interaction/lockpick.js +168 -0
  275. package/dist/interaction/proximityPrompt.d.ts +2 -0
  276. package/dist/interaction/proximityPrompt.js +2 -0
  277. package/dist/interaction/qte.d.ts +5 -0
  278. package/dist/interaction/qte.js +5 -0
  279. package/dist/interaction/skillCheck.d.ts +5 -0
  280. package/dist/interaction/skillCheck.js +5 -0
  281. package/dist/inventory/encumbrance.d.ts +69 -0
  282. package/dist/inventory/encumbrance.js +68 -0
  283. package/dist/inventory/inventoryModel.d.ts +12 -0
  284. package/dist/inventory/inventoryModel.js +12 -0
  285. package/dist/inventory/shapedGrid.d.ts +16 -0
  286. package/dist/inventory/shapedGrid.js +16 -0
  287. package/dist/inventory/slotModel.d.ts +10 -0
  288. package/dist/inventory/slotModel.js +10 -0
  289. package/dist/item/durability.d.ts +5 -0
  290. package/dist/item/durability.js +6 -3
  291. package/dist/item/itemIdentity.d.ts +171 -0
  292. package/dist/item/itemIdentity.js +138 -0
  293. package/dist/item/itemInstanceRegistry.d.ts +32 -0
  294. package/dist/item/itemInstanceRegistry.js +34 -0
  295. package/dist/item/itemgen.d.ts +163 -0
  296. package/dist/item/itemgen.js +151 -0
  297. package/dist/item/modularItem.d.ts +5 -0
  298. package/dist/item/modularItem.js +5 -0
  299. package/dist/item/use.d.ts +5 -0
  300. package/dist/item/use.js +5 -0
  301. package/dist/item/useBehavior.d.ts +142 -0
  302. package/dist/item/useBehavior.js +105 -0
  303. package/dist/item/weapon.d.ts +5 -0
  304. package/dist/item/weapon.js +5 -0
  305. package/dist/math/scalar.d.ts +25 -0
  306. package/dist/math/scalar.js +47 -0
  307. package/dist/meta/changelog.d.ts +4 -1
  308. package/dist/meta/changelog.js +39 -2
  309. package/dist/movement/avoidance.d.ts +45 -0
  310. package/dist/movement/avoidance.js +123 -0
  311. package/dist/movement/cameraRig.d.ts +8 -0
  312. package/dist/movement/cameraRig.js +8 -0
  313. package/dist/movement/dash.d.ts +11 -0
  314. package/dist/movement/dash.js +11 -0
  315. package/dist/movement/formation.d.ts +135 -0
  316. package/dist/movement/formation.js +194 -0
  317. package/dist/movement/glideModel.d.ts +5 -0
  318. package/dist/movement/glideModel.js +5 -0
  319. package/dist/movement/grappleSwing.d.ts +5 -0
  320. package/dist/movement/grappleSwing.js +5 -0
  321. package/dist/movement/leaderTrail.d.ts +5 -0
  322. package/dist/movement/leaderTrail.js +5 -0
  323. package/dist/movement/movementModel.d.ts +50 -9
  324. package/dist/movement/movementModel.js +75 -29
  325. package/dist/movement/playerMovement.js +167 -12
  326. package/dist/movement/poseState.d.ts +13 -0
  327. package/dist/movement/poseState.js +16 -0
  328. package/dist/movement/steering.d.ts +8 -0
  329. package/dist/movement/steering.js +18 -0
  330. package/dist/multiplayer/combatSnapshot.d.ts +3 -0
  331. package/dist/multiplayer/combatSnapshot.js +16 -8
  332. package/dist/multiplayer/lagCompensation.d.ts +5 -0
  333. package/dist/multiplayer/lagCompensation.js +5 -0
  334. package/dist/multiplayer/presenceModel.d.ts +11 -1
  335. package/dist/multiplayer/presenceModel.js +17 -1
  336. package/dist/multiplayer/simultaneousCommit.d.ts +3 -0
  337. package/dist/multiplayer/simultaneousCommit.js +3 -0
  338. package/dist/multiplayer.d.ts +11 -0
  339. package/dist/multiplayer.js +11 -0
  340. package/dist/nav/pathFollow.d.ts +57 -0
  341. package/dist/nav/pathFollow.js +109 -0
  342. package/dist/orders/orderKinds.d.ts +156 -0
  343. package/dist/orders/orderKinds.js +197 -0
  344. package/dist/orders/orderQueue.d.ts +208 -0
  345. package/dist/orders/orderQueue.js +211 -0
  346. package/dist/physics/drivableVehicle.d.ts +31 -0
  347. package/dist/physics/drivableVehicle.js +16 -0
  348. package/dist/physics/flightDynamics.d.ts +82 -0
  349. package/dist/physics/flightDynamics.js +165 -0
  350. package/dist/physics/gravityField.d.ts +25 -0
  351. package/dist/physics/gravityField.js +41 -0
  352. package/dist/physics/kinematicVehicle.d.ts +108 -1
  353. package/dist/physics/kinematicVehicle.js +212 -15
  354. package/dist/physics/physicsWorld.d.ts +50 -12
  355. package/dist/physics/physicsWorld.js +120 -122
  356. package/dist/physics/radialImpulse.d.ts +25 -0
  357. package/dist/physics/radialImpulse.js +27 -0
  358. package/dist/physics/vehicleObstacles.d.ts +44 -0
  359. package/dist/physics/vehicleObstacles.js +115 -0
  360. package/dist/procedural.d.ts +3 -0
  361. package/dist/procedural.js +3 -0
  362. package/dist/progression/statGraph.d.ts +116 -0
  363. package/dist/progression/statGraph.js +318 -0
  364. package/dist/puzzle/cellGrid.d.ts +9 -0
  365. package/dist/puzzle/cellGrid.js +9 -0
  366. package/dist/puzzle/fallingPiece.d.ts +9 -0
  367. package/dist/puzzle/fallingPiece.js +9 -0
  368. package/dist/puzzle/nonogram.d.ts +39 -0
  369. package/dist/puzzle/nonogram.js +136 -0
  370. package/dist/random/nameGen.d.ts +5 -0
  371. package/dist/random/nameGen.js +5 -0
  372. package/dist/random/pick.d.ts +8 -0
  373. package/dist/random/pick.js +31 -0
  374. package/dist/random/rng.d.ts +26 -1
  375. package/dist/random/rng.js +32 -9
  376. package/dist/random/seedLink.d.ts +4 -0
  377. package/dist/random/seedLink.js +4 -0
  378. package/dist/relation/keyedValues.d.ts +54 -0
  379. package/dist/relation/keyedValues.js +115 -0
  380. package/dist/relation/thresholds.d.ts +62 -0
  381. package/dist/relation/thresholds.js +62 -0
  382. package/dist/render/color.d.ts +12 -0
  383. package/dist/render/color.js +26 -0
  384. package/dist/render/lookPreset.d.ts +41 -0
  385. package/dist/render/lookPreset.js +29 -0
  386. package/dist/render/postProcessing.d.ts +20 -0
  387. package/dist/render/postProcessing.js +21 -1
  388. package/dist/rules/predicate.d.ts +53 -0
  389. package/dist/rules/predicate.js +64 -0
  390. package/dist/rules/ruleEffects.d.ts +38 -0
  391. package/dist/rules/ruleEffects.js +37 -0
  392. package/dist/rules/triggeredRules.d.ts +160 -0
  393. package/dist/rules/triggeredRules.js +241 -0
  394. package/dist/runtime/adapter.d.ts +55 -3
  395. package/dist/runtime/adapter.js +57 -1
  396. package/dist/runtime/cameraDirector.d.ts +1 -0
  397. package/dist/runtime/cameraDirector.js +1 -0
  398. package/dist/runtime/commandRunner.d.ts +1 -0
  399. package/dist/runtime/commandRunner.js +1 -0
  400. package/dist/runtime/context/combat.d.ts +51 -0
  401. package/dist/runtime/context/combat.js +130 -0
  402. package/dist/runtime/context/combatFx.d.ts +23 -0
  403. package/dist/runtime/context/combatFx.js +161 -0
  404. package/dist/runtime/context/deathLoot.d.ts +30 -0
  405. package/dist/runtime/context/deathLoot.js +39 -0
  406. package/dist/runtime/context/player.d.ts +48 -0
  407. package/dist/runtime/context/player.js +125 -0
  408. package/dist/runtime/context/registries.d.ts +13 -0
  409. package/dist/runtime/context/registries.js +84 -0
  410. package/dist/runtime/context/scene.d.ts +59 -0
  411. package/dist/runtime/context/scene.js +291 -0
  412. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  413. package/dist/runtime/context/snapshotCodecs.js +26 -0
  414. package/dist/runtime/context/worldItems.d.ts +21 -0
  415. package/dist/runtime/context/worldItems.js +37 -0
  416. package/dist/runtime/descriptors/baseline.d.ts +48 -0
  417. package/dist/runtime/descriptors/baseline.js +157 -0
  418. package/dist/runtime/descriptors/features.d.ts +79 -0
  419. package/dist/runtime/descriptors/features.js +299 -0
  420. package/dist/runtime/gameContext.d.ts +2 -327
  421. package/dist/runtime/gameContext.js +273 -778
  422. package/dist/runtime/gameContextTypes.d.ts +467 -0
  423. package/dist/runtime/gameContextTypes.js +1 -0
  424. package/dist/runtime/gameRuntime.d.ts +1 -0
  425. package/dist/runtime/gameRuntime.js +1 -0
  426. package/dist/runtime/headlessRunner.d.ts +62 -0
  427. package/dist/runtime/headlessRunner.js +45 -0
  428. package/dist/runtime/hostPersistence.d.ts +12 -0
  429. package/dist/runtime/hostPersistence.js +12 -0
  430. package/dist/runtime/hostPolicy.d.ts +51 -0
  431. package/dist/runtime/hostPolicy.js +68 -0
  432. package/dist/runtime/hostedGameRunner.d.ts +11 -4
  433. package/dist/runtime/hostedGameRunner.js +27 -3
  434. package/dist/runtime/hostedWorldSession.d.ts +15 -5
  435. package/dist/runtime/hostedWorldSession.js +12 -4
  436. package/dist/runtime/inputSnapshot.d.ts +8 -3
  437. package/dist/runtime/inputSnapshot.js +6 -2
  438. package/dist/runtime/motionIntents.d.ts +11 -2
  439. package/dist/runtime/motionIntents.js +16 -2
  440. package/dist/runtime/perContext.d.ts +15 -0
  441. package/dist/runtime/perContext.js +21 -0
  442. package/dist/runtime/runtimeSave.d.ts +79 -0
  443. package/dist/runtime/runtimeSave.js +105 -0
  444. package/dist/runtime/save.d.ts +4 -0
  445. package/dist/runtime/save.js +4 -0
  446. package/dist/runtime/snapshot.d.ts +7 -0
  447. package/dist/runtime/snapshot.js +7 -0
  448. package/dist/runtime/visibility.d.ts +2 -1
  449. package/dist/runtime/visibility.js +2 -1
  450. package/dist/runtime/worldChannel.d.ts +7 -3
  451. package/dist/runtime/worldChannel.js +51 -6
  452. package/dist/runtime/worldMirror.d.ts +14 -2
  453. package/dist/runtime/worldMirror.js +15 -2
  454. package/dist/runtime/worldProjection.d.ts +47 -0
  455. package/dist/runtime/worldProjection.js +52 -0
  456. package/dist/runtime/worldReplication.d.ts +30 -7
  457. package/dist/runtime/worldReplication.js +41 -9
  458. package/dist/runtime/worldSnapshot.d.ts +42 -3
  459. package/dist/runtime/worldSnapshot.js +34 -7
  460. package/dist/scene/assetCatalog.d.ts +26 -0
  461. package/dist/scene/assetCatalog.js +4 -0
  462. package/dist/scene/assetDiagnostics.d.ts +69 -0
  463. package/dist/scene/assetDiagnostics.js +110 -0
  464. package/dist/scene/assetGenerator.d.ts +65 -0
  465. package/dist/scene/assetGenerator.js +64 -0
  466. package/dist/scene/assetSpace.d.ts +137 -0
  467. package/dist/scene/assetSpace.js +257 -0
  468. package/dist/scene/authoredTriggers.d.ts +219 -0
  469. package/dist/scene/authoredTriggers.js +454 -0
  470. package/dist/scene/behaviorRuntime.d.ts +75 -0
  471. package/dist/scene/behaviorRuntime.js +217 -0
  472. package/dist/scene/behaviors.d.ts +14 -2
  473. package/dist/scene/behaviors.js +7 -2
  474. package/dist/scene/bodyBind.d.ts +60 -0
  475. package/dist/scene/bodyBind.js +51 -0
  476. package/dist/scene/builtinSceneKinds.d.ts +5 -0
  477. package/dist/scene/builtinSceneKinds.js +67 -0
  478. package/dist/scene/colliders.d.ts +71 -1
  479. package/dist/scene/colliders.js +145 -1
  480. package/dist/scene/collisionMesh.d.ts +94 -0
  481. package/dist/scene/collisionMesh.js +699 -0
  482. package/dist/scene/companion.d.ts +147 -0
  483. package/dist/scene/companion.js +193 -0
  484. package/dist/scene/entityStats.d.ts +18 -11
  485. package/dist/scene/entityStats.js +21 -16
  486. package/dist/scene/entityStore.d.ts +12 -1
  487. package/dist/scene/entityStore.js +15 -0
  488. package/dist/scene/facing.d.ts +18 -0
  489. package/dist/scene/facing.js +26 -0
  490. package/dist/scene/modelSockets.d.ts +33 -0
  491. package/dist/scene/modelSockets.js +29 -0
  492. package/dist/scene/objectQuery.d.ts +2 -0
  493. package/dist/scene/objectQuery.js +2 -0
  494. package/dist/scene/objectStore.d.ts +4 -0
  495. package/dist/scene/objectStore.js +6 -0
  496. package/dist/scene/possession.d.ts +8 -0
  497. package/dist/scene/possession.js +14 -0
  498. package/dist/scene/roster.d.ts +3 -0
  499. package/dist/scene/roster.js +12 -0
  500. package/dist/scene/sceneKinds.d.ts +256 -0
  501. package/dist/scene/sceneKinds.js +218 -0
  502. package/dist/scene/sceneOwnership.d.ts +169 -0
  503. package/dist/scene/sceneOwnership.js +179 -0
  504. package/dist/scene/sceneRaycast.js +15 -0
  505. package/dist/scene/selectionBookmarks.d.ts +93 -0
  506. package/dist/scene/selectionBookmarks.js +122 -0
  507. package/dist/scene/vehicleSeat.d.ts +68 -0
  508. package/dist/scene/vehicleSeat.js +59 -0
  509. package/dist/sensor/concealment.d.ts +3 -0
  510. package/dist/sensor/concealment.js +3 -0
  511. package/dist/sensor/freezeMonitor.d.ts +1 -0
  512. package/dist/sensor/freezeMonitor.js +1 -0
  513. package/dist/sensor/frustumSensor.d.ts +7 -3
  514. package/dist/sensor/frustumSensor.js +7 -3
  515. package/dist/sensor/hiddenStateProbe.d.ts +5 -2
  516. package/dist/sensor/hiddenStateProbe.js +5 -2
  517. package/dist/sensor/recordingBuffer.d.ts +2 -1
  518. package/dist/sensor/recordingBuffer.js +2 -1
  519. package/dist/sensor/replayLoop.d.ts +6 -2
  520. package/dist/sensor/replayLoop.js +6 -2
  521. package/dist/sensor/revealQuery.d.ts +2 -1
  522. package/dist/sensor/revealQuery.js +2 -1
  523. package/dist/sensor/visionCone.d.ts +10 -3
  524. package/dist/sensor/visionCone.js +10 -3
  525. package/dist/session/contestedChannel.d.ts +1 -0
  526. package/dist/session/contestedChannel.js +1 -0
  527. package/dist/session/extraction.d.ts +1 -0
  528. package/dist/session/extraction.js +1 -0
  529. package/dist/session/roles.d.ts +6 -0
  530. package/dist/session/roles.js +6 -0
  531. package/dist/session/roundState.d.ts +8 -0
  532. package/dist/session/roundState.js +2 -0
  533. package/dist/settings/settingsModel.d.ts +1 -0
  534. package/dist/settings/settingsModel.js +1 -0
  535. package/dist/stats/accumulatorMeter.d.ts +6 -0
  536. package/dist/stats/accumulatorMeter.js +6 -0
  537. package/dist/stats/eventMeter.d.ts +5 -0
  538. package/dist/stats/eventMeter.js +5 -0
  539. package/dist/stats/rollCheck.d.ts +5 -0
  540. package/dist/stats/rollCheck.js +5 -0
  541. package/dist/stats/spawnLevelStats.d.ts +110 -0
  542. package/dist/stats/spawnLevelStats.js +140 -0
  543. package/dist/stats/statModifiers.d.ts +5 -0
  544. package/dist/stats/statModifiers.js +5 -0
  545. package/dist/stats/statPool.d.ts +70 -0
  546. package/dist/stats/statPool.js +59 -0
  547. package/dist/store/changeSignal.d.ts +3 -0
  548. package/dist/store/changeSignal.js +2 -0
  549. package/dist/store/defineKeyedStore.d.ts +38 -0
  550. package/dist/store/defineKeyedStore.js +42 -0
  551. package/dist/store/defineStore.d.ts +33 -0
  552. package/dist/store/defineStore.js +35 -0
  553. package/dist/store/observableKeyedStore.d.ts +7 -0
  554. package/dist/store/observableKeyedStore.js +35 -2
  555. package/dist/survival/decayMeter.d.ts +55 -0
  556. package/dist/survival/decayMeter.js +130 -57
  557. package/dist/survival/moodle.d.ts +2 -0
  558. package/dist/survival/regionHealth.d.ts +6 -0
  559. package/dist/survival/regionHealth.js +6 -3
  560. package/dist/tactics/fallingGrid.d.ts +4 -2
  561. package/dist/tactics/fallingGrid.js +4 -2
  562. package/dist/tactics/predictiveQuery.d.ts +3 -0
  563. package/dist/tactics/predictiveQuery.js +3 -0
  564. package/dist/tactics/snapshot.d.ts +2 -0
  565. package/dist/tactics/snapshot.js +2 -0
  566. package/dist/tactics/surface.d.ts +1 -0
  567. package/dist/tactics/surface.js +1 -0
  568. package/dist/tactics/tacticalGrid.d.ts +1 -0
  569. package/dist/tactics/tacticalGrid.js +1 -0
  570. package/dist/time/beatClock.d.ts +14 -4
  571. package/dist/time/beatClock.js +14 -4
  572. package/dist/time/calendarClock.d.ts +3 -0
  573. package/dist/time/calendarClock.js +3 -0
  574. package/dist/time/idleProgress.d.ts +4 -0
  575. package/dist/time/idleProgress.js +4 -0
  576. package/dist/time/simClock.d.ts +2 -0
  577. package/dist/time/simClock.js +20 -5
  578. package/dist/ui/actionModel.d.ts +110 -0
  579. package/dist/ui/actionModel.js +159 -0
  580. package/dist/ui/gameLayout.d.ts +30 -11
  581. package/dist/ui/gameLayout.js +30 -11
  582. package/dist/ui/hudDocument.d.ts +114 -0
  583. package/dist/ui/hudDocument.js +220 -0
  584. package/dist/ui/hudLayout.d.ts +38 -1
  585. package/dist/ui/hudLayout.js +220 -17
  586. package/dist/ui/selectionModel.d.ts +92 -0
  587. package/dist/ui/selectionModel.js +88 -0
  588. package/dist/ui/tooltipModel.d.ts +63 -0
  589. package/dist/ui/tooltipModel.js +82 -0
  590. package/dist/ui.d.ts +15 -0
  591. package/dist/ui.js +15 -0
  592. package/dist/visibility/bounds.d.ts +8 -1
  593. package/dist/visibility/bounds.js +8 -1
  594. package/dist/visibility/frustum.d.ts +12 -3
  595. package/dist/visibility/frustum.js +12 -3
  596. package/dist/work/jobQueue.d.ts +201 -0
  597. package/dist/work/jobQueue.js +253 -0
  598. package/dist/work/unitTraining.d.ts +58 -0
  599. package/dist/work/unitTraining.js +52 -0
  600. package/dist/world/authoredEntities.d.ts +29 -0
  601. package/dist/world/authoredEntities.js +29 -0
  602. package/dist/world/authoredObjects.d.ts +72 -0
  603. package/dist/world/authoredObjects.js +81 -0
  604. package/dist/world/authoredSpawn.d.ts +39 -0
  605. package/dist/world/authoredSpawn.js +37 -0
  606. package/dist/world/buildSockets.d.ts +152 -0
  607. package/dist/world/buildSockets.js +188 -0
  608. package/dist/world/buildingGenerator.d.ts +18 -0
  609. package/dist/world/buildingGenerator.js +62 -0
  610. package/dist/world/buildings.d.ts +5 -0
  611. package/dist/world/buildings.js +5 -0
  612. package/dist/world/catenary.d.ts +22 -0
  613. package/dist/world/catenary.js +69 -0
  614. package/dist/world/cityBlocks.d.ts +152 -0
  615. package/dist/world/cityBlocks.js +670 -0
  616. package/dist/world/cityContent.d.ts +77 -0
  617. package/dist/world/cityContent.js +261 -0
  618. package/dist/world/cityGeometry.d.ts +114 -0
  619. package/dist/world/cityGeometry.js +573 -0
  620. package/dist/world/cityKind.d.ts +314 -0
  621. package/dist/world/cityKind.js +1578 -0
  622. package/dist/world/features.d.ts +133 -10
  623. package/dist/world/features.js +66 -6
  624. package/dist/world/footprintGrid.d.ts +59 -0
  625. package/dist/world/footprintGrid.js +141 -0
  626. package/dist/world/geometry.d.ts +64 -0
  627. package/dist/world/geometry.js +94 -0
  628. package/dist/world/grassKind.d.ts +51 -0
  629. package/dist/world/grassKind.js +101 -0
  630. package/dist/world/markers.d.ts +50 -4
  631. package/dist/world/markers.js +49 -0
  632. package/dist/world/massing.js +1 -1
  633. package/dist/world/minimapBake.d.ts +78 -0
  634. package/dist/world/minimapBake.js +208 -0
  635. package/dist/world/minimapTrack.d.ts +17 -0
  636. package/dist/world/minimapTrack.js +22 -0
  637. package/dist/world/pathInstances.d.ts +27 -0
  638. package/dist/world/pathInstances.js +27 -0
  639. package/dist/world/pathNetwork.d.ts +159 -0
  640. package/dist/world/pathNetwork.js +684 -0
  641. package/dist/world/pathTerrain.d.ts +80 -0
  642. package/dist/world/pathTerrain.js +142 -0
  643. package/dist/world/place.d.ts +166 -0
  644. package/dist/world/place.js +96 -0
  645. package/dist/world/placeAsset.d.ts +81 -0
  646. package/dist/world/placeAsset.js +79 -0
  647. package/dist/world/placement.d.ts +4 -0
  648. package/dist/world/placement.js +4 -0
  649. package/dist/world/placementController.d.ts +6 -0
  650. package/dist/world/placementController.js +6 -0
  651. package/dist/world/poleLineKind.d.ts +40 -0
  652. package/dist/world/poleLineKind.js +97 -0
  653. package/dist/world/polyline.d.ts +34 -0
  654. package/dist/world/polyline.js +88 -0
  655. package/dist/world/resourceNode.d.ts +109 -0
  656. package/dist/world/resourceNode.js +140 -0
  657. package/dist/world/roads.d.ts +8 -0
  658. package/dist/world/roads.js +38 -0
  659. package/dist/world/scatter.js +2 -20
  660. package/dist/world/scatterRegion.d.ts +164 -0
  661. package/dist/world/scatterRegion.js +409 -0
  662. package/dist/world/sceneShapes.d.ts +43 -0
  663. package/dist/world/sceneShapes.js +1 -0
  664. package/dist/world/segment.d.ts +27 -0
  665. package/dist/world/segment.js +43 -0
  666. package/dist/world/soilKind.d.ts +48 -0
  667. package/dist/world/soilKind.js +92 -0
  668. package/dist/world/spatialSample.d.ts +241 -0
  669. package/dist/world/spatialSample.js +465 -0
  670. package/dist/world/streets.d.ts +29 -0
  671. package/dist/world/streets.js +55 -0
  672. package/dist/world/terraform.d.ts +213 -1
  673. package/dist/world/terraform.js +627 -22
  674. package/dist/world/terrain.d.ts +164 -20
  675. package/dist/world/terrain.js +325 -41
  676. package/dist/world/terrainGuides.d.ts +182 -0
  677. package/dist/world/terrainGuides.js +343 -0
  678. package/dist/world/vec2.d.ts +81 -0
  679. package/dist/world/vec2.js +128 -0
  680. package/dist/world/vec3.d.ts +49 -0
  681. package/dist/world/vec3.js +76 -0
  682. package/dist/world/vegetation.d.ts +71 -0
  683. package/dist/world/vegetation.js +151 -0
  684. package/dist/world/visibilityField.d.ts +154 -0
  685. package/dist/world/visibilityField.js +271 -0
  686. package/dist/world/volumetricClouds.d.ts +52 -0
  687. package/dist/world/volumetricClouds.js +43 -0
  688. package/dist/world/walls.d.ts +9 -0
  689. package/dist/world/walls.js +9 -0
  690. package/dist/world/waterKind.d.ts +55 -0
  691. package/dist/world/waterKind.js +99 -0
  692. package/dist/world.d.ts +141 -0
  693. package/dist/world.js +141 -0
  694. package/package.json +32 -8
  695. package/skills/game-design/SKILL.md +28 -0
  696. package/skills/game-design/agents/openai.yaml +4 -0
  697. package/skills/game-design/references/application-playbook.md +170 -0
  698. package/skills/game-design/references/field-guide.md +288 -0
  699. package/skills/jgengine/SKILL.md +54 -0
  700. package/skills/jgengine/api.md +527 -0
  701. package/skills/jgengine/capabilities.md +131 -0
  702. package/skills/jgengine/recipes/minimal-game.md +88 -0
  703. package/skills/jgengine-combat/SKILL.md +38 -0
  704. package/skills/jgengine-combat/api.md +585 -0
  705. package/skills/jgengine-combat/capabilities.md +213 -0
  706. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  707. package/skills/jgengine-combat/reference.md +108 -0
  708. package/skills/jgengine-gameplay/SKILL.md +39 -0
  709. package/skills/jgengine-gameplay/api.md +1920 -0
  710. package/skills/jgengine-gameplay/capabilities.md +454 -0
  711. package/skills/jgengine-gameplay/reference-systems.md +56 -0
  712. package/skills/jgengine-world/SKILL.md +51 -0
  713. package/skills/jgengine-world/api.md +3056 -0
  714. package/skills/jgengine-world/capabilities.md +476 -0
  715. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  716. package/skills/jgengine-world/reference.md +261 -0
  717. package/dist/cartridge/runtime.d.ts +0 -15
  718. package/dist/cartridge/runtime.js +0 -556
  719. package/dist/cartridge/spec.d.ts +0 -188
  720. package/dist/cartridge/spec.js +0 -19
  721. package/dist/cartridge/validate.d.ts +0 -2
  722. package/dist/cartridge/validate.js +0 -78
  723. package/dist/store/engineStore.d.ts +0 -22
  724. package/dist/store/engineStore.js +0 -50
  725. package/dist/store/features.d.ts +0 -10
  726. package/dist/store/features.js +0 -23
  727. package/llms.txt +0 -5389
@@ -0,0 +1,1578 @@
1
+ /**
2
+ * `city` studio: a procedural district authored as a box volume — drop a volume and sliders tune
3
+ * the whole synthesis, from a rigid Manhattan grid (`gridness` 1, `curviness` 0) to winding
4
+ * hillside estates, a two-road crossroads farm town, or — at the loopiness-high / branching-and-
5
+ * connectivity-low corner — a closed RACE CIRCUIT. Streets come from the unified seed-driven
6
+ * {@link buildPathNetwork} engine (city and track are the same generator at opposite slider
7
+ * extremes), carry a visible hierarchy (boulevards with medians, avenues, streets, lanes —
8
+ * optionally gravel), and connect or deliberately dead-end into cul-de-sac bulbs; water gaps become
9
+ * styled bridge decks and ridges become tunnel bores, both path features on continuous streets so a
10
+ * lap stays closed. The block/parcel/building FABRIC is optional (`fabric` off = bare roads or a
11
+ * plot-free track). Generation is BLOCK-FIRST (`cityBlocks`): the street graph's planar faces become closed
12
+ * block polygons (a curved street bounds a curved block), each inset to curb and land rings with
13
+ * the sidewalk band between them, then classified and subdivided into polygonal frontage PARCELS
14
+ * whose buildable polygons (after setbacks) place every building. Inside the district a ZONE
15
+ * layer (core/mid/edge bands, invertible or spatially uniform) drives what a parcel carries: a
16
+ * weighted class mix per band picks the building class (tower, slab, shop, rowhouse, house,
17
+ * mansion, farmhouse, barn, silo), and each class composes deterministic massing PIECES
18
+ * (`cityContent`) — setback tower tiers, gabled houses, gambrel barns, domed silos. Blocks left
19
+ * unbuilt become plazas/greens/meadows by band, or crop-row farm fields; slivers and block
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
23
+ * locally, so hand-placed intent wins over the profile. Named presets (Manhattan, Los Angeles,
24
+ * Beverly Hills, rural Ohio…) ship as plain slider bundles on the schema. Pure config + resolver
25
+ * here; the instanced renderer lives in the `shell`. Everything is deterministic per seed and
26
+ * persisted in `editor.scene.json`.
27
+ *
28
+ * @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
29
+ */
30
+ import { seededStreams } from "../random/rng.js";
31
+ import { parseParams, registerSceneKind, } from "../scene/sceneKinds.js";
32
+ import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_STYLE } from "./buildings.js";
33
+ import { buildLotPieces, pickClass, pickSpecies, rollClassPlacement, zoneBand, zoneMetric, } from "./cityContent.js";
34
+ import { furnitureSpots } from "./streets.js";
35
+ import { buildPathNetwork, pathNetworkMode, } from "./pathNetwork.js";
36
+ import { buildablePolygon, carveCorridors, conformPolygonToRing, cutParcel, extractBlocks, isSliverBlock, pointsInPolygon, RingWalker, } from "./cityBlocks.js";
37
+ import { fitRectInPolygon, rayDistanceToRing, pointInPolygon, polygonArea, polygonCentroid, polygonMeanWidth, polygonsOverlap, insetRingUniform, ringBounds, } from "./cityGeometry.js";
38
+ /** The editor volume kind marking a box as a procedural city district. */
39
+ export const CITY_KIND = "city";
40
+ /** The editor volume kind that locally overrides a city's zone band/mix — a district in a district. */
41
+ export const CITY_ZONE_KIND = "cityzone";
42
+ /** City defaults: a zoned mixed metropolis — towers downtown, slabs mid-ring, houses at the edge. */
43
+ export const CITY_DEFAULTS = {
44
+ gridness: 0.85,
45
+ curviness: 0.15,
46
+ branching: 0.4,
47
+ loopiness: 0,
48
+ connectivity: 0.88,
49
+ deadEnds: 0.35,
50
+ minCurveRadius: 24,
51
+ minTurnAngle: 0,
52
+ maxTurnAngle: 150,
53
+ fabric: true,
54
+ tunnels: true,
55
+ blockSize: 48,
56
+ blockAspect: 1,
57
+ openSpace: 0.12,
58
+ roadsideOccupancy: 0.88,
59
+ blockDensity: 0.6,
60
+ buildingRoadSetback: 2.5,
61
+ buildingSpacing: 1.4,
62
+ clusterStrength: 0.5,
63
+ streetWidth: 7,
64
+ boulevards: 0.35,
65
+ gravelLanes: false,
66
+ surface: "auto",
67
+ profile: "core-out",
68
+ coreExtent: 0.35,
69
+ midExtent: 0.7,
70
+ coreMix: [
71
+ { item: "tower", weight: 3 },
72
+ { item: "slab", weight: 1 },
73
+ { item: "shop", weight: 1 },
74
+ ],
75
+ midMix: [
76
+ { item: "slab", weight: 2 },
77
+ { item: "rowhouse", weight: 2 },
78
+ { item: "shop", weight: 1 },
79
+ ],
80
+ edgeMix: [
81
+ { item: "house", weight: 3 },
82
+ { item: "rowhouse", weight: 1 },
83
+ ],
84
+ lotScale: 1,
85
+ floorsMin: 1,
86
+ floorsMax: 30,
87
+ floorHeight: 3,
88
+ treeDensity: 0.5,
89
+ treeMix: [
90
+ { item: "broadleaf", weight: 3 },
91
+ { item: "conifer", weight: 1 },
92
+ ],
93
+ lightDensity: 0.5,
94
+ hedges: true,
95
+ driveways: true,
96
+ parking: true,
97
+ fields: false,
98
+ maxSlope: 0.5,
99
+ minElevation: -2,
100
+ bridges: true,
101
+ bridgeStyle: "arch",
102
+ sidewalks: true,
103
+ sidewalkWidth: 1.9,
104
+ style: DEFAULT_BUILDING_STYLE,
105
+ seed: "",
106
+ };
107
+ /** The city parameter schema — drives the inspector sliders and `meta` parse via the studio seam. */
108
+ export const CITY_SCHEMA = {
109
+ groups: [
110
+ { id: "layout", label: "Layout" },
111
+ { id: "placement", label: "Placement" },
112
+ { id: "zoning", label: "Zoning" },
113
+ { id: "buildings", label: "Buildings" },
114
+ { id: "greenery", label: "Greenery & furniture" },
115
+ ],
116
+ fields: [
117
+ { type: "range", key: "gridness", label: "grid-ness", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.gridness },
118
+ { type: "range", key: "curviness", label: "curviness", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.curviness },
119
+ { type: "range", key: "branching", label: "branching", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.branching },
120
+ { type: "range", key: "loopiness", label: "loopiness", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.loopiness },
121
+ { type: "range", key: "connectivity", label: "connectivity", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.connectivity },
122
+ { type: "range", key: "deadEnds", label: "dead ends", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.deadEnds },
123
+ { type: "range", key: "minCurveRadius", label: "min curve radius", group: "layout", min: 6, max: 120, step: 1, default: CITY_DEFAULTS.minCurveRadius, unit: "m" },
124
+ { type: "range", key: "minTurnAngle", label: "min turn angle", group: "layout", min: 0, max: 60, step: 1, default: CITY_DEFAULTS.minTurnAngle, unit: "°" },
125
+ { type: "range", key: "maxTurnAngle", label: "max turn angle", group: "layout", min: 30, max: 180, step: 1, default: CITY_DEFAULTS.maxTurnAngle, unit: "°" },
126
+ { type: "range", key: "blockSize", label: "block size", group: "layout", min: 20, max: 140, step: 1, default: CITY_DEFAULTS.blockSize, unit: "m" },
127
+ { type: "range", key: "blockAspect", label: "block aspect", group: "layout", min: 1, max: 3, step: 0.05, default: CITY_DEFAULTS.blockAspect },
128
+ { type: "range", key: "openSpace", label: "open space", group: "layout", min: 0, max: 0.9, step: 0.01, default: CITY_DEFAULTS.openSpace },
129
+ { type: "range", key: "streetWidth", label: "street width", group: "layout", min: 3, max: 16, step: 0.5, default: CITY_DEFAULTS.streetWidth, unit: "m" },
130
+ { type: "range", key: "boulevards", label: "boulevards", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.boulevards },
131
+ { type: "bool", key: "gravelLanes", label: "gravel lanes", group: "layout", default: CITY_DEFAULTS.gravelLanes },
132
+ {
133
+ type: "select",
134
+ key: "surface",
135
+ label: "surface",
136
+ group: "layout",
137
+ default: CITY_DEFAULTS.surface,
138
+ options: [{ value: "auto" }, { value: "gravel" }],
139
+ },
140
+ { type: "bool", key: "fabric", label: "buildings & parcels", group: "layout", default: CITY_DEFAULTS.fabric },
141
+ { type: "bool", key: "bridges", label: "bridges over water", group: "layout", default: CITY_DEFAULTS.bridges },
142
+ { type: "bool", key: "tunnels", label: "tunnels through ridges", group: "layout", default: CITY_DEFAULTS.tunnels },
143
+ {
144
+ type: "select",
145
+ key: "bridgeStyle",
146
+ label: "bridge style",
147
+ group: "layout",
148
+ default: CITY_DEFAULTS.bridgeStyle,
149
+ options: [{ value: "arch" }, { value: "truss" }, { value: "beam" }],
150
+ },
151
+ { type: "bool", key: "sidewalks", label: "sidewalks", group: "layout", default: CITY_DEFAULTS.sidewalks },
152
+ { type: "range", key: "sidewalkWidth", label: "sidewalk width", group: "layout", min: 1, max: 4, step: 0.1, default: CITY_DEFAULTS.sidewalkWidth, unit: "m" },
153
+ { type: "action", key: "layoutRandomize", label: "randomize layout", group: "layout", action: "randomize" },
154
+ {
155
+ type: "select",
156
+ key: "profile",
157
+ label: "zone profile",
158
+ group: "zoning",
159
+ default: CITY_DEFAULTS.profile,
160
+ options: [{ value: "core-out" }, { value: "inverted" }, { value: "uniform" }],
161
+ },
162
+ { type: "range", key: "coreExtent", label: "core extent", group: "zoning", min: 0.05, max: 0.9, step: 0.01, default: CITY_DEFAULTS.coreExtent },
163
+ { type: "range", key: "midExtent", label: "mid extent", group: "zoning", min: 0.1, max: 0.95, step: 0.01, default: CITY_DEFAULTS.midExtent },
164
+ { type: "weightedList", key: "coreMix", label: "core mix", group: "zoning", itemLabel: "class", default: CITY_DEFAULTS.coreMix },
165
+ { type: "weightedList", key: "midMix", label: "mid mix", group: "zoning", itemLabel: "class", default: CITY_DEFAULTS.midMix },
166
+ { type: "weightedList", key: "edgeMix", label: "edge mix", group: "zoning", itemLabel: "class", default: CITY_DEFAULTS.edgeMix },
167
+ { type: "range", key: "lotScale", label: "lot scale", group: "zoning", min: 0.5, max: 2.5, step: 0.05, default: CITY_DEFAULTS.lotScale },
168
+ { type: "range", key: "roadsideOccupancy", label: "roadside occupancy", group: "placement", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.roadsideOccupancy },
169
+ { type: "range", key: "blockDensity", label: "block density", group: "placement", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.blockDensity },
170
+ { type: "range", key: "buildingRoadSetback", label: "road setback", group: "placement", min: 0, max: 12, step: 0.1, default: CITY_DEFAULTS.buildingRoadSetback, unit: "m" },
171
+ { type: "range", key: "buildingSpacing", label: "building spacing", group: "placement", min: 0.2, max: 10, step: 0.1, default: CITY_DEFAULTS.buildingSpacing, unit: "m" },
172
+ { type: "range", key: "clusterStrength", label: "cluster strength", group: "placement", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.clusterStrength },
173
+ { type: "range", key: "floorsMin", label: "floors min", group: "buildings", min: 1, max: 60, step: 1, default: CITY_DEFAULTS.floorsMin },
174
+ { type: "range", key: "floorsMax", label: "floors max", group: "buildings", min: 1, max: 60, step: 1, default: CITY_DEFAULTS.floorsMax },
175
+ { type: "range", key: "floorHeight", label: "floor height", group: "buildings", min: 2, max: 5, step: 0.1, default: CITY_DEFAULTS.floorHeight, unit: "m" },
176
+ { type: "range", key: "maxSlope", label: "max slope", group: "buildings", min: 0.05, max: 2, step: 0.05, default: CITY_DEFAULTS.maxSlope },
177
+ { type: "number", key: "minElevation", label: "build above y", group: "buildings", step: 0.5, default: CITY_DEFAULTS.minElevation },
178
+ {
179
+ type: "select",
180
+ key: "style",
181
+ label: "style",
182
+ group: "buildings",
183
+ default: CITY_DEFAULTS.style,
184
+ options: Object.keys(BUILDING_STYLE_PALETTES).map((style) => ({ value: style })),
185
+ },
186
+ { type: "range", key: "treeDensity", label: "tree density", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.treeDensity },
187
+ { type: "weightedList", key: "treeMix", label: "tree mix", group: "greenery", itemLabel: "species", default: CITY_DEFAULTS.treeMix },
188
+ { type: "range", key: "lightDensity", label: "street lights", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.lightDensity },
189
+ { type: "bool", key: "hedges", label: "estate hedges", group: "greenery", default: CITY_DEFAULTS.hedges },
190
+ { type: "bool", key: "driveways", label: "driveways", group: "greenery", default: CITY_DEFAULTS.driveways },
191
+ { type: "bool", key: "parking", label: "parking lots", group: "greenery", default: CITY_DEFAULTS.parking },
192
+ { type: "bool", key: "fields", label: "farm fields", group: "greenery", default: CITY_DEFAULTS.fields },
193
+ { type: "seed", key: "seed", label: "seed", default: CITY_DEFAULTS.seed },
194
+ ],
195
+ presets: [
196
+ {
197
+ id: "manhattan",
198
+ label: "Manhattan",
199
+ values: {
200
+ gridness: 1,
201
+ curviness: 0,
202
+ branching: 0.05,
203
+ blockSize: 32,
204
+ blockAspect: 2.4,
205
+ openSpace: 0.05,
206
+ roadsideOccupancy: 0.97,
207
+ blockDensity: 0.85,
208
+ buildingRoadSetback: 0.8,
209
+ buildingSpacing: 0.8,
210
+ clusterStrength: 0.25,
211
+ streetWidth: 9,
212
+ boulevards: 0.5,
213
+ gravelLanes: false,
214
+ profile: "core-out",
215
+ coreExtent: 0.6,
216
+ midExtent: 0.88,
217
+ coreMix: [
218
+ { item: "tower", weight: 5 },
219
+ { item: "slab", weight: 1 },
220
+ ],
221
+ midMix: [
222
+ { item: "tower", weight: 2 },
223
+ { item: "slab", weight: 2 },
224
+ ],
225
+ edgeMix: [
226
+ { item: "slab", weight: 3 },
227
+ { item: "rowhouse", weight: 2 },
228
+ ],
229
+ lotScale: 1,
230
+ floorsMin: 5,
231
+ floorsMax: 55,
232
+ treeDensity: 0.3,
233
+ treeMix: [{ item: "broadleaf", weight: 1 }],
234
+ lightDensity: 0.8,
235
+ hedges: false,
236
+ driveways: false,
237
+ parking: false,
238
+ fields: false,
239
+ sidewalks: true,
240
+ style: "capital",
241
+ },
242
+ },
243
+ {
244
+ id: "losangeles",
245
+ label: "Los Angeles",
246
+ values: {
247
+ gridness: 0.7,
248
+ curviness: 0.25,
249
+ branching: 0.5,
250
+ blockSize: 62,
251
+ blockAspect: 1.2,
252
+ openSpace: 0.08,
253
+ roadsideOccupancy: 0.9,
254
+ blockDensity: 0.55,
255
+ buildingRoadSetback: 3,
256
+ buildingSpacing: 1.8,
257
+ clusterStrength: 0.5,
258
+ streetWidth: 8,
259
+ boulevards: 0.45,
260
+ gravelLanes: false,
261
+ profile: "core-out",
262
+ coreExtent: 0.25,
263
+ midExtent: 0.55,
264
+ coreMix: [
265
+ { item: "tower", weight: 2 },
266
+ { item: "slab", weight: 2 },
267
+ { item: "shop", weight: 2 },
268
+ ],
269
+ midMix: [
270
+ { item: "shop", weight: 2 },
271
+ { item: "house", weight: 3 },
272
+ { item: "slab", weight: 1 },
273
+ ],
274
+ edgeMix: [
275
+ { item: "house", weight: 5 },
276
+ { item: "shop", weight: 1 },
277
+ ],
278
+ lotScale: 1.1,
279
+ floorsMin: 1,
280
+ floorsMax: 18,
281
+ treeDensity: 0.55,
282
+ treeMix: [
283
+ { item: "palm", weight: 4 },
284
+ { item: "broadleaf", weight: 1 },
285
+ { item: "cypress", weight: 1 },
286
+ ],
287
+ lightDensity: 0.6,
288
+ hedges: false,
289
+ driveways: true,
290
+ parking: true,
291
+ fields: false,
292
+ sidewalks: true,
293
+ style: "coastal",
294
+ },
295
+ },
296
+ {
297
+ id: "beverlyhills",
298
+ label: "Beverly Hills",
299
+ values: {
300
+ gridness: 0.2,
301
+ curviness: 0.8,
302
+ branching: 0.6,
303
+ blockSize: 90,
304
+ blockAspect: 1,
305
+ openSpace: 0.07,
306
+ roadsideOccupancy: 0.85,
307
+ blockDensity: 0.3,
308
+ buildingRoadSetback: 4.5,
309
+ buildingSpacing: 2.2,
310
+ clusterStrength: 0.35,
311
+ streetWidth: 6.5,
312
+ boulevards: 0,
313
+ gravelLanes: false,
314
+ profile: "uniform",
315
+ coreExtent: 0.35,
316
+ midExtent: 0.7,
317
+ coreMix: [
318
+ { item: "mansion", weight: 3 },
319
+ { item: "house", weight: 1 },
320
+ ],
321
+ midMix: [
322
+ { item: "mansion", weight: 3 },
323
+ { item: "house", weight: 1 },
324
+ ],
325
+ edgeMix: [
326
+ { item: "mansion", weight: 2 },
327
+ { item: "house", weight: 2 },
328
+ ],
329
+ lotScale: 1.5,
330
+ floorsMin: 1,
331
+ floorsMax: 3,
332
+ treeDensity: 0.9,
333
+ treeMix: [
334
+ { item: "palm", weight: 3 },
335
+ { item: "cypress", weight: 2 },
336
+ { item: "broadleaf", weight: 2 },
337
+ ],
338
+ lightDensity: 0.35,
339
+ hedges: true,
340
+ driveways: true,
341
+ parking: false,
342
+ fields: false,
343
+ sidewalks: true,
344
+ style: "coastal",
345
+ },
346
+ },
347
+ {
348
+ id: "ruralohio",
349
+ label: "Rural Ohio",
350
+ values: {
351
+ gridness: 1,
352
+ curviness: 0.06,
353
+ branching: 0.3,
354
+ blockSize: 120,
355
+ blockAspect: 1,
356
+ openSpace: 0.55,
357
+ roadsideOccupancy: 0.6,
358
+ blockDensity: 0.25,
359
+ buildingRoadSetback: 6,
360
+ buildingSpacing: 5,
361
+ clusterStrength: 0.8,
362
+ streetWidth: 5.5,
363
+ boulevards: 0,
364
+ gravelLanes: true,
365
+ profile: "core-out",
366
+ coreExtent: 0.22,
367
+ midExtent: 0.55,
368
+ coreMix: [
369
+ { item: "shop", weight: 2 },
370
+ { item: "house", weight: 2 },
371
+ { item: "rowhouse", weight: 1 },
372
+ ],
373
+ midMix: [
374
+ { item: "farmhouse", weight: 2 },
375
+ { item: "house", weight: 1 },
376
+ { item: "barn", weight: 1 },
377
+ ],
378
+ edgeMix: [
379
+ { item: "farmhouse", weight: 2 },
380
+ { item: "barn", weight: 2 },
381
+ { item: "silo", weight: 1 },
382
+ ],
383
+ lotScale: 1.4,
384
+ floorsMin: 1,
385
+ floorsMax: 2,
386
+ treeDensity: 0.35,
387
+ treeMix: [
388
+ { item: "broadleaf", weight: 3 },
389
+ { item: "conifer", weight: 2 },
390
+ ],
391
+ lightDensity: 0.1,
392
+ hedges: false,
393
+ driveways: true,
394
+ parking: false,
395
+ fields: true,
396
+ sidewalks: false,
397
+ style: "village",
398
+ },
399
+ },
400
+ {
401
+ id: "circuit",
402
+ label: "Race circuit",
403
+ values: {
404
+ gridness: 0.4,
405
+ curviness: 0.55,
406
+ branching: 0.15,
407
+ loopiness: 1,
408
+ connectivity: 0.05,
409
+ deadEnds: 0,
410
+ minCurveRadius: 34,
411
+ minTurnAngle: 0,
412
+ maxTurnAngle: 95,
413
+ blockSize: 95,
414
+ blockAspect: 1,
415
+ openSpace: 0,
416
+ streetWidth: 12,
417
+ boulevards: 0,
418
+ gravelLanes: false,
419
+ surface: "auto",
420
+ fabric: false,
421
+ bridges: true,
422
+ tunnels: true,
423
+ sidewalks: false,
424
+ treeDensity: 0.2,
425
+ lightDensity: 0.25,
426
+ hedges: false,
427
+ driveways: false,
428
+ parking: false,
429
+ fields: false,
430
+ style: "generic",
431
+ },
432
+ },
433
+ {
434
+ id: "rally",
435
+ label: "Rally stage",
436
+ values: {
437
+ gridness: 0.1,
438
+ curviness: 0.95,
439
+ branching: 0.2,
440
+ loopiness: 0.15,
441
+ connectivity: 0.12,
442
+ deadEnds: 0.3,
443
+ minCurveRadius: 12,
444
+ minTurnAngle: 0,
445
+ maxTurnAngle: 150,
446
+ blockSize: 72,
447
+ blockAspect: 1,
448
+ openSpace: 0,
449
+ streetWidth: 6,
450
+ boulevards: 0,
451
+ gravelLanes: true,
452
+ surface: "gravel",
453
+ fabric: false,
454
+ bridges: true,
455
+ tunnels: true,
456
+ sidewalks: false,
457
+ treeDensity: 0.65,
458
+ lightDensity: 0.05,
459
+ hedges: false,
460
+ driveways: false,
461
+ parking: false,
462
+ fields: false,
463
+ style: "village",
464
+ },
465
+ },
466
+ {
467
+ id: "metropolis",
468
+ label: "Zoned metropolis",
469
+ values: {
470
+ gridness: 0.85,
471
+ curviness: 0.15,
472
+ branching: 0.4,
473
+ blockSize: 48,
474
+ blockAspect: 1.3,
475
+ openSpace: 0.12,
476
+ roadsideOccupancy: 0.92,
477
+ blockDensity: 0.7,
478
+ buildingRoadSetback: 1.5,
479
+ buildingSpacing: 1.2,
480
+ clusterStrength: 0.45,
481
+ streetWidth: 7.5,
482
+ boulevards: 0.4,
483
+ profile: "core-out",
484
+ coreExtent: 0.45,
485
+ midExtent: 0.75,
486
+ floorsMin: 1,
487
+ floorsMax: 34,
488
+ treeDensity: 0.55,
489
+ lightDensity: 0.6,
490
+ style: "generic",
491
+ },
492
+ },
493
+ ],
494
+ };
495
+ /** Zone-override schema for `cityzone` volumes: pin a band and optionally a bespoke class mix. */
496
+ export const CITY_ZONE_SCHEMA = {
497
+ fields: [
498
+ {
499
+ type: "select",
500
+ key: "band",
501
+ label: "zone band",
502
+ default: "core",
503
+ options: [{ value: "core" }, { value: "mid" }, { value: "edge" }],
504
+ },
505
+ { type: "weightedList", key: "mix", label: "class mix", itemLabel: "class", default: [] },
506
+ ],
507
+ };
508
+ /** Parse a volume's `meta` into fully-defaulted city rules. @internal */
509
+ export function readCityRules(meta) {
510
+ const params = parseParams(CITY_SCHEMA, meta);
511
+ const floorsMin = params["floorsMin"];
512
+ const floorsMax = params["floorsMax"];
513
+ // Legacy documents tuned `buildingDensity`; honor it as the occupancy dial when the volume
514
+ // predates the placement params (and only then — presets/authors that set the new keys win).
515
+ const legacyDensity = meta?.["buildingDensity"];
516
+ const hasLegacy = typeof legacyDensity === "number" && Number.isFinite(legacyDensity) && meta?.["roadsideOccupancy"] === undefined;
517
+ return {
518
+ gridness: params["gridness"],
519
+ curviness: params["curviness"],
520
+ branching: params["branching"],
521
+ loopiness: params["loopiness"],
522
+ connectivity: params["connectivity"],
523
+ deadEnds: params["deadEnds"],
524
+ minCurveRadius: params["minCurveRadius"],
525
+ minTurnAngle: params["minTurnAngle"],
526
+ maxTurnAngle: params["maxTurnAngle"],
527
+ fabric: params["fabric"],
528
+ tunnels: params["tunnels"],
529
+ blockSize: params["blockSize"],
530
+ blockAspect: params["blockAspect"],
531
+ openSpace: params["openSpace"],
532
+ roadsideOccupancy: hasLegacy ? Math.max(0, Math.min(1, legacyDensity)) : params["roadsideOccupancy"],
533
+ blockDensity: hasLegacy ? Math.max(0, Math.min(1, legacyDensity * 0.8)) : params["blockDensity"],
534
+ buildingRoadSetback: params["buildingRoadSetback"],
535
+ buildingSpacing: params["buildingSpacing"],
536
+ clusterStrength: params["clusterStrength"],
537
+ streetWidth: params["streetWidth"],
538
+ boulevards: params["boulevards"],
539
+ gravelLanes: params["gravelLanes"],
540
+ surface: params["surface"],
541
+ profile: params["profile"],
542
+ coreExtent: params["coreExtent"],
543
+ midExtent: params["midExtent"],
544
+ coreMix: params["coreMix"],
545
+ midMix: params["midMix"],
546
+ edgeMix: params["edgeMix"],
547
+ lotScale: params["lotScale"],
548
+ floorsMin: Math.min(floorsMin, floorsMax),
549
+ floorsMax: Math.max(floorsMin, floorsMax),
550
+ floorHeight: params["floorHeight"],
551
+ treeDensity: params["treeDensity"],
552
+ treeMix: params["treeMix"],
553
+ lightDensity: params["lightDensity"],
554
+ hedges: params["hedges"],
555
+ driveways: params["driveways"],
556
+ parking: params["parking"],
557
+ fields: params["fields"],
558
+ maxSlope: params["maxSlope"],
559
+ minElevation: params["minElevation"],
560
+ bridges: params["bridges"],
561
+ bridgeStyle: params["bridgeStyle"],
562
+ sidewalks: params["sidewalks"],
563
+ sidewalkWidth: params["sidewalkWidth"],
564
+ style: params["style"],
565
+ seed: params["seed"],
566
+ };
567
+ }
568
+ /** Bounded-work caps so a huge volume can never generate unbounded content. */
569
+ const MAX_STREETS = 320;
570
+ const MAX_LOTS = 2600;
571
+ const MAX_INTERSECTIONS = 600;
572
+ const MAX_TREES = 3200;
573
+ const MAX_LIGHTS = 1200;
574
+ const MAX_FIELD_ROWS = 2400;
575
+ const MAX_PARKING = 380;
576
+ const TAU = Math.PI * 2;
577
+ function axisValue(value) {
578
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
579
+ }
580
+ /** Smooth two-octave sideways wander along a street, deterministic per street. */
581
+ function makeWander(rng, amplitude, wavelength) {
582
+ const f1 = TAU / (wavelength * (0.75 + rng() * 0.5));
583
+ const f2 = f1 * (2.3 + rng() * 0.8);
584
+ const p1 = rng() * TAU;
585
+ const p2 = rng() * TAU;
586
+ return (t) => amplitude * (Math.sin(t * f1 + p1) * 0.65 + Math.sin(t * f2 + p2) * 0.35);
587
+ }
588
+ /**
589
+ * Spatial hash of dense street centerline samples (with per-sample clearance radius). Serves both
590
+ * "is this point on/near a road" lot rejection and branch-to-street connection tests without
591
+ * quadratic polyline scans.
592
+ */
593
+ class StreetIndex {
594
+ cells = new Map();
595
+ cell = 10;
596
+ key(x, z) {
597
+ return `${Math.floor(x / this.cell)}:${Math.floor(z / this.cell)}`;
598
+ }
599
+ addStreet(index, points, width) {
600
+ const r = width / 2;
601
+ for (let i = 0; i + 1 < points.length; i += 1) {
602
+ const [ax, az] = points[i];
603
+ const [bx, bz] = points[i + 1];
604
+ const len = Math.hypot(bx - ax, bz - az);
605
+ const steps = Math.max(1, Math.ceil(len / 3));
606
+ for (let s = 0; s <= steps; s += 1) {
607
+ const t = s / steps;
608
+ const x = ax + (bx - ax) * t;
609
+ const z = az + (bz - az) * t;
610
+ const key = this.key(x, z);
611
+ const bucket = this.cells.get(key);
612
+ const sample = { x, z, r, street: index };
613
+ if (bucket === undefined)
614
+ this.cells.set(key, [sample]);
615
+ else
616
+ bucket.push(sample);
617
+ }
618
+ }
619
+ }
620
+ /** Smallest (distance − clearance) to any street sample near the point; Infinity when clear. */
621
+ clearance(x, z, margin, excludeStreet = -1) {
622
+ const cx = Math.floor(x / this.cell);
623
+ const cz = Math.floor(z / this.cell);
624
+ let worst = Infinity;
625
+ for (let i = cx - 1; i <= cx + 1; i += 1) {
626
+ for (let j = cz - 1; j <= cz + 1; j += 1) {
627
+ const bucket = this.cells.get(`${i}:${j}`);
628
+ if (bucket === undefined)
629
+ continue;
630
+ for (const sample of bucket) {
631
+ if (sample.street === excludeStreet)
632
+ continue;
633
+ const d = Math.hypot(sample.x - x, sample.z - z) - sample.r - margin;
634
+ if (d < worst)
635
+ worst = d;
636
+ }
637
+ }
638
+ }
639
+ return worst;
640
+ }
641
+ /** Nearest centerline sample to a point (expanding ring search), or null when far from roads. */
642
+ nearest(x, z, maxStreet = Infinity) {
643
+ const cx = Math.floor(x / this.cell);
644
+ const cz = Math.floor(z / this.cell);
645
+ for (let radius = 1; radius <= 4; radius += 1) {
646
+ let best = null;
647
+ for (let i = cx - radius; i <= cx + radius; i += 1) {
648
+ for (let j = cz - radius; j <= cz + radius; j += 1) {
649
+ const bucket = this.cells.get(`${i}:${j}`);
650
+ if (bucket === undefined)
651
+ continue;
652
+ for (const sample of bucket) {
653
+ if (sample.street >= maxStreet)
654
+ continue;
655
+ const d = Math.hypot(sample.x - x, sample.z - z);
656
+ if (best === null || d < best.dist)
657
+ best = { x: sample.x, z: sample.z, street: sample.street, dist: d };
658
+ }
659
+ }
660
+ }
661
+ if (best !== null)
662
+ return best;
663
+ }
664
+ return null;
665
+ }
666
+ }
667
+ /** Separating-axis test for two rotated rectangles (center, half-extents, yaw): true when a gap exists. */
668
+ function rectsSeparated(a, b) {
669
+ const axes = [];
670
+ for (const angle of [a.angle, b.angle]) {
671
+ axes.push([Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]);
672
+ }
673
+ const corners = (r) => {
674
+ const c = Math.cos(r.angle);
675
+ const s = Math.sin(r.angle);
676
+ const out = [];
677
+ for (const [dx, dz] of [
678
+ [r.hw, r.hd],
679
+ [r.hw, -r.hd],
680
+ [-r.hw, r.hd],
681
+ [-r.hw, -r.hd],
682
+ ]) {
683
+ out.push([r.x + dx * c + dz * s, r.z - dx * s + dz * c]);
684
+ }
685
+ return out;
686
+ };
687
+ const ca = corners(a);
688
+ const cb = corners(b);
689
+ for (const [ax, az] of axes) {
690
+ let minA = Infinity, maxA = -Infinity, minB = Infinity, maxB = -Infinity;
691
+ for (const [x, z] of ca) {
692
+ const p = x * ax + z * az;
693
+ if (p < minA)
694
+ minA = p;
695
+ if (p > maxA)
696
+ maxA = p;
697
+ }
698
+ for (const [x, z] of cb) {
699
+ const p = x * ax + z * az;
700
+ if (p < minB)
701
+ minB = p;
702
+ if (p > maxB)
703
+ maxB = p;
704
+ }
705
+ if (maxA < minB || maxB < minA)
706
+ return true; // gap on this axis → no overlap
707
+ }
708
+ return false;
709
+ }
710
+ /** Broadphase grid over placed lots so overlap tests only touch neighbors. */
711
+ class LotIndex {
712
+ cells = new Map();
713
+ cell = 24;
714
+ key(x, z) {
715
+ return `${Math.floor(x / this.cell)}:${Math.floor(z / this.cell)}`;
716
+ }
717
+ overlapsAny(lot, gap) {
718
+ const grown = { ...lot, hw: lot.hw + gap / 2, hd: lot.hd + gap / 2 };
719
+ const cx = Math.floor(lot.x / this.cell);
720
+ const cz = Math.floor(lot.z / this.cell);
721
+ for (let i = cx - 1; i <= cx + 1; i += 1) {
722
+ for (let j = cz - 1; j <= cz + 1; j += 1) {
723
+ const bucket = this.cells.get(`${i}:${j}`);
724
+ if (bucket === undefined)
725
+ continue;
726
+ for (const other of bucket) {
727
+ if (!rectsSeparated(grown, other))
728
+ return true;
729
+ }
730
+ }
731
+ }
732
+ return false;
733
+ }
734
+ add(lot) {
735
+ const key = this.key(lot.x, lot.z);
736
+ const bucket = this.cells.get(key);
737
+ if (bucket === undefined)
738
+ this.cells.set(key, [lot]);
739
+ else
740
+ bucket.push(lot);
741
+ }
742
+ }
743
+ const LEVEL_RANK = { boulevard: 3, avenue: 2, street: 1, lane: 0 };
744
+ /**
745
+ * The block-first fabric: extract closed block polygons from the street graph, classify each
746
+ * block (park/plaza/field/buffer/buildable), subdivide buildable block frontage into polygonal
747
+ * parcels, and derive every building from its parcel's buildable polygon. A curved street bounds
748
+ * a curved block; a diagonal one yields diagonal parcels — placement never guesses from the
749
+ * nearest road again.
750
+ */
751
+ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt, overrideAt, junctions) {
752
+ const result = { blocks: [], parcels: [], parks: [], lots: [], placed: new LotIndex(), hedges: [], driveways: [] };
753
+ const bandMix = { core: rules.coreMix, mid: rules.midMix, edge: rules.edgeMix };
754
+ const junctionBoost = (x, z) => {
755
+ let best = Infinity;
756
+ for (const j of junctions) {
757
+ const d = Math.hypot(j.x - x, j.z - z);
758
+ if (d < best)
759
+ best = d;
760
+ }
761
+ return best === Infinity ? 0 : Math.max(0, 1 - best / (rules.blockSize * 0.6));
762
+ };
763
+ const commercialCore = rules.coreMix.some((entry) => (entry.item === "tower" || entry.item === "slab" || entry.item === "shop") && entry.weight > 0);
764
+ const fabric = extractBlocks(streets.map((street) => ({ points: street.points, width: street.width, level: street.level, sidewalk: street.sidewalk })), hx, hz, { streetWidthBase: rules.streetWidth, sidewalkBase: rules.sidewalkWidth, curbMargin: 0.35 });
765
+ const bandAt = (x, z, roll) => {
766
+ const override = overrideAt === null ? null : overrideAt(x, z);
767
+ const band = override !== null ? override.band : zoneBand(zoneMetric(x, z, hx, hz), rules.profile, rules.coreExtent, rules.midExtent, roll);
768
+ const mix = override !== null && override.mix !== null && override.mix.length > 0 ? override.mix : bandMix[band];
769
+ return { band, mix };
770
+ };
771
+ const parkFromRing = (id, type, ring, jitter) => {
772
+ const bounds = ringBounds(ring);
773
+ return {
774
+ id,
775
+ center: [(bounds.minX + bounds.maxX) / 2, (bounds.minZ + bounds.maxZ) / 2],
776
+ size: [Math.max(2, bounds.maxX - bounds.minX), Math.max(2, bounds.maxZ - bounds.minZ)],
777
+ type,
778
+ jitter,
779
+ polygon: ring,
780
+ };
781
+ };
782
+ for (let bi = 0; bi < fabric.blocks.length; bi += 1) {
783
+ const rings = fabric.blocks[bi];
784
+ const rng = streams(`block:${bi}`);
785
+ const bandRoll = rng();
786
+ const openRoll = rng();
787
+ const parkJitter = rng();
788
+ const land = rings.land;
789
+ const blockId = result.blocks.length;
790
+ if (land.length < 3) {
791
+ // The inset consumed the face: whatever pavement-free scrap remains is a landscaped buffer.
792
+ if (rings.curb.length >= 3 && polygonArea(rings.curb) > 30) {
793
+ result.blocks.push({ id: `block:${bi}`, kind: "buffer", polygon: rings.curb, curb: rings.curb, area: polygonArea(rings.curb) });
794
+ result.parks.push(parkFromRing(`park:${bi}`, "buffer", rings.curb, parkJitter));
795
+ }
796
+ continue;
797
+ }
798
+ const area = polygonArea(land);
799
+ const [bcx, bcz] = polygonCentroid(land);
800
+ const { band } = bandAt(bcx, bcz, bandRoll);
801
+ const sliver = isSliverBlock(land, Math.max(90, rules.blockSize * rules.blockSize * 0.028), Math.max(5.5, rules.streetWidth * 0.75));
802
+ if (sliver) {
803
+ result.blocks.push({ id: `block:${bi}`, kind: "buffer", polygon: land, curb: rings.curb, area });
804
+ result.parks.push(parkFromRing(`park:${bi}`, "buffer", land, parkJitter));
805
+ continue;
806
+ }
807
+ let fieldBlock = false;
808
+ if (openRoll < rules.openSpace) {
809
+ // Intentional open-space block. Pocket plazas only in a commercial core, and only pocket-sized.
810
+ const bounds = ringBounds(land);
811
+ const pocket = band === "core" && commercialCore && Math.max(bounds.maxX - bounds.minX, bounds.maxZ - bounds.minZ) <= 52;
812
+ const type = rules.fields && !pocket ? "field" : pocket ? "plaza" : band === "edge" ? "meadow" : "green";
813
+ result.blocks.push({ id: `block:${bi}`, kind: type === "plaza" ? "plaza" : type === "field" ? "field" : "park", polygon: land, curb: rings.curb, area });
814
+ result.parks.push(parkFromRing(`park:${bi}`, type, land, parkJitter));
815
+ // Farm fields never displace homesteads — the farmhouse standing in its own field IS the
816
+ // look, so field blocks still subdivide their road frontage; other park types stay clear.
817
+ if (type !== "field")
818
+ continue;
819
+ fieldBlock = true;
820
+ }
821
+ else {
822
+ result.blocks.push({ id: `block:${bi}`, kind: "buildable", polygon: land, curb: rings.curb, area });
823
+ }
824
+ // --- parcel subdivision along the block's frontage ring ---
825
+ const walker = new RingWalker(land);
826
+ const halfW = Math.max(3, polygonMeanWidth(land) / 2);
827
+ const wave = makeWander(rng, 0.5, rules.blockSize * 2.6);
828
+ const blockParcels = [];
829
+ let cursor = rng() * 4;
830
+ const endStation = walker.total - 2;
831
+ let prevClass = null;
832
+ let maxDepthUsed = 0;
833
+ let guard = 0;
834
+ while (cursor < endStation && blockParcels.length < 64 && result.lots.length < MAX_LOTS && guard < 220) {
835
+ guard += 1;
836
+ const pBandRoll = rng();
837
+ const classRoll = rng();
838
+ const stickRoll = rng();
839
+ const occupancyRoll = rng();
840
+ const lotJitter = rng();
841
+ const lotRng = streams(`lot:${bi}:${Math.round(cursor * 10)}`);
842
+ const mid0 = walker.at(cursor);
843
+ // Only true street frontage grows parcels: an arc along the district rim (or facing a
844
+ // pruned dead end) has no road to face, so the cursor slides past it.
845
+ const curbProbe = [mid0.p[0] - mid0.normal[0] * 2, mid0.p[1] - mid0.normal[1] * 2];
846
+ const near = index.nearest(curbProbe[0], curbProbe[1], streets.length);
847
+ if (near === null || near.dist > streets[near.street].width / 2 + rules.sidewalkWidth * 1.6 + 3) {
848
+ cursor += 6;
849
+ prevClass = null;
850
+ continue;
851
+ }
852
+ const probe = [mid0.p[0] + mid0.normal[0] * 5, mid0.p[1] + mid0.normal[1] * 5];
853
+ const { band: pBand, mix } = bandAt(probe[0], probe[1], pBandRoll);
854
+ let cls = pickClass(mix, classRoll);
855
+ // Sticky class runs: with clustering, a house is followed by more houses — rows form.
856
+ if (prevClass !== null && stickRoll < 0.45 + rules.clusterStrength * 0.4 && mix.some((entry) => entry.item === prevClass && entry.weight > 0)) {
857
+ cls = prevClass;
858
+ }
859
+ const placement = rollClassPlacement(cls, lotRng, rules.lotScale, rules.floorsMin, rules.floorsMax, rules.buildingRoadSetback, rules.buildingSpacing);
860
+ const frontW = placement.width + placement.gap;
861
+ if (cursor + frontW > walker.total - 0.5)
862
+ break;
863
+ // Parcel depth: setback + class depth + rear yard, deepened toward the block spine by
864
+ // `blockDensity` (dense districts consume their block; loose ones keep green cores). The
865
+ // hard cap is a per-station ray cast — parcels from opposite frontages meet halfway.
866
+ const rayOrigin = [mid0.p[0] + mid0.normal[0] * 0.2, mid0.p[1] + mid0.normal[1] * 0.2];
867
+ const across = rayDistanceToRing(land, rayOrigin, mid0.normal);
868
+ const depthCap = Number.isFinite(across) ? Math.max(3, across * 0.5 - 0.3) : halfW;
869
+ const depth = Math.min(depthCap, placement.setback + placement.depth + 2 + rules.blockDensity * Math.max(0, depthCap - placement.depth - placement.setback - 2));
870
+ const s0 = cursor;
871
+ const s1 = cursor + frontW;
872
+ let usedDepth = depth;
873
+ let poly = cutParcel(walker, land, { s0, s1, depth });
874
+ const midS = walker.at((s0 + s1) / 2);
875
+ if (poly !== null && fabric.deadEnds.length > 0) {
876
+ // Cul-de-sac lanes were pruned from the face graph; carve their corridors out of parcels.
877
+ const keep = [midS.p[0] + midS.normal[0] * 1.5, midS.p[1] + midS.normal[1] * 1.5];
878
+ let carved = carveCorridors(poly, keep, fabric.deadEnds.map((d) => ({ pts: d.pts, width: d.width + 1 })), 0.8);
879
+ // Carve chords can cross a concave notch of the block; conform the result again.
880
+ if (carved.length >= 3 && carved.length !== poly.length)
881
+ carved = conformPolygonToRing(carved, land);
882
+ poly = carved.length >= 3 ? carved : null;
883
+ }
884
+ if (poly !== null) {
885
+ // Never overlap a neighbor parcel: retry shallower once, then give the arc up as yard.
886
+ let clash = blockParcels.some((other) => polygonsOverlap(poly, other.polygon, 0.18));
887
+ if (clash) {
888
+ const shallow = cutParcel(walker, land, { s0, s1, depth: depth * 0.55 });
889
+ if (shallow !== null && !blockParcels.some((other) => polygonsOverlap(shallow, other.polygon, 0.18))) {
890
+ poly = shallow;
891
+ usedDepth = depth * 0.55;
892
+ clash = false;
893
+ }
894
+ }
895
+ if (clash)
896
+ poly = null;
897
+ }
898
+ if (poly === null) {
899
+ cursor += frontW * 0.6;
900
+ prevClass = null;
901
+ continue;
902
+ }
903
+ const A = walker.at(s0).p;
904
+ const B = walker.at(s1).p;
905
+ const tangent = (() => {
906
+ const tx = B[0] - A[0];
907
+ const tz = B[1] - A[1];
908
+ const l = Math.hypot(tx, tz) || 1;
909
+ return [tx / l, tz / l];
910
+ })();
911
+ let isCorner = false;
912
+ for (const vi of walker.verticesBetween(s0, s1)) {
913
+ const angle = walker.interiorAngle(vi);
914
+ if (angle < 2.53 || angle > Math.PI * 2 - 2.53) {
915
+ isCorner = true;
916
+ break;
917
+ }
918
+ }
919
+ const frontProbe = [midS.p[0] - midS.normal[0] * 2, midS.p[1] - midS.normal[1] * 2];
920
+ const nearestStreet = index.nearest(frontProbe[0], frontProbe[1], streets.length);
921
+ const frontage = nearestStreet === null ? [] : [{ street: nearestStreet.street, a: A, b: B, tangent }];
922
+ const parcelId = `parcel:${bi}:${blockParcels.length}`;
923
+ const parcel = {
924
+ id: parcelId,
925
+ block: blockId,
926
+ polygon: poly,
927
+ buildable: [],
928
+ frontage,
929
+ area: polygonArea(poly),
930
+ depth: usedDepth,
931
+ isCorner,
932
+ kind: "vacant",
933
+ };
934
+ // Local occupancy: the district dial, drifted by the smooth wave and boosted near
935
+ // junctions so intersections read as centers of development.
936
+ const local = rules.roadsideOccupancy * (1 - rules.clusterStrength * 0.3) +
937
+ rules.clusterStrength * (wave(cursor) + 0.5) * 0.22 +
938
+ rules.clusterStrength * junctionBoost(midS.p[0], midS.p[1]) * 0.6;
939
+ if (occupancyRoll > local) {
940
+ blockParcels.push(parcel);
941
+ cursor += frontW;
942
+ prevClass = null;
943
+ continue;
944
+ }
945
+ const sideGap = Math.max(0.3, placement.gap / 2);
946
+ const buildable = buildablePolygon(poly, A, B, placement.setback, sideGap, 1.2, usedDepth);
947
+ parcel.buildable = buildable;
948
+ let placedLot = false;
949
+ if (buildable.length >= 3) {
950
+ const outDir = [-midS.normal[0], -midS.normal[1]];
951
+ const rotationY = Math.atan2(outDir[0], outDir[1]);
952
+ const maxW = Math.max(3, Math.min(placement.width, frontW - placement.gap));
953
+ const availDepth = usedDepth - placement.setback - 1.2;
954
+ const maxD = Math.max(3, Math.min(placement.depth, availDepth));
955
+ const cx0 = midS.p[0] + midS.normal[0] * (placement.setback + maxD / 2);
956
+ const cz0 = midS.p[1] + midS.normal[1] * (placement.setback + maxD / 2);
957
+ const fit = fitRectInPolygon(buildable, cx0, cz0, maxW, maxD, rotationY, 0.45);
958
+ if (fit !== null && fit.w >= 3 && fit.d >= 2.6) {
959
+ const slopeRoll = rng();
960
+ const hw = fit.w / 2;
961
+ const hd = fit.d / 2;
962
+ const ca = Math.cos(rotationY);
963
+ const sa = Math.sin(rotationY);
964
+ let ok = true;
965
+ if (slopeAt !== null && slopeAt(fit.cx, fit.cz) > rules.maxSlope * (0.85 + slopeRoll * 0.3))
966
+ ok = false;
967
+ if (ok && heightAt !== null && heightAt(fit.cx, fit.cz) < rules.minElevation)
968
+ ok = false;
969
+ // Belt and braces against pavement (bridge approaches, cul-de-sac bulbs): the footprint
970
+ // corners keep clearance to every street centerline.
971
+ if (ok && index.clearance(fit.cx, fit.cz, 1) < 0)
972
+ ok = false;
973
+ if (ok) {
974
+ for (const [dx, dz] of [
975
+ [hw, hd],
976
+ [hw, -hd],
977
+ [-hw, hd],
978
+ [-hw, -hd],
979
+ ]) {
980
+ if (index.clearance(fit.cx + dx * ca + dz * sa, fit.cz - dx * sa + dz * ca, 0.4) < 0) {
981
+ ok = false;
982
+ break;
983
+ }
984
+ }
985
+ }
986
+ const candidate = { x: fit.cx, z: fit.cz, hw, hd, angle: rotationY };
987
+ if (ok && !result.placed.overlapsAny(candidate, 0.8)) {
988
+ result.placed.add(candidate);
989
+ const pieces = buildLotPieces(cls, fit.w, fit.d, placement.floors, rules.floorHeight, lotRng);
990
+ const anchor = nearestStreet === null ? [frontProbe[0], frontProbe[1]] : [nearestStreet.x, nearestStreet.z];
991
+ result.lots.push({
992
+ id: `lot:${bi}:${result.lots.length}`,
993
+ center: [fit.cx, fit.cz],
994
+ size: [fit.w, fit.d],
995
+ rotationY,
996
+ floors: placement.floors,
997
+ jitter: lotJitter,
998
+ cls,
999
+ zone: pBand,
1000
+ anchor,
1001
+ pieces,
1002
+ parcel: parcelId,
1003
+ });
1004
+ parcel.kind = "built";
1005
+ placedLot = true;
1006
+ if (rules.driveways && (cls === "house" || cls === "mansion" || cls === "farmhouse" || cls === "barn" || cls === "silo")) {
1007
+ const dirX = fit.cx - anchor[0];
1008
+ const dirZ = fit.cz - anchor[1];
1009
+ const len = Math.hypot(dirX, dirZ) || 1;
1010
+ const ux = dirX / len;
1011
+ const uz = dirZ / len;
1012
+ const streetWidth = nearestStreet === null ? rules.streetWidth : streets[nearestStreet.street].width;
1013
+ const start = [anchor[0] + ux * (streetWidth / 2 - 0.4), anchor[1] + uz * (streetWidth / 2 - 0.4)];
1014
+ const end = [fit.cx - ux * (hd - 1), fit.cz - uz * (hd - 1)];
1015
+ const surface = cls === "farmhouse" || cls === "barn" || cls === "silo" || rules.gravelLanes ? "gravel" : "pavement";
1016
+ result.driveways.push({ points: [start, end], width: cls === "mansion" ? 3.2 : 2.6, surface });
1017
+ }
1018
+ if (rules.hedges && cls === "mansion") {
1019
+ // Perimeter hedge with a gated gap centered on the driveway mouth (street face).
1020
+ const m = 2.4;
1021
+ const hhw = hw + m;
1022
+ const hhd = hd + m;
1023
+ const th = 0.7;
1024
+ const gap = 2.2;
1025
+ const runs = [
1026
+ { x: -hhw / 2 - gap / 2, z: hhd, len: hhw - gap, rot: 0 },
1027
+ { x: hhw / 2 + gap / 2, z: hhd, len: hhw - gap, rot: 0 },
1028
+ { x: 0, z: -hhd, len: hhw * 2, rot: 0 },
1029
+ { x: hhw, z: 0, len: hhd * 2, rot: Math.PI / 2 },
1030
+ { x: -hhw, z: 0, len: hhd * 2, rot: Math.PI / 2 },
1031
+ ];
1032
+ for (const run of runs) {
1033
+ const wx = fit.cx + run.x * ca + run.z * sa;
1034
+ const wz = fit.cz - run.x * sa + run.z * ca;
1035
+ result.hedges.push({ center: [wx, wz], size: [run.len, th], rotationY: rotationY + run.rot });
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ if (!placedLot && parcel.kind === "vacant")
1042
+ parcel.kind = "yard";
1043
+ blockParcels.push(parcel);
1044
+ if (placedLot)
1045
+ maxDepthUsed = Math.max(maxDepthUsed, usedDepth);
1046
+ // A pushed parcel owns its whole frontage arc — the cursor never re-enters it.
1047
+ cursor += frontW;
1048
+ prevClass = placedLot ? cls : null;
1049
+ }
1050
+ result.parcels.push(...blockParcels);
1051
+ if (fieldBlock)
1052
+ continue; // the field polygon already owns the interior
1053
+ if (blockParcels.length === 0) {
1054
+ // Nothing subdivided (tiny ring): the block is intentional green, not leftover.
1055
+ result.blocks[blockId].kind = "park";
1056
+ result.parks.push(parkFromRing(`park:${bi}`, band === "edge" ? "meadow" : "green", land, parkJitter));
1057
+ continue;
1058
+ }
1059
+ // Block interior left behind the deepest parcels: classify it (courtyard downtown, green
1060
+ // elsewhere) when it is big enough to read as a place rather than back yards.
1061
+ if (maxDepthUsed > 0) {
1062
+ const core = insetRingUniform(land, maxDepthUsed + 0.5);
1063
+ if (core.length >= 3 && polygonArea(core) > 240) {
1064
+ const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.5 ? "courtyard" : "green") : "meadow";
1065
+ result.parks.push(parkFromRing(`court:${bi}`, type, core, parkJitter));
1066
+ }
1067
+ }
1068
+ }
1069
+ return result;
1070
+ }
1071
+ /** Crop rows for field parks, clipped to the park's road-derived polygon and around placed lots. */
1072
+ function buildFieldRows(parks, placed, streams) {
1073
+ let total = 0;
1074
+ const rng = streams("fields");
1075
+ for (const park of parks) {
1076
+ if (park.type !== "field")
1077
+ continue;
1078
+ const rows = [];
1079
+ const alongX = park.size[0] >= park.size[1];
1080
+ const runLength = alongX ? park.size[0] : park.size[1];
1081
+ const across = alongX ? park.size[1] : park.size[0];
1082
+ const spacing = 3.2 + rng() * 0.8;
1083
+ const count = Math.floor((across - 3) / spacing);
1084
+ for (let r = 0; r < count && total < MAX_FIELD_ROWS; r += 1) {
1085
+ const offset = -across / 2 + 2 + r * spacing;
1086
+ let current = [];
1087
+ const steps = Math.max(2, Math.ceil(runLength / 4));
1088
+ for (let sIdx = 0; sIdx <= steps; sIdx += 1) {
1089
+ const along = -runLength / 2 + 2 + ((runLength - 4) * sIdx) / steps;
1090
+ const x = park.center[0] + (alongX ? along : offset);
1091
+ const z = park.center[1] + (alongX ? offset : along);
1092
+ // Rows follow the field's road-derived polygon: a curved block edge ends the row early.
1093
+ const outside = park.polygon !== undefined && !pointInPolygon(park.polygon, x, z);
1094
+ const blocked = outside || placed.overlapsAny({ x, z, hw: 1.2, hd: 1.2, angle: 0 }, 1.5);
1095
+ if (blocked) {
1096
+ if (current.length >= 2)
1097
+ rows.push(current);
1098
+ current = [];
1099
+ }
1100
+ else {
1101
+ current.push([x, z]);
1102
+ }
1103
+ }
1104
+ if (current.length >= 2)
1105
+ rows.push(current);
1106
+ total += 1;
1107
+ }
1108
+ if (rows.length > 0)
1109
+ park.rows = rows;
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Synthesize the deterministic city plan for one `city` volume: streets → bridges → parks → zoned
1114
+ * frontage lots with massing pieces → furniture, all in the volume's local frame and then
1115
+ * rotated/translated into world space. Same volume (id, footprint, meta) over the same terrain
1116
+ * always resolves to the identical plan. When `context` provides a ground sampler, lots respect
1117
+ * the `maxSlope` cliff rule — hillside and canyon districts keep their steep faces open. When
1118
+ * `context.zoneOverrides` carries sibling `cityzone` volumes, lots inside them adopt the override
1119
+ * band/mix. Returns null without a usable footprint.
1120
+ *
1121
+ * @capability city-district resolve a `city` volume into deterministic streets, zoned lots, parks, and furniture
1122
+ */
1123
+ export function resolveCityObject(object, context) {
1124
+ const center = object.center ?? object.position;
1125
+ if (center === undefined)
1126
+ return null;
1127
+ const he = object.halfExtents;
1128
+ const hx = axisValue(he?.x) ?? object.radius ?? 0;
1129
+ const hz = axisValue(he?.z) ?? object.radius ?? 0;
1130
+ const rules = readCityRules(object.meta);
1131
+ const rotationY = object.rotationY ?? 0;
1132
+ const empty = {
1133
+ center: [center.x, center.y, center.z],
1134
+ size: [hx * 2, hz * 2],
1135
+ rotationY,
1136
+ rules,
1137
+ streets: [],
1138
+ intersections: [],
1139
+ bridges: [],
1140
+ tunnels: [],
1141
+ blocks: [],
1142
+ parcels: [],
1143
+ lots: [],
1144
+ parks: [],
1145
+ trees: [],
1146
+ lights: [],
1147
+ hedges: [],
1148
+ driveways: [],
1149
+ parkingLots: [],
1150
+ };
1151
+ if (hx < rules.blockSize * 0.75 || hz < rules.blockSize * 0.75)
1152
+ return empty;
1153
+ const cos = Math.cos(rotationY);
1154
+ const sin = Math.sin(rotationY);
1155
+ const toWorld = (x, z) => [center.x + x * cos + z * sin, center.z - x * sin + z * cos];
1156
+ const sampleHeight = context?.sampleHeight;
1157
+ const heightAt = sampleHeight === undefined
1158
+ ? null
1159
+ : (x, z) => {
1160
+ const [wx, wz] = toWorld(x, z);
1161
+ return sampleHeight(wx, wz);
1162
+ };
1163
+ const slopeAt = sampleHeight === undefined
1164
+ ? null
1165
+ : (x, z) => {
1166
+ const [wx, wz] = toWorld(x, z);
1167
+ const d = 4;
1168
+ const dx = sampleHeight(wx + d, wz) - sampleHeight(wx - d, wz);
1169
+ const dz = sampleHeight(wx, wz + d) - sampleHeight(wx, wz - d);
1170
+ return Math.hypot(dx, dz) / (2 * d);
1171
+ };
1172
+ // Child `cityzone` volumes override the band (and optionally the class mix) for lots inside them.
1173
+ const overrides = (context?.zoneOverrides ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
1174
+ const overrideAt = overrides.length === 0
1175
+ ? null
1176
+ : (x, z) => {
1177
+ const [wx, wz] = toWorld(x, z);
1178
+ for (const volume of overrides) {
1179
+ const vc = volume.center ?? volume.position;
1180
+ if (vc === undefined)
1181
+ continue;
1182
+ const vhx = axisValue(volume.halfExtents?.x) ?? volume.radius ?? 0;
1183
+ const vhz = axisValue(volume.halfExtents?.z) ?? volume.radius ?? 0;
1184
+ if (vhx <= 0 || vhz <= 0)
1185
+ continue;
1186
+ const yaw = volume.rotationY ?? 0;
1187
+ const dx = wx - vc.x;
1188
+ const dz = wz - vc.z;
1189
+ const lx = dx * Math.cos(yaw) - dz * Math.sin(yaw);
1190
+ const lz = dx * Math.sin(yaw) + dz * Math.cos(yaw);
1191
+ if (Math.abs(lx) > vhx || Math.abs(lz) > vhz)
1192
+ continue;
1193
+ const params = parseParams(CITY_ZONE_SCHEMA, volume.meta);
1194
+ const mix = params["mix"];
1195
+ return { band: params["band"], mix: mix.length > 0 ? mix : null };
1196
+ }
1197
+ return null;
1198
+ };
1199
+ const streams = seededStreams(`city:${rules.seed.length > 0 ? rules.seed : object.id}`);
1200
+ // The unified path-network engine grows the whole road/track graph from the sliders — a city street
1201
+ // net and a closed race circuit are the SAME generator at opposite slider extremes.
1202
+ const netRules = {
1203
+ seed: rules.seed.length > 0 ? rules.seed : object.id,
1204
+ gridness: rules.gridness,
1205
+ loopiness: rules.loopiness,
1206
+ connectivity: rules.connectivity,
1207
+ branching: rules.branching,
1208
+ deadEnds: rules.deadEnds,
1209
+ segmentLength: rules.blockSize,
1210
+ aspect: rules.blockAspect,
1211
+ winding: rules.curviness,
1212
+ minCurveRadius: rules.minCurveRadius,
1213
+ minTurnAngle: rules.minTurnAngle,
1214
+ maxTurnAngle: rules.maxTurnAngle,
1215
+ width: rules.streetWidth,
1216
+ boulevards: rules.boulevards,
1217
+ };
1218
+ const network = buildPathNetwork(netRules, hx, hz, {
1219
+ heightAt: heightAt ?? undefined,
1220
+ minElevation: rules.minElevation,
1221
+ bridges: rules.bridges,
1222
+ tunnels: rules.tunnels,
1223
+ });
1224
+ // Resolve each generated street's surface + sidewalk from its hierarchy level.
1225
+ const surfaceFor = (level) => rules.surface === "gravel" || (level === "lane" && rules.gravelLanes) ? "gravel" : "asphalt";
1226
+ const local = network.streets.slice(0, MAX_STREETS).map((street) => {
1227
+ const surface = surfaceFor(street.level);
1228
+ return {
1229
+ points: street.points.map(([x, z]) => [x, z]),
1230
+ width: street.width,
1231
+ level: street.level,
1232
+ surface,
1233
+ sidewalk: rules.sidewalks && surface === "asphalt",
1234
+ ...(street.bulb === undefined ? {} : { bulb: [street.bulb[0], street.bulb[1]] }),
1235
+ ...(street.features.length === 0 ? {} : { features: street.features }),
1236
+ };
1237
+ });
1238
+ const index = new StreetIndex();
1239
+ local.forEach((street, i) => index.addStreet(i, street.points, street.width));
1240
+ const intersections = network.junctions
1241
+ .slice(0, MAX_INTERSECTIONS)
1242
+ .map((j) => ({ x: j.x, z: j.z, radius: j.radius, level: j.level, arms: j.arms.map((arm) => ({ ...arm })) }));
1243
+ const localBridges = network.bridges.map((b) => ({
1244
+ points: b.points.map(([x, z]) => [x, z]),
1245
+ width: b.width,
1246
+ bankHeight: b.bankHeight,
1247
+ }));
1248
+ const localTunnels = network.tunnels.map((t) => ({
1249
+ points: t.points.map(([x, z]) => [x, z]),
1250
+ width: t.width,
1251
+ bankHeight: t.bankHeight,
1252
+ }));
1253
+ // Fabric (block/parcel/building subdivision) is optional: a bare road or race track leaves it off,
1254
+ // so a circuit never carries forced-on plots — parcels and buildings appear only when you want it.
1255
+ const emptyFabric = { blocks: [], parcels: [], parks: [], lots: [], placed: new LotIndex(), hedges: [], driveways: [] };
1256
+ const lotResult = rules.fabric
1257
+ ? buildFabric(rules, streams, local, index, hx, hz, slopeAt, heightAt, overrideAt, intersections)
1258
+ : emptyFabric;
1259
+ const parks = lotResult.parks;
1260
+ if (rules.fabric)
1261
+ buildFieldRows(parks, lotResult.placed, streams);
1262
+ // --- Street furniture: bounded, seeded, and hooked to the network. ---
1263
+ const trees = [];
1264
+ const lights = [];
1265
+ const nearIntersection = (x, z, pad) => {
1266
+ for (const cross of intersections) {
1267
+ if (Math.hypot(cross.x - x, cross.z - z) < cross.radius + pad)
1268
+ return true;
1269
+ }
1270
+ return false;
1271
+ };
1272
+ const insideBounds = (x, z) => Math.abs(x) < hx - 1.5 && Math.abs(z) < hz - 1.5;
1273
+ const asDescriptor = (street) => ({
1274
+ kind: "road",
1275
+ path: street.points,
1276
+ width: street.width,
1277
+ color: "",
1278
+ markings: false,
1279
+ markingColor: "",
1280
+ elevation: 0,
1281
+ sidewalk: street.sidewalk ? { width: 1.9, color: "" } : false,
1282
+ });
1283
+ if (rules.lightDensity > 0) {
1284
+ const lightRng = streams("lights");
1285
+ // Density drives pole spacing directly (0.1 → a pole every ~120 m; 1 → every 18 m), and at low
1286
+ // densities only the avenue-and-up hierarchy is lit — a farm road keeps its dark sky.
1287
+ const spacing = Math.min(120, Math.max(14, 18 / rules.lightDensity));
1288
+ for (const street of local) {
1289
+ if (street.level === "lane" || street.surface === "gravel")
1290
+ continue;
1291
+ if (rules.lightDensity < 0.3 && LEVEL_RANK[street.level] < LEVEL_RANK.avenue)
1292
+ continue;
1293
+ const spots = furnitureSpots(asDescriptor(street), { spacing, outset: street.sidewalk ? 0.6 : 0.2 });
1294
+ for (const spot of spots) {
1295
+ if (lights.length >= MAX_LIGHTS)
1296
+ break;
1297
+ const [x, z] = spot.position;
1298
+ if (!insideBounds(x, z) || nearIntersection(x, z, 1.5))
1299
+ continue;
1300
+ if (index.clearance(x, z, 1.0) < 0)
1301
+ continue;
1302
+ if (lightRng() > 0.95)
1303
+ continue; // occasional missing pole so rows never read stamped
1304
+ // Lamp arm faces back over the road: flip the outward-facing spot heading.
1305
+ lights.push({ x, z, heading: spot.heading + Math.PI });
1306
+ }
1307
+ }
1308
+ }
1309
+ if (rules.treeDensity > 0) {
1310
+ const treeRng = streams("trees");
1311
+ const spacing = 30 - 21 * rules.treeDensity;
1312
+ for (const street of local) {
1313
+ if (street.surface === "gravel" && rules.treeDensity < 0.6)
1314
+ continue;
1315
+ const spots = furnitureSpots(asDescriptor(street), { spacing, stagger: false, outset: street.sidewalk ? 2.6 : 2.0 });
1316
+ for (const spot of spots) {
1317
+ if (trees.length >= MAX_TREES)
1318
+ break;
1319
+ if (treeRng() > rules.treeDensity * 0.85 + 0.15)
1320
+ continue;
1321
+ const x = spot.position[0] + (treeRng() - 0.5) * 1.2;
1322
+ const z = spot.position[1] + (treeRng() - 0.5) * 1.2;
1323
+ // Keep canopies out of the crossing sight-triangle and off the carriageway edge.
1324
+ if (!insideBounds(x, z) || nearIntersection(x, z, 5))
1325
+ continue;
1326
+ if (index.clearance(x, z, 2.6) < 0)
1327
+ continue;
1328
+ if (lotResult.placed.overlapsAny({ x, z, hw: 0.5, hd: 0.5, angle: 0 }, 0.5))
1329
+ continue;
1330
+ trees.push({ x, z, species: pickSpecies(rules.treeMix, treeRng()), scale: 0.85 + treeRng() * 0.5, jitter: treeRng() });
1331
+ }
1332
+ }
1333
+ // Park planting: formal corners on plazas, loose clusters on greens/courtyards, sparse
1334
+ // meadows and buffers — all sampled inside the park's road-derived polygon.
1335
+ const parkRng = streams("parkTrees");
1336
+ for (const park of parks) {
1337
+ if (trees.length >= MAX_TREES)
1338
+ break;
1339
+ const parkArea = park.polygon !== undefined ? polygonArea(park.polygon) : park.size[0] * park.size[1];
1340
+ const count = park.type === "plaza"
1341
+ ? 4
1342
+ : park.type === "green" || park.type === "courtyard"
1343
+ ? Math.min(10, Math.max(3, Math.floor(parkArea / 260)))
1344
+ : park.type === "meadow"
1345
+ ? 2 + Math.floor(parkRng() * 4)
1346
+ : park.type === "buffer"
1347
+ ? Math.min(4, Math.floor(parkArea / 320))
1348
+ : 0;
1349
+ if (count <= 0)
1350
+ continue;
1351
+ let spots;
1352
+ if (park.type === "plaza") {
1353
+ spots = [
1354
+ [park.center[0] - park.size[0] * 0.36, park.center[1] - park.size[1] * 0.36],
1355
+ [park.center[0] + park.size[0] * 0.36, park.center[1] - park.size[1] * 0.36],
1356
+ [park.center[0] - park.size[0] * 0.36, park.center[1] + park.size[1] * 0.36],
1357
+ [park.center[0] + park.size[0] * 0.36, park.center[1] + park.size[1] * 0.36],
1358
+ ].filter(([x, z]) => park.polygon === undefined || pointInPolygon(park.polygon, x, z));
1359
+ }
1360
+ else if (park.polygon !== undefined) {
1361
+ spots = pointsInPolygon(park.polygon, count, parkRng, 1.2);
1362
+ }
1363
+ else {
1364
+ const random = [];
1365
+ for (let i = 0; i < count; i += 1) {
1366
+ random.push([park.center[0] + (parkRng() - 0.5) * park.size[0] * 0.8, park.center[1] + (parkRng() - 0.5) * park.size[1] * 0.8]);
1367
+ }
1368
+ spots = random;
1369
+ }
1370
+ for (const [x, z] of spots) {
1371
+ if (trees.length >= MAX_TREES)
1372
+ break;
1373
+ if (!insideBounds(x, z))
1374
+ continue;
1375
+ if (index.clearance(x, z, 0.4) < 0)
1376
+ continue;
1377
+ if (lotResult.placed.overlapsAny({ x, z, hw: 0.5, hd: 0.5, angle: 0 }, 0.5))
1378
+ continue;
1379
+ trees.push({ x, z, species: pickSpecies(rules.treeMix, parkRng()), scale: 0.9 + parkRng() * 0.6, jitter: parkRng() });
1380
+ }
1381
+ }
1382
+ }
1383
+ // Parking pads behind commercial lots, collision-checked against every placed footprint.
1384
+ const parking = [];
1385
+ if (rules.parking) {
1386
+ const parkingRng = streams("parking");
1387
+ const padIndex = new LotIndex();
1388
+ for (const lot of lotResult.lots) {
1389
+ if (parking.length >= MAX_PARKING)
1390
+ break;
1391
+ if (lot.cls !== "shop" && lot.cls !== "slab")
1392
+ continue;
1393
+ if (parkingRng() > 0.6)
1394
+ continue;
1395
+ const depth = 9 + parkingRng() * 5;
1396
+ const ca = Math.cos(lot.rotationY);
1397
+ const sa = Math.sin(lot.rotationY);
1398
+ // Behind the lot: away from the street face (-z in lot-local space).
1399
+ const off = lot.size[1] / 2 + depth / 2 + 1.2;
1400
+ const x = lot.center[0] - sa * off;
1401
+ const z = lot.center[1] - ca * off;
1402
+ const pad = { x, z, hw: (lot.size[0] * 1.05) / 2, hd: depth / 2, angle: lot.rotationY };
1403
+ if (Math.abs(x) > hx - 3 || Math.abs(z) > hz - 3)
1404
+ continue;
1405
+ if (index.clearance(x, z, 0.5) < 0)
1406
+ continue;
1407
+ if (lotResult.placed.overlapsAny(pad, 0.5) || padIndex.overlapsAny(pad, 0.5))
1408
+ continue;
1409
+ padIndex.add(pad);
1410
+ parking.push({ center: [x, z], size: [lot.size[0] * 1.05, depth], rotationY: lot.rotationY });
1411
+ }
1412
+ }
1413
+ return {
1414
+ center: [center.x, center.y, center.z],
1415
+ size: [hx * 2, hz * 2],
1416
+ rotationY,
1417
+ rules,
1418
+ streets: local.map((street, i) => ({
1419
+ id: `street:${i}`,
1420
+ points: street.points.map(([x, z]) => toWorld(x, z)),
1421
+ width: street.width,
1422
+ level: street.level,
1423
+ surface: street.surface,
1424
+ sidewalk: street.sidewalk,
1425
+ ...(street.bulb === undefined ? {} : { bulb: toWorld(street.bulb[0], street.bulb[1]) }),
1426
+ ...(street.features === undefined ? {} : { features: street.features.map((f) => ({ ...f })) }),
1427
+ })),
1428
+ intersections: intersections.map((cross, i) => {
1429
+ const [x, z] = toWorld(cross.x, cross.z);
1430
+ return {
1431
+ id: `cross:${i}`,
1432
+ x,
1433
+ z,
1434
+ radius: cross.radius,
1435
+ level: cross.level,
1436
+ arms: cross.arms.map((arm) => ({ angle: arm.angle - rotationY, width: arm.width })),
1437
+ };
1438
+ }),
1439
+ bridges: localBridges.map((bridge, i) => ({
1440
+ id: `bridge:${i}`,
1441
+ points: bridge.points.map(([x, z]) => toWorld(x, z)),
1442
+ width: bridge.width,
1443
+ style: rules.bridgeStyle,
1444
+ bankHeight: bridge.bankHeight,
1445
+ })),
1446
+ tunnels: localTunnels.map((tunnel, i) => ({
1447
+ id: `tunnel:${i}`,
1448
+ points: tunnel.points.map(([x, z]) => toWorld(x, z)),
1449
+ width: tunnel.width,
1450
+ bankHeight: tunnel.bankHeight,
1451
+ })),
1452
+ blocks: lotResult.blocks.map((block) => ({
1453
+ id: block.id,
1454
+ kind: block.kind,
1455
+ polygon: block.polygon.map(([x, z]) => toWorld(x, z)),
1456
+ curb: block.curb.map(([x, z]) => toWorld(x, z)),
1457
+ area: block.area,
1458
+ })),
1459
+ parcels: lotResult.parcels.map((parcel) => ({
1460
+ id: parcel.id,
1461
+ block: lotResult.blocks[parcel.block]?.id ?? `block:${parcel.block}`,
1462
+ polygon: parcel.polygon.map(([x, z]) => toWorld(x, z)),
1463
+ buildable: parcel.buildable.map(([x, z]) => toWorld(x, z)),
1464
+ frontage: parcel.frontage.map((front) => ({
1465
+ road: `street:${front.street}`,
1466
+ edgeStart: toWorld(front.a[0], front.a[1]),
1467
+ edgeEnd: toWorld(front.b[0], front.b[1]),
1468
+ tangent: [
1469
+ front.tangent[0] * Math.cos(rotationY) + front.tangent[1] * Math.sin(rotationY),
1470
+ -front.tangent[0] * Math.sin(rotationY) + front.tangent[1] * Math.cos(rotationY),
1471
+ ],
1472
+ })),
1473
+ area: parcel.area,
1474
+ depth: parcel.depth,
1475
+ isCorner: parcel.isCorner,
1476
+ kind: parcel.kind,
1477
+ })),
1478
+ lots: lotResult.lots.map((lot) => ({
1479
+ id: lot.id,
1480
+ center: toWorld(lot.center[0], lot.center[1]),
1481
+ size: lot.size,
1482
+ rotationY: lot.rotationY - rotationY,
1483
+ floors: lot.floors,
1484
+ jitter: lot.jitter,
1485
+ class: lot.cls,
1486
+ zone: lot.zone,
1487
+ anchor: toWorld(lot.anchor[0], lot.anchor[1]),
1488
+ pieces: lot.pieces,
1489
+ parcel: lot.parcel,
1490
+ })),
1491
+ parks: parks.map((park) => ({
1492
+ id: park.id,
1493
+ center: toWorld(park.center[0], park.center[1]),
1494
+ size: park.size,
1495
+ rotationY: -rotationY,
1496
+ type: park.type,
1497
+ jitter: park.jitter,
1498
+ ...(park.polygon === undefined ? {} : { polygon: park.polygon.map(([x, z]) => toWorld(x, z)) }),
1499
+ ...(park.rows === undefined ? {} : { rows: park.rows.map((row) => row.map(([x, z]) => toWorld(x, z))) }),
1500
+ })),
1501
+ trees: trees.map((tree) => {
1502
+ const [x, z] = toWorld(tree.x, tree.z);
1503
+ return { x, z, species: tree.species, scale: tree.scale, jitter: tree.jitter };
1504
+ }),
1505
+ lights: lights.map((light) => {
1506
+ const [x, z] = toWorld(light.x, light.z);
1507
+ return { x, z, heading: light.heading - rotationY };
1508
+ }),
1509
+ hedges: lotResult.hedges.map((hedge) => {
1510
+ const [x, z] = toWorld(hedge.center[0], hedge.center[1]);
1511
+ return { center: [x, z], size: hedge.size, rotationY: hedge.rotationY - rotationY };
1512
+ }),
1513
+ driveways: lotResult.driveways.map((drive) => ({
1514
+ points: drive.points.map(([x, z]) => toWorld(x, z)),
1515
+ width: drive.width,
1516
+ surface: drive.surface,
1517
+ })),
1518
+ parkingLots: parking.map((pad) => {
1519
+ const [x, z] = toWorld(pad.center[0], pad.center[1]);
1520
+ return { center: [x, z], size: pad.size, rotationY: pad.rotationY - rotationY };
1521
+ }),
1522
+ };
1523
+ }
1524
+ /** Registers the `city` + `cityzone` scene kinds (schema + resolver). Called by {@link registerBuiltinSceneKinds}. @internal */
1525
+ export function registerCityKind() {
1526
+ registerSceneKind({
1527
+ kind: CITY_KIND,
1528
+ target: "volume",
1529
+ label: "City / track",
1530
+ addCategory: "Studios",
1531
+ accent: "#8fa8c9",
1532
+ schema: CITY_SCHEMA,
1533
+ resolve: (object, _params, context) => resolveCityObject(object, context),
1534
+ note: (object) => {
1535
+ const resolved = resolveCityObject(object);
1536
+ if (resolved === null)
1537
+ return "Give the volume a box footprint.";
1538
+ if (resolved.streets.length === 0)
1539
+ return `Footprint too small — needs at least ${Math.ceil(resolved.rules.blockSize * 1.5)} m across.`;
1540
+ const mode = pathNetworkMode({
1541
+ seed: resolved.rules.seed,
1542
+ gridness: resolved.rules.gridness,
1543
+ loopiness: resolved.rules.loopiness,
1544
+ connectivity: resolved.rules.connectivity,
1545
+ branching: resolved.rules.branching,
1546
+ deadEnds: resolved.rules.deadEnds,
1547
+ segmentLength: resolved.rules.blockSize,
1548
+ aspect: resolved.rules.blockAspect,
1549
+ winding: resolved.rules.curviness,
1550
+ minCurveRadius: resolved.rules.minCurveRadius,
1551
+ minTurnAngle: resolved.rules.minTurnAngle,
1552
+ maxTurnAngle: resolved.rules.maxTurnAngle,
1553
+ width: resolved.rules.streetWidth,
1554
+ boulevards: resolved.rules.boulevards,
1555
+ });
1556
+ const feats = [
1557
+ resolved.bridges.length > 0 ? `${resolved.bridges.length} bridges` : "",
1558
+ resolved.tunnels.length > 0 ? `${resolved.tunnels.length} tunnels` : "",
1559
+ ].filter((s) => s.length > 0);
1560
+ const head = mode === "circuit" ? `circuit · ${resolved.streets.length} streets` : `${resolved.streets.length} streets`;
1561
+ const fabric = resolved.rules.fabric ? ` · ${resolved.lots.length} buildings · ${resolved.parks.length} parks` : " · no fabric";
1562
+ return `${head}${fabric}${feats.length > 0 ? ` · ${feats.join(" · ")}` : ""}`;
1563
+ },
1564
+ });
1565
+ registerSceneKind({
1566
+ kind: CITY_ZONE_KIND,
1567
+ target: "volume",
1568
+ label: "City zone override",
1569
+ addCategory: "Studios",
1570
+ accent: "#c9b88f",
1571
+ schema: CITY_ZONE_SCHEMA,
1572
+ note: (object) => {
1573
+ const params = parseParams(CITY_ZONE_SCHEMA, object.meta);
1574
+ const mix = params["mix"];
1575
+ return `Pins the "${params["band"]}" band${mix.length > 0 ? " with a custom mix" : ""} inside any city district it overlaps.`;
1576
+ },
1577
+ });
1578
+ }