@jgengine/core 0.14.0 → 0.15.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 (214) hide show
  1. package/CHANGELOG.md +500 -1
  2. package/dist/ai/difficulty.d.ts +154 -0
  3. package/dist/ai/difficulty.js +194 -0
  4. package/dist/ai/driver.d.ts +107 -0
  5. package/dist/ai/driver.js +175 -0
  6. package/dist/ai/flock.js +13 -23
  7. package/dist/ai/laneSelect.js +2 -1
  8. package/dist/ai/mobBrain.js +2 -1
  9. package/dist/ai/waveRunner.d.ts +81 -0
  10. package/dist/ai/waveRunner.js +104 -0
  11. package/dist/audio/musicState.d.ts +47 -0
  12. package/dist/audio/musicState.js +38 -0
  13. package/dist/audio/stepCadence.d.ts +47 -0
  14. package/dist/audio/stepCadence.js +29 -0
  15. package/dist/audio/synth.d.ts +12 -0
  16. package/dist/authoring.d.ts +6 -4
  17. package/dist/authoring.js +6 -4
  18. package/dist/combat/castRunner.d.ts +13 -1
  19. package/dist/combat/castRunner.js +5 -1
  20. package/dist/combat/comboMeter.d.ts +140 -0
  21. package/dist/combat/comboMeter.js +215 -0
  22. package/dist/combat/comboPoints.d.ts +11 -0
  23. package/dist/combat/comboPoints.js +11 -2
  24. package/dist/combat/damageResolution.d.ts +1 -1
  25. package/dist/combat/defensiveWindow.d.ts +5 -0
  26. package/dist/combat/effects.d.ts +24 -0
  27. package/dist/combat/effects.js +6 -1
  28. package/dist/combat/projectiles.d.ts +6 -0
  29. package/dist/combat/projectiles.js +2 -2
  30. package/dist/combat/regenShield.d.ts +40 -2
  31. package/dist/combat/regenShield.js +66 -18
  32. package/dist/combat/resourcePool.d.ts +19 -0
  33. package/dist/combat/resourcePool.js +20 -2
  34. package/dist/combat/shotOrigin.d.ts +5 -0
  35. package/dist/combat/statusApplication.d.ts +1 -1
  36. package/dist/combat/statusApplication.js +2 -1
  37. package/dist/combat/statusEffectView.d.ts +56 -0
  38. package/dist/combat/statusEffectView.js +52 -0
  39. package/dist/combat.d.ts +4 -2
  40. package/dist/combat.js +2 -0
  41. package/dist/devtools/fallbackSeams.d.ts +3 -2
  42. package/dist/devtools/fallbackSeams.js +1 -1
  43. package/dist/devtools/textureErrors.d.ts +40 -0
  44. package/dist/devtools/textureErrors.js +68 -0
  45. package/dist/economy/shopStock.d.ts +110 -0
  46. package/dist/economy/shopStock.js +157 -0
  47. package/dist/economy/wallet.d.ts +6 -0
  48. package/dist/economy/wallet.js +1 -0
  49. package/dist/game/achievements.d.ts +80 -0
  50. package/dist/game/achievements.js +124 -0
  51. package/dist/game/cameraConfig.d.ts +7 -0
  52. package/dist/game/codex.d.ts +60 -0
  53. package/dist/game/codex.js +89 -0
  54. package/dist/game/credits.d.ts +36 -0
  55. package/dist/game/credits.js +0 -0
  56. package/dist/game/defineGame.d.ts +28 -3
  57. package/dist/game/defineGame.js +5 -0
  58. package/dist/game/dialogueGraph.d.ts +116 -0
  59. package/dist/game/dialogueGraph.js +103 -0
  60. package/dist/game/eventTicker.d.ts +95 -0
  61. package/dist/game/eventTicker.js +91 -0
  62. package/dist/game/events.d.ts +24 -5
  63. package/dist/game/events.js +18 -0
  64. package/dist/game/gamePhase.d.ts +1 -1
  65. package/dist/game/gamePhase.js +1 -1
  66. package/dist/game/leaderboardRank.d.ts +71 -0
  67. package/dist/game/leaderboardRank.js +76 -0
  68. package/dist/game/lootTable.d.ts +10 -1
  69. package/dist/game/lootTable.js +4 -2
  70. package/dist/game/notifications.d.ts +57 -0
  71. package/dist/game/notifications.js +107 -0
  72. package/dist/game/playableGame.d.ts +39 -2
  73. package/dist/game/quest.d.ts +26 -0
  74. package/dist/game/quest.js +31 -0
  75. package/dist/game/runDraft.js +2 -1
  76. package/dist/game/saveSlots.d.ts +102 -0
  77. package/dist/game/saveSlots.js +136 -0
  78. package/dist/game/talentTreeView.d.ts +106 -0
  79. package/dist/game/talentTreeView.js +114 -0
  80. package/dist/game/worldItem.js +2 -1
  81. package/dist/gameplay.d.ts +15 -4
  82. package/dist/gameplay.js +13 -2
  83. package/dist/i18n/i18n.d.ts +51 -0
  84. package/dist/i18n/i18n.js +82 -0
  85. package/dist/input/axisInput.d.ts +1 -1
  86. package/dist/input/axisInput.js +18 -2
  87. package/dist/input/rebindSession.d.ts +141 -0
  88. package/dist/input/rebindSession.js +235 -0
  89. package/dist/input/touchControlsMode.d.ts +16 -0
  90. package/dist/input/touchControlsMode.js +23 -0
  91. package/dist/input/touchScheme.d.ts +30 -1
  92. package/dist/input/touchScheme.js +31 -1
  93. package/dist/interaction/promptRegistry.d.ts +78 -0
  94. package/dist/interaction/promptRegistry.js +130 -0
  95. package/dist/inventory/storageTier.js +3 -2
  96. package/dist/meta/changelog.d.ts +1 -1
  97. package/dist/meta/changelog.js +145 -1
  98. package/dist/movement/movementModel.d.ts +26 -3
  99. package/dist/movement/movementModel.js +121 -15
  100. package/dist/movement/playerMovement.js +161 -25
  101. package/dist/physics/drivableVehicle.js +3 -1
  102. package/dist/physics/kinematicVehicle.d.ts +56 -1
  103. package/dist/physics/kinematicVehicle.js +99 -10
  104. package/dist/physics/vehicleObstacles.js +19 -0
  105. package/dist/random/resolveRng.d.ts +14 -0
  106. package/dist/random/resolveRng.js +31 -0
  107. package/dist/render/postProcessing.d.ts +12 -1
  108. package/dist/runtime/cameraDirector.d.ts +2 -2
  109. package/dist/runtime/context/combat.js +8 -0
  110. package/dist/runtime/gameContext.js +6 -1
  111. package/dist/runtime/gameContextTypes.d.ts +18 -0
  112. package/dist/runtime/inputSnapshot.d.ts +10 -1
  113. package/dist/runtime/inputSnapshot.js +13 -1
  114. package/dist/runtime/runtimeSave.d.ts +3 -3
  115. package/dist/runtime/runtimeSave.js +11 -2
  116. package/dist/scene/assetDiagnostics.js +2 -2
  117. package/dist/scene/autoTarget.js +2 -1
  118. package/dist/scene/captureCheck.js +3 -2
  119. package/dist/scene/entityStore.d.ts +4 -1
  120. package/dist/scene/entityStore.js +3 -0
  121. package/dist/scene/objectStore.d.ts +10 -0
  122. package/dist/scene/objectStore.js +29 -6
  123. package/dist/scene/sceneKinds.d.ts +18 -0
  124. package/dist/scene/sceneKinds.js +19 -0
  125. package/dist/scene/sequenceDirector.d.ts +110 -0
  126. package/dist/scene/sequenceDirector.js +146 -0
  127. package/dist/session/roles.js +3 -2
  128. package/dist/settings/settingsModel.d.ts +1 -0
  129. package/dist/settings/settingsModel.js +1 -0
  130. package/dist/stats/rollCheck.js +4 -2
  131. package/dist/time/dayNightCycle.d.ts +114 -0
  132. package/dist/time/dayNightCycle.js +208 -0
  133. package/dist/time/timerSet.d.ts +132 -0
  134. package/dist/time/timerSet.js +243 -0
  135. package/dist/ui/accessibility.d.ts +51 -0
  136. package/dist/ui/accessibility.js +83 -0
  137. package/dist/ui/coachMarks.d.ts +99 -0
  138. package/dist/ui/coachMarks.js +126 -0
  139. package/dist/ui/floatingText.d.ts +114 -0
  140. package/dist/ui/floatingText.js +203 -0
  141. package/dist/ui/modalStack.d.ts +143 -0
  142. package/dist/ui/modalStack.js +160 -0
  143. package/dist/ui/objectiveBanner.d.ts +123 -0
  144. package/dist/ui/objectiveBanner.js +162 -0
  145. package/dist/ui/photoMode.d.ts +32 -0
  146. package/dist/ui/photoMode.js +51 -0
  147. package/dist/ui/radialMenu.d.ts +47 -0
  148. package/dist/ui/radialMenu.js +74 -0
  149. package/dist/ui/screenMarkers.d.ts +128 -0
  150. package/dist/ui/screenMarkers.js +118 -0
  151. package/dist/ui.d.ts +8 -0
  152. package/dist/ui.js +8 -0
  153. package/dist/vfx/cameraShake.d.ts +97 -0
  154. package/dist/vfx/cameraShake.js +136 -0
  155. package/dist/vfx/damageDirection.d.ts +102 -0
  156. package/dist/vfx/damageDirection.js +188 -0
  157. package/dist/vfx/particles.d.ts +103 -0
  158. package/dist/vfx/particles.js +256 -0
  159. package/dist/vfx/screenEffects.d.ts +149 -0
  160. package/dist/vfx/screenEffects.js +191 -0
  161. package/dist/world/authoredObjects.d.ts +18 -0
  162. package/dist/world/authoredObjects.js +21 -0
  163. package/dist/world/authoredSpawn.js +19 -0
  164. package/dist/world/buildingLots.d.ts +54 -11
  165. package/dist/world/buildingLots.js +116 -30
  166. package/dist/world/cityBlocks.d.ts +45 -0
  167. package/dist/world/cityBlocks.js +159 -0
  168. package/dist/world/cityContent.d.ts +46 -2
  169. package/dist/world/cityContent.js +160 -2
  170. package/dist/world/cityGenerator.d.ts +261 -23
  171. package/dist/world/cityGenerator.js +643 -33
  172. package/dist/world/cityGeometry.d.ts +20 -0
  173. package/dist/world/cityGeometry.js +91 -0
  174. package/dist/world/cityKind.js +1 -44
  175. package/dist/world/fastTravel.d.ts +64 -0
  176. package/dist/world/fastTravel.js +108 -0
  177. package/dist/world/features.d.ts +16 -0
  178. package/dist/world/features.js +2 -0
  179. package/dist/world/grounded.d.ts +11 -0
  180. package/dist/world/grounded.js +12 -0
  181. package/dist/world/mapAnnotations.d.ts +75 -0
  182. package/dist/world/mapAnnotations.js +139 -0
  183. package/dist/world/markers.js +1 -0
  184. package/dist/world/resourceNode.d.ts +1 -1
  185. package/dist/world/resourceNode.js +2 -1
  186. package/dist/world/roads.d.ts +294 -5
  187. package/dist/world/roads.js +1262 -21
  188. package/dist/world/sharedAnnotations.d.ts +84 -0
  189. package/dist/world/sharedAnnotations.js +76 -0
  190. package/dist/world/spawnOverride.d.ts +37 -0
  191. package/dist/world/spawnOverride.js +50 -0
  192. package/dist/world/streetGenerator.d.ts +60 -11
  193. package/dist/world/streetGenerator.js +1786 -129
  194. package/dist/world/terrain.d.ts +1 -0
  195. package/dist/world/terrain.js +2 -0
  196. package/dist/world/waypoints.d.ts +63 -0
  197. package/dist/world/waypoints.js +137 -0
  198. package/dist/world.d.ts +17 -2
  199. package/dist/world.js +17 -2
  200. package/package.json +1 -1
  201. package/skills/ce-handoff/SKILL.md +149 -0
  202. package/skills/game-design/SKILL.md +2 -0
  203. package/skills/jgengine/SKILL.md +3 -1
  204. package/skills/jgengine/api.md +17 -7
  205. package/skills/jgengine/capabilities.md +12 -0
  206. package/skills/jgengine/recipes/minimal-game.md +7 -4
  207. package/skills/jgengine-combat/api.md +40 -4
  208. package/skills/jgengine-combat/capabilities.md +12 -0
  209. package/skills/jgengine-combat/reference.md +2 -0
  210. package/skills/jgengine-gameplay/api.md +212 -11
  211. package/skills/jgengine-gameplay/capabilities.md +66 -1
  212. package/skills/jgengine-world/api.md +397 -11
  213. package/skills/jgengine-world/capabilities.md +123 -2
  214. package/skills/jgengine-world/reference.md +15 -1
@@ -10,6 +10,8 @@ export const CITY_LOT_CLASSES = [
10
10
  "barn",
11
11
  "silo",
12
12
  ];
13
+ /** All filler classes, for validation and interior-pass allow-lists. */
14
+ export const CITY_FILLER_CLASSES = ["garage", "depot"];
13
15
  const CLASS_PROFILES = {
14
16
  tower: { width: [16, 24], depth: [14, 20], setbackFactor: 0.3, spacingFactor: 1, floors: [12, 34], backRow: true },
15
17
  slab: { width: [14, 20], depth: [11, 15], setbackFactor: 0.5, spacingFactor: 1, floors: [4, 9], backRow: true },
@@ -20,7 +22,17 @@ const CLASS_PROFILES = {
20
22
  farmhouse: { width: [11, 14], depth: [9, 12], setbackFactor: 2.8, spacingFactor: 4, floors: [2, 2], backRow: false },
21
23
  barn: { width: [12, 16], depth: [9, 13], setbackFactor: 3.6, spacingFactor: 5, floors: [1, 1], backRow: false },
22
24
  silo: { width: [5, 6], depth: [5, 6], setbackFactor: 4, spacingFactor: 3, floors: [1, 1], backRow: false },
25
+ // Interior-fill fillers: broad, plain footprints that pack the backs of dense blocks.
26
+ garage: { width: [18, 26], depth: [16, 22], setbackFactor: 0.3, spacingFactor: 0.4, floors: [2, 6], backRow: true },
27
+ depot: { width: [20, 30], depth: [18, 26], setbackFactor: 0.4, spacingFactor: 0.5, floors: [1, 2], backRow: true },
23
28
  };
29
+ /**
30
+ * Natural frontage-width range (meters) of a class's massing — what the plot-size class bias reads
31
+ * so towers land on wide plots and rowhouses on narrow ones. @internal
32
+ */
33
+ export function classWidthRange(cls) {
34
+ return CLASS_PROFILES[cls]?.width;
35
+ }
24
36
  function range(rng, [lo, hi]) {
25
37
  return lo + rng() * (hi - lo);
26
38
  }
@@ -30,11 +42,13 @@ function range(rng, [lo, hi]) {
30
42
  * aligned building wall; spacing likewise, so neighbors form rhythmic rows instead of scatter.
31
43
  * @internal
32
44
  */
33
- export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, setbackBase, spacingBase) {
45
+ export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, setbackBase, spacingBase, widthStretch = 1) {
34
46
  const profile = CLASS_PROFILES[cls];
35
47
  const floors = Math.round(range(rng, profile.floors));
36
48
  return {
37
- width: range(rng, profile.width) * lotScale,
49
+ // `widthStretch` (≥1, from the block-fill dial) is a post-multiply on the rolled width — it
50
+ // never changes the rng draw order, so the default (1) stays byte-identical.
51
+ width: range(rng, profile.width) * lotScale * widthStretch,
38
52
  depth: range(rng, profile.depth) * lotScale,
39
53
  setback: setbackBase * profile.setbackFactor * (0.9 + rng() * 0.2),
40
54
  gap: Math.max(0.2, spacingBase * profile.spacingFactor * (0.7 + rng() * 0.6)),
@@ -42,6 +56,26 @@ export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, set
42
56
  backRow: profile.backRow,
43
57
  };
44
58
  }
59
+ /**
60
+ * Weight multiplier favouring classes whose natural frontage fits a plot of width `plotW`: a class
61
+ * far wider than the plot (a tower profile on a 6 m slice) is heavily downweighted, a class that
62
+ * would rattle around a much wider plot is mildly downweighted, and anything whose width range
63
+ * genuinely fits keeps weight 1. Pure data over {@link CLASS_PROFILES} — this is what turns a
64
+ * mixed-plot frontage into terraces on the slices and detached houses on the wide parcels.
65
+ * @internal
66
+ */
67
+ export function classPlotFit(cls, plotW) {
68
+ const [lo, hi] = CLASS_PROFILES[cls].width;
69
+ if (plotW < lo * 0.6)
70
+ return 0; // physically cannot fit — a tower never squeezes onto a slice
71
+ if (plotW < lo) {
72
+ const r = plotW / lo;
73
+ return Math.max(0.1, r * r);
74
+ }
75
+ if (plotW > hi * 1.7)
76
+ return Math.max(0.35, (hi * 1.7) / plotW);
77
+ return 1;
78
+ }
45
79
  /**
46
80
  * Normalized zone metric for a point in district-local space: 0 at the center line, 1 at the rim,
47
81
  * shaped to the district's rectangle (Chebyshev) so bands follow the volume's footprint.
@@ -236,6 +270,130 @@ export function buildLotPieces(cls, width, depth, floors, floorHeight, rng) {
236
270
  }
237
271
  return pieces;
238
272
  }
273
+ case "garage": {
274
+ // Parking structure: a plain banded box (the window bands read as deck slots) with a flat cap.
275
+ pieces.push(box([0, 0, 0], [width, H, depth], "wall", { grounded: true, banded: true }));
276
+ pieces.push(box([0, H, 0], [width * 1.02, 0.4, depth * 1.02], "trim"));
277
+ if (rng() < 0.6) {
278
+ // Stair/lift core poking above the roof deck.
279
+ const cx = (rng() - 0.5) * width * 0.5;
280
+ const cz = (rng() - 0.5) * depth * 0.4;
281
+ pieces.push(box([cx, H + 0.4, cz], [width * 0.16, 1.6 + rng() * 1.2, depth * 0.16], "accent"));
282
+ }
283
+ return pieces;
284
+ }
285
+ case "depot": {
286
+ // Warehouse/depot: a low broad box under a shallow gable, with an optional loading canopy.
287
+ const bw = width * 0.98;
288
+ const bd = depth * 0.96;
289
+ pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
290
+ const roofH = Math.min(2.4 + rng() * 1.6, bd * 0.28);
291
+ pieces.push(gable([0, H, 0], [bw, roofH, bd]));
292
+ if (rng() < 0.5) {
293
+ pieces.push(box([0, Math.min(H - 0.4, 2.6), bd / 2 - 0.4], [bw * 0.55, 0.4, 0.8], "accent"));
294
+ }
295
+ return pieces;
296
+ }
297
+ }
298
+ }
299
+ /** All landmark classes, for schema hints, validation, and default allow-lists. */
300
+ export const CITY_LANDMARK_CLASSES = ["hall", "arena", "market", "campus"];
301
+ const LANDMARK_PROFILES = {
302
+ hall: { floors: [2, 3], minDepthFactor: 0.55 },
303
+ arena: { floors: [3, 5], minDepthFactor: 0.85 },
304
+ market: { floors: [1, 2], minDepthFactor: 0.6 },
305
+ campus: { floors: [3, 6], minDepthFactor: 0.7 },
306
+ };
307
+ /** Minimum depth-into-block factor for a landmark class (the pass grows shallow clusters to this). @internal */
308
+ export function landmarkMinDepthFactor(cls) {
309
+ return LANDMARK_PROFILES[cls].minDepthFactor;
310
+ }
311
+ /** Roll a landmark's floor count from its profile range. @internal */
312
+ export function landmarkFloors(cls, rng) {
313
+ return Math.round(range(rng, LANDMARK_PROFILES[cls].floors));
314
+ }
315
+ /**
316
+ * Compose the massing pieces for one landmark parcel, filling the given `width`×`depth` footprint
317
+ * (lot-local, x along frontage, z into the block). Deterministic per `rng`. Every piece is kept
318
+ * inside the `[-width/2, width/2] × [-depth/2, depth/2]` box so the reported footprint matches the
319
+ * merged cluster's AABB. Sizes are in meters.
320
+ * @internal
321
+ */
322
+ export function buildLandmarkPieces(cls, width, depth, floors, floorHeight, rng) {
323
+ const H = Math.max(1, floors) * floorHeight;
324
+ const pieces = [];
325
+ switch (cls) {
326
+ case "hall": {
327
+ // Civic block: broad walled base, an entablature, a fronting colonnade, and a central dome.
328
+ const bw = width * 0.96;
329
+ const bd = depth * 0.96;
330
+ pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
331
+ pieces.push(box([0, H, 0], [bw, 0.6, bd], "trim"));
332
+ const cols = Math.max(4, Math.round(bw / 6));
333
+ const colH = H * 0.92;
334
+ const zFront = bd / 2 - 0.6;
335
+ for (let i = 0; i < cols; i += 1) {
336
+ const t = cols === 1 ? 0.5 : i / (cols - 1);
337
+ const x = (t - 0.5) * (bw - 1.4);
338
+ pieces.push(box([x, 0, zFront], [0.7, colH, 0.7], "trim", { grounded: true }));
339
+ }
340
+ // Portico roof over the colonnade.
341
+ pieces.push(box([0, colH, zFront], [bw - 1.0, 0.5, 1.6], "roof"));
342
+ const dd = Math.min(bw, bd) * (0.4 + rng() * 0.06);
343
+ pieces.push({ shape: "dome", offset: [0, H, 0], size: [dd, dd * 0.55, dd], rotationY: 0, role: "roof", grounded: false, banded: false });
344
+ return pieces;
345
+ }
346
+ case "arena": {
347
+ // Bowl: an elliptical outer wall, a rim ring, and a sunken inner field ring.
348
+ const ow = width * 0.94;
349
+ const od = depth * 0.94;
350
+ pieces.push({ shape: "cylinder", offset: [0, 0, 0], size: [ow, H, od], rotationY: 0, role: "wall", grounded: true, banded: true });
351
+ pieces.push({ shape: "cylinder", offset: [0, H, 0], size: [ow * 0.99, 0.9, od * 0.99], rotationY: 0, role: "trim", grounded: false, banded: false });
352
+ pieces.push({ shape: "cylinder", offset: [0, 0, 0], size: [ow * 0.66, H * 0.5, od * 0.66], rotationY: 0, role: "accent", grounded: true, banded: false });
353
+ if (rng() < 0.6) {
354
+ // A short mast/scoreboard on the rim.
355
+ pieces.push(box([0, H, 0], [1.4, 3 + rng() * 3, 1.4], "trim"));
356
+ }
357
+ return pieces;
358
+ }
359
+ case "market": {
360
+ // Broad low gabled hall with an open stall face on the street side.
361
+ const bw = width * 0.97;
362
+ const bd = depth * 0.95;
363
+ pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
364
+ const roofH = Math.min(4 + rng() * 2, bd * 0.4);
365
+ pieces.push(gable([0, H, 0], [bw, roofH, bd]));
366
+ // Clerestory monitor along the ridge.
367
+ pieces.push(box([0, H, 0], [bw * 0.5, roofH * 0.5, bd * 0.25], "accent"));
368
+ // Awning/stall lip on the front face.
369
+ pieces.push(box([0, Math.min(H - 0.5, 3), bd / 2 - 0.4], [bw * 0.94, 0.4, 1.0], "accent"));
370
+ return pieces;
371
+ }
372
+ case "campus": {
373
+ // Courtyard: four connected slab wings around an open center, varied heights.
374
+ const bw = width * 0.96;
375
+ const bd = depth * 0.96;
376
+ const wingT = Math.min(bw, bd) * 0.22;
377
+ const innerD = Math.max(1, bd - 2 * wingT);
378
+ const frontH = H * (0.9 + rng() * 0.2);
379
+ const backH = H * (0.85 + rng() * 0.2);
380
+ const leftH = H * (0.9 + rng() * 0.25);
381
+ const rightH = H * (1.0 + rng() * 0.2);
382
+ pieces.push(box([0, 0, (bd - wingT) / 2], [bw, frontH, wingT], "wall", { grounded: true, banded: true }));
383
+ pieces.push(box([0, 0, -(bd - wingT) / 2], [bw, backH, wingT], "wall", { grounded: true, banded: true }));
384
+ pieces.push(box([(bw - wingT) / 2, 0, 0], [wingT, rightH, innerD], "wall", { grounded: true, banded: true }));
385
+ pieces.push(box([-(bw - wingT) / 2, 0, 0], [wingT, leftH, innerD], "wall", { grounded: true, banded: true }));
386
+ // Parapet caps.
387
+ for (const [wx, wz, ww, wd, wh] of [
388
+ [0, (bd - wingT) / 2, bw, wingT, frontH],
389
+ [0, -(bd - wingT) / 2, bw, wingT, backH],
390
+ [(bw - wingT) / 2, 0, wingT, innerD, rightH],
391
+ [-(bw - wingT) / 2, 0, wingT, innerD, leftH],
392
+ ]) {
393
+ pieces.push(box([wx, wh, wz], [ww, 0.4, wd], "trim"));
394
+ }
395
+ return pieces;
396
+ }
239
397
  }
240
398
  }
241
399
  /** All species, for schema hints and validation. */
@@ -1,41 +1,279 @@
1
1
  /**
2
- * The seed-driven procedural CITY GENERATOR — the composition seam over the two fabric engines:
3
- * {@link generateStreets} grows the street network, and {@link deriveBuildingLots} lines its
4
- * frontage with street-facing building lots. One call, one seed, one deterministic city: same rules
5
- * ⇒ identical streets and identical lots, with both engines' bounded-work caps intact. Local
6
- * (volume-centered) coords, same as the street generator; the caller translates into world space.
2
+ * The seed-driven procedural CITY GENERATOR — plot-first composition over the street and block
3
+ * engines. {@link generateStreets} grows the street network (with a seeded organic outline, so no
4
+ * two seeds share a silhouette), {@link extractBlocks} turns it into closed block polygons, and the
5
+ * plot pass subdivides every buildable block's street frontage into PLOTS of many sizes — narrow
6
+ * rowhouse slivers up to block-scale grand parcels. Everything that stands in the city stands on a
7
+ * plot, and every plot fronts a street by construction: there is no landmark special case (a
8
+ * landmark is just a grand plot) and no street-less interior fill. One call, one seed, one
9
+ * deterministic city. Local (volume-centered) coords; the caller translates into world space.
7
10
  *
8
- * Street dials default to a dense, gently-varied city net (high gridness/connectivity, light
9
- * winding); pass any subset of {@link StreetNetworkRules} to override, and lot options pass through
10
- * to the frontage engine untouched. Lanes are skipped as lot frontage by default — buildings line
11
- * boulevards, avenues, and streets — so alleys stay service alleys; opt lanes in via
12
- * `lots.laneFrontage`.
13
- *
14
- * @capability city-generator compose a deterministic city — street network plus street-facing building lots — from one seed
11
+ * @capability city-generator compose a deterministic city — street network, blocks, street-fronting plots of varied size — from one seed
15
12
  */
16
- import { type BuildingLotOptions, type PlacedBuildingLot } from "./buildingLots.js";
17
- import { type StreetNetwork, type StreetNetworkContext, type StreetNetworkRules } from "./streetGenerator.js";
18
- /** Options for {@link generateCity}: a seed, street-dial overrides, and lot pass-through options. */
13
+ import type { PlacedBuildingLot } from "./buildingLots.js";
14
+ import { type CityFillerClass, type CityLandmarkClass, type CityLotClass, type CityLotPiece, type CityZoneBand, type CityZoneProfile } from "./cityContent.js";
15
+ import { type Vec2 } from "./cityGeometry.js";
16
+ import type { WeightedParamEntry } from "../scene/sceneKinds.js";
17
+ import { type StreetLevel, type StreetNetwork, type StreetNetworkContext, type StreetNetworkRules } from "./streetGenerator.js";
18
+ /** Options for {@link generateCity}: a seed, street-dial overrides, and plot pass-through options. */
19
19
  export interface CityGeneratorOptions {
20
- /** Drives both the street network and downstream lot/floor variation. */
20
+ /** Drives the street network and all downstream plot/class/massing variation. */
21
21
  seed: string;
22
- /** Street-dial overrides; anything omitted takes the city defaults above. */
22
+ /** Street-dial overrides; anything omitted takes the city defaults above (including `outline`). */
23
23
  streets?: Partial<Omit<StreetNetworkRules, "seed">>;
24
- /** Frontage-lot options, minus `roads` (the generated streets are the roads). */
25
- lots?: Omit<BuildingLotOptions, "roads" | "seed"> & {
26
- /** Also line `lane`-level streets (alleys) with lots. Default false. */
24
+ /** Plot-pass options (sizes scale off `footprint`, gaps off `spacing`, fronts off `setback`). */
25
+ lots?: {
26
+ /** Base plot-size hint: tier frontage/depth ranges scale by `w/12` and `d/10`. Default 12×10. */
27
+ footprint?: {
28
+ w: number;
29
+ d: number;
30
+ };
31
+ /** Base gap between neighbouring plots along a frontage, world units. Default 2. */
32
+ spacing?: number;
33
+ /** Mix between uniform medium plots (0) and the full zone/street tier distribution (1). Default 1. */
34
+ variety?: number;
35
+ /** Sidewalk strip between the curb and the building front, world units. Default 3. */
36
+ setback?: number;
37
+ /** Manhattan streetwall dial (0..1): frontage coverage and plot depth grow with it. Default {@link DEFAULT_BLOCK_FILL}. */
38
+ blockFill?: number;
39
+ /** Also line `lane`-level streets (alleys) with plots. Default false. */
27
40
  laneFrontage?: boolean;
41
+ /** Hard cap on emitted plots so a big network stays bounded. Default 600. */
42
+ maxLots?: number;
28
43
  };
29
44
  /** Ground sampler + bridge/tunnel toggles forwarded to the street generator. */
30
45
  context?: StreetNetworkContext;
46
+ /**
47
+ * Also resolve each plot into zone/class/floors/massing. Pass `true` for city defaults, or an
48
+ * overrides object to tune bands/mixes/floors. Omit (default) to keep the cheap geometry-only
49
+ * path — `lotContent` stays absent. See {@link resolveCityLotContent}.
50
+ */
51
+ content?: boolean | CityContentOverrides;
52
+ }
53
+ /** Size tier a plot was cut at. `grand` is the block-scale tier that used to be a landmark pass. */
54
+ export type CityPlotTier = "small" | "medium" | "large" | "grand";
55
+ /** All plot tiers, for schema hints and validation. */
56
+ export declare const CITY_PLOT_TIERS: readonly CityPlotTier[];
57
+ /** The street frontage a plot faces: which street, and the frontage chord along it. */
58
+ export interface CityPlotFrontage {
59
+ /** Index into `network.streets`. */
60
+ street: number;
61
+ /** Frontage chord endpoints on the plot boundary. */
62
+ a: Vec2;
63
+ b: Vec2;
64
+ }
65
+ /** One polygonal plot cut from a block's street frontage. Every plot fronts a street. */
66
+ export interface CityPlot {
67
+ /** Plot polygon (CCW) in local XZ. */
68
+ polygon: Vec2[];
69
+ /** Building-rect center inside the plot (matches the paired {@link PlacedBuildingLot}). */
70
+ center: Vec2;
71
+ /** Yaw turning the building front toward its street. */
72
+ rotationY: number;
73
+ /** Building-rect frontage width / depth fitted inside the plot polygon. */
74
+ width: number;
75
+ depth: number;
76
+ frontage: CityPlotFrontage;
77
+ /** Hierarchy level of the fronted street. */
78
+ streetLevel: StreetLevel;
79
+ /** Index of the block the plot was cut from; `-1` for plots lining a cul-de-sac/branch corridor. */
80
+ block: number;
81
+ tier: CityPlotTier;
82
+ /** True when the plot wraps a block corner (two frontages meet inside it). */
83
+ corner: boolean;
31
84
  }
32
- /** A generated city: the street network and the building lots lining its frontage. */
85
+ /** A generated city: streets, plots (with rect building lots as a compat view), and park blocks. */
33
86
  export interface GeneratedCity {
34
87
  network: StreetNetwork;
88
+ /** Rect building-lot view of `plots` (same order, same indices) — the renderer-compat seam. */
35
89
  lots: PlacedBuildingLot[];
90
+ /** The polygonal plots the city is made of. `plots[i]` pairs with `lots[i]`. */
91
+ plots: CityPlot[];
92
+ /** Land polygons of whole blocks reserved as parks/open space (no plots cut). */
93
+ parks: Vec2[][];
94
+ /**
95
+ * Present only when {@link CityGeneratorOptions.content} was requested: every plot enriched with
96
+ * its zone band, building class, floors, and lot-local massing pieces. Same order as `lots`.
97
+ */
98
+ lotContent?: readonly ResolvedCityLot[];
36
99
  }
37
100
  /**
38
- * Grow a street network inside the `hx`/`hz` half-extents and line its frontage with building lots.
39
- * Deterministic: identical options ⇒ identical city.
101
+ * Grow a street network inside the `hx`/`hz` half-extents, extract its blocks, and subdivide their
102
+ * frontage into plots. Deterministic: identical options ⇒ identical city.
40
103
  */
41
104
  export declare function generateCity(options: CityGeneratorOptions, hx: number, hz: number): GeneratedCity;
105
+ /** Default `blockFill`: gently gapped frontage — plots with breathing room, parks reserved. */
106
+ export declare const DEFAULT_BLOCK_FILL = 0.45;
107
+ /** Default grand-plot share dial — a handful of block-scale parcels per default city. */
108
+ export declare const DEFAULT_LANDMARK_SHARE = 0.04;
109
+ /** Hard cap on grand plots emitted regardless of dial/city size. */
110
+ export declare const LANDMARK_HARD_CAP = 12;
111
+ /** Options for {@link deriveCityPlots}. */
112
+ export interface CityPlotOptions {
113
+ seed: string;
114
+ /** Half-extents the city was grown in — zone tiers normalize to these. */
115
+ halfExtents: readonly [number, number];
116
+ /** Cut plots along `lane`-level frontage too. Default false. */
117
+ laneFrontage?: boolean;
118
+ /** Sidewalk strip between curb and building front, meters. Default 3. */
119
+ setback?: number;
120
+ /** Base gap between neighbouring plots, meters. Default 2. */
121
+ spacing?: number;
122
+ /** Mix between uniform medium plots (0) and the full zone/street tier distribution (1). Default 1. */
123
+ variety?: number;
124
+ /** Streetwall dial (0..1): frontage coverage, plot depth, and park share ride on it. Default {@link DEFAULT_BLOCK_FILL}. */
125
+ blockFill?: number;
126
+ /** Grand-plot share dial (0..1): probability weight of the block-scale tier. Default {@link DEFAULT_LANDMARK_SHARE}; `0` disables grand plots. */
127
+ landmarks?: number;
128
+ /** Tier size multiplier `[wScale, dScale]`. Default `[1, 1]`. */
129
+ plotScale?: readonly [number, number];
130
+ /** Hard cap on emitted plots. Default 600. */
131
+ maxPlots?: number;
132
+ /** Zone profile the tier weights read. Defaults match {@link resolveCityLotContent}. */
133
+ profile?: CityZoneProfile;
134
+ coreExtent?: number;
135
+ midExtent?: number;
136
+ }
137
+ /** Output of {@link deriveCityPlots}: the plots plus whole-block park polygons. */
138
+ export interface CityPlotResult {
139
+ plots: CityPlot[];
140
+ parks: Vec2[][];
141
+ }
142
+ /**
143
+ * Subdivide every buildable block's street frontage into size-tiered polygonal plots. Blocks come
144
+ * from the street network's planar faces ({@link extractBlocks}), so every plot fronts a real
145
+ * street; block interiors stay open (courtyards) and a seeded fraction of whole blocks becomes
146
+ * parks. Grand plots — the old "landmarks" — are just the biggest tier of the same pass.
147
+ * Deterministic per seed, bounded by `maxPlots` and per-block caps.
148
+ *
149
+ * @capability city-generator subdivide street blocks into size-tiered street-fronting plots
150
+ */
151
+ export declare function deriveCityPlots(network: StreetNetwork, options: CityPlotOptions): CityPlotResult;
152
+ /** Weighted building-class mix per zone band; the radial profile decides which band a lot falls in. */
153
+ export interface CityZoneMixes {
154
+ core: readonly WeightedParamEntry[];
155
+ mid: readonly WeightedParamEntry[];
156
+ edge: readonly WeightedParamEntry[];
157
+ }
158
+ /** Default zoned-metropolis mixes: towers/slabs downtown, slabs+rowhouses mid, houses at the edge. */
159
+ export declare const DEFAULT_CITY_ZONE_MIXES: CityZoneMixes;
160
+ /**
161
+ * Per-street-level class weight multipliers applied to the band mix before the class pick, so towers
162
+ * and slabs bias toward wide boulevard/avenue frontage while houses bias toward lanes and quiet
163
+ * streets. A missing class ⇒ multiplier 1. Pure data modulating the weighted mixes.
164
+ */
165
+ export type CityLevelClassBias = Record<StreetLevel, Partial<Record<CityLotClass, number>>>;
166
+ /** Default street-level bias — boulevards favor big massing, lanes favor small/rural massing. */
167
+ export declare const DEFAULT_CITY_LEVEL_BIAS: CityLevelClassBias;
168
+ /** Tunable overrides for {@link resolveCityLotContent} (seed/halfExtents/laneFrontage are supplied). */
169
+ export interface CityContentOverrides {
170
+ /** Radial band profile: `core-out` (default), `inverted`, or spatially `uniform`. */
171
+ profile?: CityZoneProfile;
172
+ /** Fraction of the half-extent the core band covers. Default 0.35. */
173
+ coreExtent?: number;
174
+ /** Fraction of the half-extent where the mid band ends and the edge begins. Default 0.7. */
175
+ midExtent?: number;
176
+ /** Weighted class mix per band; any omitted band takes {@link DEFAULT_CITY_ZONE_MIXES}. */
177
+ mixes?: Partial<CityZoneMixes>;
178
+ /** Global massing-size multiplier forwarded to the class placement roll. Default 1. */
179
+ lotScale?: number;
180
+ /** Minimum building floors (district clamp over class ranges). Default 1. */
181
+ floorsMin?: number;
182
+ /** Maximum building floors. Default 30. */
183
+ floorsMax?: number;
184
+ /** Height of one floor in meters. Default 3. */
185
+ floorHeight?: number;
186
+ /** Base road setback in meters (class factors scale it). Default 2.5. */
187
+ setback?: number;
188
+ /** Base side spacing in meters between neighbours. Default 1.4. */
189
+ spacing?: number;
190
+ /**
191
+ * Bias the band mix by the plot's frontage street level before the class pick. `true` (default)
192
+ * uses {@link DEFAULT_CITY_LEVEL_BIAS}; pass a table to customise; `false` disables the bias.
193
+ */
194
+ streetLevelBias?: boolean | CityLevelClassBias;
195
+ /**
196
+ * Grand-plot share dial (0..1): the probability weight of the block-scale plot tier at geometry
197
+ * time — a landmark is nothing but a grand plot. Default {@link DEFAULT_LANDMARK_SHARE}; `0`
198
+ * yields a city with no grand plots. Count is hard-capped at {@link LANDMARK_HARD_CAP}.
199
+ */
200
+ landmarks?: number;
201
+ /**
202
+ * Restrict grand plots to these classes (a subset of {@link CITY_LANDMARK_CLASSES}). Omit to
203
+ * allow every class the zone table would pick.
204
+ */
205
+ landmarkClasses?: readonly CityLandmarkClass[];
206
+ /**
207
+ * Streetwall dial (0..1). Default {@link DEFAULT_BLOCK_FILL}. As it rises frontage coverage
208
+ * approaches a contiguous wall, plots deepen toward the block spine, and fewer whole blocks are
209
+ * reserved as parks; falling it loosens the frontage and greens the city. Block interiors are
210
+ * never filled with street-less buildings — every plot fronts a street.
211
+ */
212
+ blockFill?: number;
213
+ }
214
+ /** Full options for {@link resolveCityLotContent}: the city geometry frame plus content overrides. */
215
+ export interface CityContentOptions extends CityContentOverrides {
216
+ /** Seed for the deterministic class/floor/massing rolls. Reuse the city seed for a coherent city. */
217
+ seed: string;
218
+ /** Half-extents the city was grown in — the radial zone metric normalizes to these. */
219
+ halfExtents: readonly [number, number];
220
+ /** Whether lanes were lined with plots (mirror the geometry option). Default false. */
221
+ laneFrontage?: boolean;
222
+ }
223
+ /** Axis-aligned massing extents (lot-local): full width along x and depth along z. */
224
+ export interface MassingFootprint {
225
+ /** Full width of the massing bounding box along local x (frontage). */
226
+ w: number;
227
+ /** Full depth of the massing bounding box along local z (into the block). */
228
+ d: number;
229
+ /** Bounding-box corners in lot-local space, so a renderer can ground/cull the parcel. */
230
+ bounds: {
231
+ minX: number;
232
+ maxX: number;
233
+ minZ: number;
234
+ maxZ: number;
235
+ };
236
+ }
237
+ /** One plot enriched with its zone/class/floors/massing — the renderer instances `pieces` at `center`. */
238
+ export interface ResolvedCityLot {
239
+ /** The paired rect building lot (`city.lots[i]`). */
240
+ lot: PlacedBuildingLot;
241
+ /** Zone band the plot center fell in under the profile. */
242
+ zone: CityZoneBand;
243
+ /**
244
+ * Building class. Ordinary plots roll a {@link CityLotClass} from the band mix; grand plots roll
245
+ * a {@link CityLandmarkClass}. ({@link CityFillerClass} remains in the union for renderer compat.)
246
+ */
247
+ class: CityLotClass | CityLandmarkClass | CityFillerClass;
248
+ /**
249
+ * Set only on grand plots; equals {@link ResolvedCityLot.class} there and is `undefined` for
250
+ * every ordinary building. Renderers branch on this.
251
+ */
252
+ landmark?: CityLandmarkClass;
253
+ /** Hierarchy level of the frontage street (`street` when it can't be recovered). */
254
+ streetLevel: StreetLevel;
255
+ /** Building floor count after the district clamp. */
256
+ floors: number;
257
+ /** Deterministic massing pieces in lot-local space (x along frontage, z into block, y up from grade). */
258
+ pieces: readonly CityLotPiece[];
259
+ /** Lot center in world XZ (mirrors `lot.center`) — the anchor the renderer offsets pieces from. */
260
+ center: Vec2;
261
+ /** Building yaw so its front faces the road (mirrors `lot.rotationY`). */
262
+ rotationY: number;
263
+ /** Actual massing extents in lot-local space (from `pieces`), for grounding and culling. */
264
+ footprint: MassingFootprint;
265
+ }
266
+ /**
267
+ * Enrich a generated city's plots into classed, massed buildings: each plot gets its zone band
268
+ * (radial position under a {@link CityZoneProfile}), a building class rolled from the band's
269
+ * weighted mix — biased by street level AND plot size, with grand plots rolling a landmark class —
270
+ * floors, and the deterministic massing pieces the class composes, sized to the plot's building
271
+ * rect. Deterministic per `seed` via {@link seededStreams}, bounded (one pass over `city.lots`),
272
+ * and pure. Same seed + city ⇒ identical resolved lots.
273
+ *
274
+ * A city without `plots` (a hand-built `{network, lots}` pair) still resolves: every lot is treated
275
+ * as an ordinary plot of its footprint size.
276
+ *
277
+ * @capability city-generator resolve city plots into zoned, classed, massed buildings
278
+ */
279
+ export declare function resolveCityLotContent(city: Pick<GeneratedCity, "network" | "lots"> & Partial<Pick<GeneratedCity, "plots" | "parks">>, options: CityContentOptions): ResolvedCityLot[];