@jgengine/core 0.13.0 → 0.15.0-next.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 (223) hide show
  1. package/CHANGELOG.md +529 -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/authoring.d.ts +6 -4
  12. package/dist/authoring.js +6 -4
  13. package/dist/combat/castRunner.d.ts +13 -1
  14. package/dist/combat/castRunner.js +5 -1
  15. package/dist/combat/comboMeter.d.ts +140 -0
  16. package/dist/combat/comboMeter.js +215 -0
  17. package/dist/combat/comboPoints.d.ts +11 -0
  18. package/dist/combat/comboPoints.js +11 -2
  19. package/dist/combat/damageResolution.d.ts +1 -1
  20. package/dist/combat/defensiveWindow.d.ts +5 -0
  21. package/dist/combat/effects.d.ts +24 -0
  22. package/dist/combat/effects.js +6 -1
  23. package/dist/combat/projectiles.d.ts +6 -0
  24. package/dist/combat/projectiles.js +2 -2
  25. package/dist/combat/regenShield.d.ts +40 -2
  26. package/dist/combat/regenShield.js +66 -18
  27. package/dist/combat/resourcePool.d.ts +19 -0
  28. package/dist/combat/resourcePool.js +20 -2
  29. package/dist/combat/shotOrigin.d.ts +5 -0
  30. package/dist/combat/statusApplication.d.ts +1 -1
  31. package/dist/combat/statusApplication.js +2 -1
  32. package/dist/combat/statusEffectView.d.ts +56 -0
  33. package/dist/combat/statusEffectView.js +52 -0
  34. package/dist/combat.d.ts +4 -2
  35. package/dist/combat.js +2 -0
  36. package/dist/devtools/fallbackSeams.d.ts +3 -2
  37. package/dist/devtools/fallbackSeams.js +1 -1
  38. package/dist/devtools/textureErrors.d.ts +40 -0
  39. package/dist/devtools/textureErrors.js +68 -0
  40. package/dist/economy/shopStock.d.ts +110 -0
  41. package/dist/economy/shopStock.js +157 -0
  42. package/dist/economy/wallet.d.ts +6 -0
  43. package/dist/economy/wallet.js +1 -0
  44. package/dist/game/achievements.d.ts +80 -0
  45. package/dist/game/achievements.js +124 -0
  46. package/dist/game/cameraConfig.d.ts +7 -0
  47. package/dist/game/codex.d.ts +60 -0
  48. package/dist/game/codex.js +89 -0
  49. package/dist/game/credits.d.ts +36 -0
  50. package/dist/game/credits.js +0 -0
  51. package/dist/game/defineGame.d.ts +28 -3
  52. package/dist/game/defineGame.js +5 -0
  53. package/dist/game/dialogueGraph.d.ts +116 -0
  54. package/dist/game/dialogueGraph.js +103 -0
  55. package/dist/game/eventTicker.d.ts +95 -0
  56. package/dist/game/eventTicker.js +91 -0
  57. package/dist/game/events.d.ts +24 -5
  58. package/dist/game/events.js +18 -0
  59. package/dist/game/gamePhase.d.ts +1 -1
  60. package/dist/game/gamePhase.js +1 -1
  61. package/dist/game/leaderboardRank.d.ts +71 -0
  62. package/dist/game/leaderboardRank.js +76 -0
  63. package/dist/game/lootTable.d.ts +10 -1
  64. package/dist/game/lootTable.js +4 -2
  65. package/dist/game/notifications.d.ts +57 -0
  66. package/dist/game/notifications.js +107 -0
  67. package/dist/game/playableGame.d.ts +39 -2
  68. package/dist/game/quest.d.ts +26 -0
  69. package/dist/game/quest.js +31 -0
  70. package/dist/game/runDraft.js +2 -1
  71. package/dist/game/saveSlots.d.ts +102 -0
  72. package/dist/game/saveSlots.js +136 -0
  73. package/dist/game/talentTreeView.d.ts +106 -0
  74. package/dist/game/talentTreeView.js +114 -0
  75. package/dist/game/worldItem.js +2 -1
  76. package/dist/gameplay.d.ts +15 -4
  77. package/dist/gameplay.js +13 -2
  78. package/dist/i18n/i18n.d.ts +51 -0
  79. package/dist/i18n/i18n.js +82 -0
  80. package/dist/input/axisInput.d.ts +1 -1
  81. package/dist/input/axisInput.js +18 -2
  82. package/dist/input/rebindSession.d.ts +141 -0
  83. package/dist/input/rebindSession.js +235 -0
  84. package/dist/input/touchControlsMode.d.ts +16 -0
  85. package/dist/input/touchControlsMode.js +23 -0
  86. package/dist/input/touchScheme.d.ts +30 -1
  87. package/dist/input/touchScheme.js +31 -1
  88. package/dist/interaction/promptRegistry.d.ts +78 -0
  89. package/dist/interaction/promptRegistry.js +130 -0
  90. package/dist/inventory/storageTier.js +3 -2
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +21 -1
  93. package/dist/movement/movementModel.d.ts +26 -3
  94. package/dist/movement/movementModel.js +121 -15
  95. package/dist/movement/playerMovement.js +161 -25
  96. package/dist/physics/drivableVehicle.js +3 -1
  97. package/dist/physics/kinematicVehicle.d.ts +56 -1
  98. package/dist/physics/kinematicVehicle.js +99 -10
  99. package/dist/physics/vehicleObstacles.js +19 -0
  100. package/dist/random/resolveRng.d.ts +14 -0
  101. package/dist/random/resolveRng.js +31 -0
  102. package/dist/render/postProcessing.d.ts +12 -1
  103. package/dist/runtime/cameraDirector.d.ts +2 -2
  104. package/dist/runtime/context/combat.js +8 -0
  105. package/dist/runtime/context/scene.d.ts +2 -0
  106. package/dist/runtime/context/scene.js +48 -1
  107. package/dist/runtime/gameContext.js +8 -2
  108. package/dist/runtime/gameContextTypes.d.ts +39 -0
  109. package/dist/runtime/inputSnapshot.d.ts +10 -1
  110. package/dist/runtime/inputSnapshot.js +13 -1
  111. package/dist/runtime/runtimeSave.d.ts +3 -3
  112. package/dist/runtime/runtimeSave.js +11 -2
  113. package/dist/scene/assetDiagnostics.js +2 -2
  114. package/dist/scene/autoTarget.js +2 -1
  115. package/dist/scene/builtinSceneKinds.d.ts +6 -1
  116. package/dist/scene/builtinSceneKinds.js +10 -3
  117. package/dist/scene/captureCheck.js +3 -2
  118. package/dist/scene/colliders.d.ts +20 -0
  119. package/dist/scene/colliders.js +78 -0
  120. package/dist/scene/collisionMesh.d.ts +8 -0
  121. package/dist/scene/collisionMesh.js +48 -0
  122. package/dist/scene/entityStore.d.ts +4 -1
  123. package/dist/scene/entityStore.js +3 -0
  124. package/dist/scene/objectStore.d.ts +10 -0
  125. package/dist/scene/objectStore.js +29 -6
  126. package/dist/scene/sceneKinds.d.ts +37 -0
  127. package/dist/scene/sceneKinds.js +19 -0
  128. package/dist/scene/sequenceDirector.d.ts +110 -0
  129. package/dist/scene/sequenceDirector.js +146 -0
  130. package/dist/session/roles.js +3 -2
  131. package/dist/settings/settingsModel.d.ts +1 -0
  132. package/dist/settings/settingsModel.js +1 -0
  133. package/dist/stats/rollCheck.js +4 -2
  134. package/dist/time/dayNightCycle.d.ts +114 -0
  135. package/dist/time/dayNightCycle.js +208 -0
  136. package/dist/time/timerSet.d.ts +132 -0
  137. package/dist/time/timerSet.js +243 -0
  138. package/dist/ui/accessibility.d.ts +51 -0
  139. package/dist/ui/accessibility.js +83 -0
  140. package/dist/ui/coachMarks.d.ts +99 -0
  141. package/dist/ui/coachMarks.js +126 -0
  142. package/dist/ui/floatingText.d.ts +114 -0
  143. package/dist/ui/floatingText.js +203 -0
  144. package/dist/ui/hudScale.d.ts +16 -1
  145. package/dist/ui/hudScale.js +22 -1
  146. package/dist/ui/modalStack.d.ts +143 -0
  147. package/dist/ui/modalStack.js +160 -0
  148. package/dist/ui/objectiveBanner.d.ts +123 -0
  149. package/dist/ui/objectiveBanner.js +162 -0
  150. package/dist/ui/photoMode.d.ts +32 -0
  151. package/dist/ui/photoMode.js +51 -0
  152. package/dist/ui/radialMenu.d.ts +47 -0
  153. package/dist/ui/radialMenu.js +74 -0
  154. package/dist/ui/screenMarkers.d.ts +128 -0
  155. package/dist/ui/screenMarkers.js +118 -0
  156. package/dist/ui.d.ts +8 -0
  157. package/dist/ui.js +8 -0
  158. package/dist/vfx/cameraShake.d.ts +97 -0
  159. package/dist/vfx/cameraShake.js +136 -0
  160. package/dist/vfx/damageDirection.d.ts +102 -0
  161. package/dist/vfx/damageDirection.js +188 -0
  162. package/dist/vfx/particles.d.ts +103 -0
  163. package/dist/vfx/particles.js +256 -0
  164. package/dist/vfx/screenEffects.d.ts +149 -0
  165. package/dist/vfx/screenEffects.js +191 -0
  166. package/dist/world/authoredObjects.d.ts +18 -0
  167. package/dist/world/authoredObjects.js +21 -0
  168. package/dist/world/authoredSpawn.js +19 -0
  169. package/dist/world/buildingLots.d.ts +54 -11
  170. package/dist/world/buildingLots.js +116 -30
  171. package/dist/world/cityBlocks.d.ts +45 -0
  172. package/dist/world/cityBlocks.js +159 -0
  173. package/dist/world/cityContent.d.ts +46 -2
  174. package/dist/world/cityContent.js +160 -2
  175. package/dist/world/cityGenerator.d.ts +261 -23
  176. package/dist/world/cityGenerator.js +643 -33
  177. package/dist/world/cityGeometry.d.ts +20 -0
  178. package/dist/world/cityGeometry.js +91 -0
  179. package/dist/world/cityKind.js +9 -46
  180. package/dist/world/fastTravel.d.ts +64 -0
  181. package/dist/world/fastTravel.js +108 -0
  182. package/dist/world/features.d.ts +16 -0
  183. package/dist/world/features.js +2 -0
  184. package/dist/world/grassKind.js +4 -1
  185. package/dist/world/grounded.d.ts +11 -0
  186. package/dist/world/grounded.js +12 -0
  187. package/dist/world/mapAnnotations.d.ts +75 -0
  188. package/dist/world/mapAnnotations.js +139 -0
  189. package/dist/world/markers.js +1 -0
  190. package/dist/world/resourceNode.d.ts +1 -1
  191. package/dist/world/resourceNode.js +2 -1
  192. package/dist/world/roads.d.ts +294 -5
  193. package/dist/world/roads.js +1262 -21
  194. package/dist/world/scatterCoverage.d.ts +98 -0
  195. package/dist/world/scatterCoverage.js +101 -0
  196. package/dist/world/scatterRegion.js +6 -0
  197. package/dist/world/sharedAnnotations.d.ts +84 -0
  198. package/dist/world/sharedAnnotations.js +76 -0
  199. package/dist/world/spawnOverride.d.ts +37 -0
  200. package/dist/world/spawnOverride.js +50 -0
  201. package/dist/world/streetGenerator.d.ts +60 -11
  202. package/dist/world/streetGenerator.js +1786 -129
  203. package/dist/world/terrain.d.ts +1 -0
  204. package/dist/world/terrain.js +2 -0
  205. package/dist/world/waypoints.d.ts +63 -0
  206. package/dist/world/waypoints.js +137 -0
  207. package/dist/world.d.ts +18 -2
  208. package/dist/world.js +18 -2
  209. package/package.json +2 -2
  210. package/skills/ce-handoff/SKILL.md +149 -0
  211. package/skills/game-design/SKILL.md +2 -0
  212. package/skills/jgengine/SKILL.md +7 -3
  213. package/skills/jgengine/api.md +17 -7
  214. package/skills/jgengine/capabilities.md +12 -0
  215. package/skills/jgengine/recipes/minimal-game.md +7 -4
  216. package/skills/jgengine-combat/api.md +40 -4
  217. package/skills/jgengine-combat/capabilities.md +12 -0
  218. package/skills/jgengine-combat/reference.md +2 -0
  219. package/skills/jgengine-gameplay/api.md +212 -11
  220. package/skills/jgengine-gameplay/capabilities.md +66 -1
  221. package/skills/jgengine-world/api.md +408 -12
  222. package/skills/jgengine-world/capabilities.md +127 -2
  223. package/skills/jgengine-world/reference.md +15 -1
@@ -63,6 +63,26 @@ export declare function insetRingUniform(ring: readonly Vec2[], distance: number
63
63
  export declare function polygonMeanWidth(ring: readonly Vec2[]): number;
64
64
  /** All four corners of a rotated rect (center, half-extents, yaw in engine rotationY convention). */
65
65
  export declare function rectCorners(cx: number, cz: number, hw: number, hd: number, rotationY: number): Vec2[];
66
+ /** A rotated rectangle: center, half-extents, and yaw in the engine rotationY convention. */
67
+ export interface OrientedRect {
68
+ x: number;
69
+ z: number;
70
+ hw: number;
71
+ hd: number;
72
+ angle: number;
73
+ }
74
+ /**
75
+ * Separating-axis test for two rotated rectangles: true when a gap exists (no overlap).
76
+ * @capability city-district exact overlap test between two rotated rectangles (plots, lots, footprints)
77
+ */
78
+ export declare function rectsSeparated(a: OrientedRect, b: OrientedRect): boolean;
79
+ /**
80
+ * True when a rotated rect stays at least `clearance` away from every segment of a polyline —
81
+ * the plot-contract test that keeps building plots off road corridors (pass the road half-width
82
+ * as the clearance). Exact segment-vs-expanded-rect intersection, not corner sampling.
83
+ * @capability city-district keep a rotated rect (plot/footprint) clear of a road corridor polyline
84
+ */
85
+ export declare function rectClearsPolyline(rect: OrientedRect, path: readonly Vec2[], clearance: number): boolean;
66
86
  /** True when the whole rotated rect (corners + edge midpoints) sits inside the ring. */
67
87
  export declare function rectInsidePolygon(ring: readonly Vec2[], cx: number, cz: number, hw: number, hd: number, rotationY: number): boolean;
68
88
  /**
@@ -328,6 +328,97 @@ export function rectCorners(cx, cz, hw, hd, rotationY) {
328
328
  }
329
329
  return out;
330
330
  }
331
+ /**
332
+ * Separating-axis test for two rotated rectangles: true when a gap exists (no overlap).
333
+ * @capability city-district exact overlap test between two rotated rectangles (plots, lots, footprints)
334
+ */
335
+ export function rectsSeparated(a, b) {
336
+ const axes = [];
337
+ for (const angle of [a.angle, b.angle]) {
338
+ axes.push([Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]);
339
+ }
340
+ const ca = rectCorners(a.x, a.z, a.hw, a.hd, a.angle);
341
+ const cb = rectCorners(b.x, b.z, b.hw, b.hd, b.angle);
342
+ for (const [ax, az] of axes) {
343
+ let minA = Infinity;
344
+ let maxA = -Infinity;
345
+ let minB = Infinity;
346
+ let maxB = -Infinity;
347
+ for (const [x, z] of ca) {
348
+ const p = x * ax + z * az;
349
+ if (p < minA)
350
+ minA = p;
351
+ if (p > maxA)
352
+ maxA = p;
353
+ }
354
+ for (const [x, z] of cb) {
355
+ const p = x * ax + z * az;
356
+ if (p < minB)
357
+ minB = p;
358
+ if (p > maxB)
359
+ maxB = p;
360
+ }
361
+ if (maxA < minB || maxB < minA)
362
+ return true; // gap on this axis → no overlap
363
+ }
364
+ return false;
365
+ }
366
+ /** True when a segment touches an axis-aligned box (separating-axis: x, z, and the segment normal). */
367
+ function segmentTouchesAabb(ax, az, bx, bz, minX, minZ, maxX, maxZ) {
368
+ if (Math.max(ax, bx) < minX || Math.min(ax, bx) > maxX)
369
+ return false;
370
+ if (Math.max(az, bz) < minZ || Math.min(az, bz) > maxZ)
371
+ return false;
372
+ const nx = -(bz - az);
373
+ const nz = bx - ax;
374
+ const d0 = nx * (minX - ax) + nz * (minZ - az);
375
+ const d1 = nx * (maxX - ax) + nz * (minZ - az);
376
+ const d2 = nx * (maxX - ax) + nz * (maxZ - az);
377
+ const d3 = nx * (minX - ax) + nz * (maxZ - az);
378
+ if (d0 > 0 && d1 > 0 && d2 > 0 && d3 > 0)
379
+ return false;
380
+ if (d0 < 0 && d1 < 0 && d2 < 0 && d3 < 0)
381
+ return false;
382
+ return true;
383
+ }
384
+ /**
385
+ * True when a rotated rect stays at least `clearance` away from every segment of a polyline —
386
+ * the plot-contract test that keeps building plots off road corridors (pass the road half-width
387
+ * as the clearance). Exact segment-vs-expanded-rect intersection, not corner sampling.
388
+ * @capability city-district keep a rotated rect (plot/footprint) clear of a road corridor polyline
389
+ */
390
+ export function rectClearsPolyline(rect, path, clearance) {
391
+ if (path.length < 2)
392
+ return true;
393
+ const c = Math.cos(rect.angle);
394
+ const s = Math.sin(rect.angle);
395
+ const ex = rect.hw + clearance;
396
+ const ez = rect.hd + clearance;
397
+ // Cheap world-space bbox reject before transforming segments into the rect frame.
398
+ const reach = Math.hypot(ex, ez);
399
+ const minX = rect.x - reach;
400
+ const maxX = rect.x + reach;
401
+ const minZ = rect.z - reach;
402
+ const maxZ = rect.z + reach;
403
+ let px = path[0][0];
404
+ let pz = path[0][1];
405
+ let plx = (px - rect.x) * c - (pz - rect.z) * s;
406
+ let plz = (px - rect.x) * s + (pz - rect.z) * c;
407
+ for (let i = 1; i < path.length; i += 1) {
408
+ const qx = path[i][0];
409
+ const qz = path[i][1];
410
+ const inside = !(Math.max(px, qx) < minX || Math.min(px, qx) > maxX || Math.max(pz, qz) < minZ || Math.min(pz, qz) > maxZ);
411
+ const qlx = (qx - rect.x) * c - (qz - rect.z) * s;
412
+ const qlz = (qx - rect.x) * s + (qz - rect.z) * c;
413
+ if (inside && segmentTouchesAabb(plx, plz, qlx, qlz, -ex, -ez, ex, ez))
414
+ return false;
415
+ px = qx;
416
+ pz = qz;
417
+ plx = qlx;
418
+ plz = qlz;
419
+ }
420
+ return true;
421
+ }
331
422
  /** True when the whole rotated rect (corners + edge midpoints) sits inside the ring. */
332
423
  export function rectInsidePolygon(ring, cx, cz, hw, hd, rotationY) {
333
424
  const corners = rectCorners(cx, cz, hw, hd, rotationY);
@@ -32,9 +32,10 @@ import { parseParams, registerSceneKind, } from "../scene/sceneKinds.js";
32
32
  import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_STYLE } from "./buildings.js";
33
33
  import { buildLotPieces, pickClass, pickSpecies, rollClassPlacement, zoneBand, zoneMetric, } from "./cityContent.js";
34
34
  import { furnitureSpots } from "./streets.js";
35
+ import { CITY_BUILDING_BUDGET, budgetWarning, placedCoverage } from "./scatterCoverage.js";
35
36
  import { generateStreets, streetNetworkMode, } from "./streetGenerator.js";
36
37
  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
+ import { fitRectInPolygon, rayDistanceToRing, pointInPolygon, polygonArea, polygonCentroid, polygonMeanWidth, polygonsOverlap, insetRingUniform, rectsSeparated, ringBounds, } from "./cityGeometry.js";
38
39
  /** The editor volume kind marking a box as a procedural city district. */
39
40
  export const CITY_KIND = "city";
40
41
  /** The editor volume kind that locally overrides a city's zone band/mix — a district in a district. */
@@ -567,7 +568,8 @@ export function readCityRules(meta) {
567
568
  }
568
569
  /** Bounded-work caps so a huge volume can never generate unbounded content. */
569
570
  const MAX_STREETS = 320;
570
- const MAX_LOTS = 2600;
571
+ /** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
572
+ const MAX_LOTS = CITY_BUILDING_BUDGET;
571
573
  const MAX_INTERSECTIONS = 600;
572
574
  const MAX_TREES = 3200;
573
575
  const MAX_LIGHTS = 1200;
@@ -664,49 +666,6 @@ class StreetIndex {
664
666
  return null;
665
667
  }
666
668
  }
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
669
  /** Broadphase grid over placed lots so overlap tests only touch neighbors. */
711
670
  class LotIndex {
712
671
  cells = new Map();
@@ -1530,6 +1489,7 @@ export function registerCityKind() {
1530
1489
  addCategory: "Studios",
1531
1490
  accent: "#8fa8c9",
1532
1491
  schema: CITY_SCHEMA,
1492
+ coverage: { spec: "city", densityKey: "roadsideOccupancy" },
1533
1493
  resolve: (object, _params, context) => resolveCityObject(object, context),
1534
1494
  note: (object) => {
1535
1495
  const resolved = resolveCityObject(object);
@@ -1558,7 +1518,10 @@ export function registerCityKind() {
1558
1518
  resolved.tunnels.length > 0 ? `${resolved.tunnels.length} tunnels` : "",
1559
1519
  ].filter((s) => s.length > 0);
1560
1520
  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";
1521
+ // Shared clamp-and-warn: when the block/parcel pipeline hits the building budget, surface it in
1522
+ // the same "capped at N (budget)" wording grass and scatter use.
1523
+ const buildingCap = resolved.rules.fabric ? budgetWarning(placedCoverage("city", 0, resolved.lots.length)) : "";
1524
+ const fabric = resolved.rules.fabric ? ` · ${resolved.lots.length} buildings${buildingCap} · ${resolved.parks.length} parks` : " · no fabric";
1562
1525
  return `${head}${fabric}${feats.length > 0 ? ` · ${feats.join(" · ")}` : ""}`;
1563
1526
  },
1564
1527
  });
@@ -0,0 +1,64 @@
1
+ import type { WorldXZ } from "./minimap.js";
2
+ /** A fast-travel destination the game defines. Discovery is tracked separately. */
3
+ export interface TravelPointDef<TMeta = unknown> {
4
+ id: string;
5
+ name: string;
6
+ /** World-XZ location — feeds distance sorting and map markers. */
7
+ position: WorldXZ;
8
+ region?: string;
9
+ icon?: string;
10
+ /** Discovered from the start (a home base / starting waypoint). */
11
+ initial?: boolean;
12
+ meta?: TMeta;
13
+ }
14
+ /** A destination plus its discovery state (and distance from a query origin, when given). */
15
+ export interface TravelPointView<TMeta = unknown> extends TravelPointDef<TMeta> {
16
+ discovered: boolean;
17
+ discoveredAt: number | null;
18
+ /** XZ distance from the query origin passed to `list`/`destinations`/`nearest`, else undefined. */
19
+ distance?: number;
20
+ }
21
+ /** Serializable discovery state. */
22
+ export interface FastTravelSnapshot {
23
+ discovered: Record<string, number>;
24
+ }
25
+ /** A network of fast-travel points with per-player discovery + distance queries. */
26
+ export interface FastTravelNetwork<TMeta = unknown> {
27
+ /** Unlock a point; returns the newly-discovered view, or null if unknown or already discovered. */
28
+ discover(id: string): TravelPointView<TMeta> | null;
29
+ isDiscovered(id: string): boolean;
30
+ get(id: string): TravelPointView<TMeta> | null;
31
+ /** All points (discovery state + optional distance), in definition order — or by distance when `from` is given. */
32
+ list(from?: WorldXZ): readonly TravelPointView<TMeta>[];
33
+ /** Discovered points only (distance-sorted when `from` is given). */
34
+ destinations(from?: WorldXZ): readonly TravelPointView<TMeta>[];
35
+ /** Nearest discovered point to `from` (optionally excluding an id — e.g. the one you're standing on). */
36
+ nearest(from: WorldXZ, options?: {
37
+ exclude?: string;
38
+ }): TravelPointView<TMeta> | null;
39
+ /** Whether a destination can be traveled to (discovered). Combat/cost gates are the game's to add. */
40
+ canTravel(id: string): boolean;
41
+ discoveredCount(): number;
42
+ total(): number;
43
+ subscribe(listener: () => void): () => void;
44
+ snapshot(): FastTravelSnapshot;
45
+ restore(snapshot: FastTravelSnapshot): void;
46
+ }
47
+ /** Options for {@link createFastTravelNetwork}. */
48
+ export interface FastTravelOptions<TMeta = unknown> {
49
+ points: readonly TravelPointDef<TMeta>[];
50
+ /** Injected clock (ms). Default `Date.now`. */
51
+ now?: () => number;
52
+ /** Fires when a point is discovered for the first time — wire to a toast/notification. */
53
+ onDiscover?: (point: TravelPointView<TMeta>) => void;
54
+ }
55
+ /**
56
+ * A fast-travel network: defined destinations plus per-player discovery, with
57
+ * distance-sorted queries, a `nearest` lookup, a `canTravel` gate, an
58
+ * `onDiscover` seam, and serializable `snapshot`/`restore`. Travel itself is a
59
+ * teleport the game applies to `TravelPointView.position`; this owns the
60
+ * unlock/where-can-I-go state. Points marked `initial` start discovered.
61
+ *
62
+ * @capability fast-travel network of fast-travel destinations with discovery, distance-sorted queries, nearest lookup, a travel gate, and serializable state
63
+ */
64
+ export declare function createFastTravelNetwork<TMeta = unknown>(options: FastTravelOptions<TMeta>): FastTravelNetwork<TMeta>;
@@ -0,0 +1,108 @@
1
+ function distanceXZ(a, b) {
2
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
3
+ }
4
+ /**
5
+ * A fast-travel network: defined destinations plus per-player discovery, with
6
+ * distance-sorted queries, a `nearest` lookup, a `canTravel` gate, an
7
+ * `onDiscover` seam, and serializable `snapshot`/`restore`. Travel itself is a
8
+ * teleport the game applies to `TravelPointView.position`; this owns the
9
+ * unlock/where-can-I-go state. Points marked `initial` start discovered.
10
+ *
11
+ * @capability fast-travel network of fast-travel destinations with discovery, distance-sorted queries, nearest lookup, a travel gate, and serializable state
12
+ */
13
+ export function createFastTravelNetwork(options) {
14
+ const now = options.now ?? Date.now;
15
+ const order = [];
16
+ const defs = new Map();
17
+ const discoveredAt = new Map();
18
+ for (const def of options.points) {
19
+ if (defs.has(def.id))
20
+ continue;
21
+ defs.set(def.id, def);
22
+ order.push(def.id);
23
+ if (def.initial === true)
24
+ discoveredAt.set(def.id, 0);
25
+ }
26
+ const listeners = new Set();
27
+ function notify() {
28
+ for (const listener of listeners)
29
+ listener();
30
+ }
31
+ function viewOf(id, from) {
32
+ const def = defs.get(id);
33
+ const at = discoveredAt.get(id) ?? null;
34
+ return {
35
+ ...def,
36
+ discovered: at !== null,
37
+ discoveredAt: at,
38
+ ...(from !== undefined ? { distance: distanceXZ(def.position, from) } : {}),
39
+ };
40
+ }
41
+ function views(from) {
42
+ const all = order.map((id) => viewOf(id, from));
43
+ if (from !== undefined)
44
+ all.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0));
45
+ return all;
46
+ }
47
+ const network = {
48
+ discover(id) {
49
+ if (!defs.has(id) || discoveredAt.has(id))
50
+ return null;
51
+ discoveredAt.set(id, now());
52
+ const view = viewOf(id);
53
+ options.onDiscover?.(view);
54
+ notify();
55
+ return view;
56
+ },
57
+ isDiscovered(id) {
58
+ return discoveredAt.has(id);
59
+ },
60
+ get(id) {
61
+ return defs.has(id) ? viewOf(id) : null;
62
+ },
63
+ list(from) {
64
+ return views(from);
65
+ },
66
+ destinations(from) {
67
+ return views(from).filter((view) => view.discovered);
68
+ },
69
+ nearest(from, opts = {}) {
70
+ let best = null;
71
+ for (const view of views(from)) {
72
+ if (!view.discovered || view.id === opts.exclude)
73
+ continue;
74
+ if (best === null || (view.distance ?? 0) < (best.distance ?? 0))
75
+ best = view;
76
+ }
77
+ return best;
78
+ },
79
+ canTravel(id) {
80
+ return discoveredAt.has(id);
81
+ },
82
+ discoveredCount() {
83
+ return discoveredAt.size;
84
+ },
85
+ total() {
86
+ return order.length;
87
+ },
88
+ subscribe(listener) {
89
+ listeners.add(listener);
90
+ return () => listeners.delete(listener);
91
+ },
92
+ snapshot() {
93
+ return { discovered: Object.fromEntries(discoveredAt) };
94
+ },
95
+ restore(snapshot) {
96
+ discoveredAt.clear();
97
+ for (const id of order)
98
+ if (defs.get(id).initial === true)
99
+ discoveredAt.set(id, 0);
100
+ for (const [id, at] of Object.entries(snapshot.discovered)) {
101
+ if (defs.has(id))
102
+ discoveredAt.set(id, at);
103
+ }
104
+ notify();
105
+ },
106
+ };
107
+ return network;
108
+ }
@@ -76,6 +76,14 @@ export interface TerrainDetailMaterialConfig {
76
76
  repeat?: number;
77
77
  /** Blend strength of the texture over the procedural/vertex-colour base, 0..1. Default 1. */
78
78
  strength?: number;
79
+ /**
80
+ * How much of the map's OWN colour replaces the terrain's authored colour, 0..1. Default 1 —
81
+ * at which the map's albedo wins and the biome tint, macro sweeps, and slope-rock blend under it
82
+ * are painted over, so a whole region flattens to the texture's single hue. Drop toward 0 to keep
83
+ * the authored colour and take only the map's grain (its light and shade), which is what a
84
+ * hand-tinted world wants from a stock PBR set.
85
+ */
86
+ tint?: number;
79
87
  }
80
88
  export interface TerrainFlattenMask {
81
89
  center: EnvironmentVec2;
@@ -283,6 +291,14 @@ export interface SkyEnvironmentConfig {
283
291
  hazeStrength?: number;
284
292
  /** Sun-glow brightness multiplier around the sun disc. Default 1. */
285
293
  sunGlowStrength?: number;
294
+ /**
295
+ * How fast the gradient reaches `zenithColor` off the horizon. Below 1 pulls the zenith hue down
296
+ * into the frame — the difference between a sky an eye-level camera actually sees and a horizon
297
+ * wash; above 1 keeps colour banked overhead. Default 0.65.
298
+ */
299
+ gradientExponent?: number;
300
+ /** Procedural cloud-band opacity over the gradient: 0 is a clear sky, 1 is heavy overcast. Default 0.35. */
301
+ cloudiness?: number;
286
302
  fog?: {
287
303
  color?: string;
288
304
  near?: number;
@@ -122,6 +122,8 @@ export function sky(config = {}) {
122
122
  ...(config.radius === undefined ? {} : { radius: config.radius }),
123
123
  ...(config.hazeStrength === undefined ? {} : { hazeStrength: config.hazeStrength }),
124
124
  ...(config.sunGlowStrength === undefined ? {} : { sunGlowStrength: config.sunGlowStrength }),
125
+ ...(config.gradientExponent === undefined ? {} : { gradientExponent: config.gradientExponent }),
126
+ ...(config.cloudiness === undefined ? {} : { cloudiness: config.cloudiness }),
125
127
  ...(config.fog === undefined ? {} : { fog: config.fog }),
126
128
  ...(config.volumetricClouds === undefined ? {} : { volumetricClouds: config.volumetricClouds }),
127
129
  });
@@ -8,6 +8,7 @@
8
8
  * @capability grass-field editor-authorable GPU grass patch
9
9
  */
10
10
  import { registerSceneKind } from "../scene/sceneKinds.js";
11
+ import { budgetWarning, densityCoverage } from "./scatterCoverage.js";
11
12
  /** The editor volume kind marking a box as a GPU grass patch. */
12
13
  export const GRASS_FIELD_KIND = "grass_field";
13
14
  /** Grass defaults: a lush, lightly-swaying meadow. */
@@ -89,13 +90,15 @@ export function registerGrassKind() {
89
90
  addCategory: "Studios",
90
91
  accent: "#7fbf4d",
91
92
  schema: GRASS_SCHEMA,
93
+ coverage: { spec: "grass_field", densityKey: "density" },
92
94
  resolve: (object) => resolveGrassObject(object),
93
95
  note: (object) => {
94
96
  const resolved = resolveGrassObject(object);
95
97
  if (resolved === null)
96
98
  return "Give the volume a box footprint.";
97
99
  const area = resolved.size[0] * resolved.size[1];
98
- return `${Math.round(resolved.size[0])}×${Math.round(resolved.size[1])} m · ≈${Math.round(area * resolved.rules.density).toLocaleString()} blades`;
100
+ const coverage = densityCoverage("grass_field", area, resolved.rules.density);
101
+ return `${Math.round(resolved.size[0])}×${Math.round(resolved.size[1])} m · ≈ ${coverage.count.toLocaleString()} blades${budgetWarning(coverage)}`;
99
102
  },
100
103
  });
101
104
  }
@@ -0,0 +1,11 @@
1
+ import type { GameContext } from "../runtime/gameContext.js";
2
+ /**
3
+ * Ground an XZ point onto the live world surface as `[x, groundY, z]`.
4
+ *
5
+ * The one-line pose pattern every game re-derives as
6
+ * `[x, ctx.world.groundHeightAt(x, z), z]` — use this instead so spawn,
7
+ * placement, and AI pose sites stay consistent.
8
+ *
9
+ * @capability grounded ground an XZ point onto the world surface as a pose triple
10
+ */
11
+ export declare function grounded(ctx: Pick<GameContext, "world">, x: number, z: number): readonly [number, number, number];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Ground an XZ point onto the live world surface as `[x, groundY, z]`.
3
+ *
4
+ * The one-line pose pattern every game re-derives as
5
+ * `[x, ctx.world.groundHeightAt(x, z), z]` — use this instead so spawn,
6
+ * placement, and AI pose sites stay consistent.
7
+ *
8
+ * @capability grounded ground an XZ point onto the world surface as a pose triple
9
+ */
10
+ export function grounded(ctx, x, z) {
11
+ return [x, ctx.world.groundHeightAt(x, z), z];
12
+ }
@@ -0,0 +1,75 @@
1
+ import type { WorldXZ } from "./minimap.js";
2
+ import type { MapLayerTone, MapRoute, MapZone, MapZoneShape } from "./mapLayers.js";
3
+ /** A freehand drawn line on the map — a world-XZ polyline. */
4
+ export interface MapStroke {
5
+ id: string;
6
+ points: readonly WorldXZ[];
7
+ tone?: MapLayerTone;
8
+ /** Line width in map pixels. */
9
+ width?: number;
10
+ }
11
+ /** A drawn area annotation (circle/rect/polygon), reusing the map-zone shape vocabulary. */
12
+ export interface MapShapeAnnotation {
13
+ id: string;
14
+ shape: MapZoneShape;
15
+ tone?: MapLayerTone;
16
+ label?: string;
17
+ }
18
+ /** A text note pinned at a world point. */
19
+ export interface MapNote {
20
+ id: string;
21
+ position: WorldXZ;
22
+ text: string;
23
+ tone?: MapLayerTone;
24
+ }
25
+ /** Whole serializable state of an annotation layer — drop into a save blob. */
26
+ export interface AnnotationSnapshot {
27
+ strokes: readonly MapStroke[];
28
+ shapes: readonly MapShapeAnnotation[];
29
+ notes: readonly MapNote[];
30
+ }
31
+ /** Player-drawn map annotation layer — strokes, shapes, and notes over caller-owned map data. */
32
+ export interface AnnotationLayer {
33
+ /** Add a freehand stroke; returns its id. Ignored (returns "") when fewer than 2 points. */
34
+ addStroke(points: readonly WorldXZ[], options?: {
35
+ id?: string;
36
+ tone?: MapLayerTone;
37
+ width?: number;
38
+ }): string;
39
+ addShape(shape: MapZoneShape, options?: {
40
+ id?: string;
41
+ tone?: MapLayerTone;
42
+ label?: string;
43
+ }): string;
44
+ addNote(position: WorldXZ, text: string, options?: {
45
+ id?: string;
46
+ tone?: MapLayerTone;
47
+ }): string;
48
+ remove(id: string): boolean;
49
+ clear(): void;
50
+ strokes(): readonly MapStroke[];
51
+ shapes(): readonly MapShapeAnnotation[];
52
+ notes(): readonly MapNote[];
53
+ /** Project strokes into `MapRoute`s for a map's `routes` prop. Stable identity between changes. */
54
+ routes(): readonly MapRoute[];
55
+ /** Project shapes into `MapZone`s for a map's `zones` prop. Stable identity between changes. */
56
+ zones(): readonly MapZone[];
57
+ subscribe(listener: () => void): () => void;
58
+ snapshot(): AnnotationSnapshot;
59
+ restore(snapshot: AnnotationSnapshot): void;
60
+ }
61
+ /** Options for {@link createAnnotationLayer}. */
62
+ export interface AnnotationLayerOptions {
63
+ /** Default tone applied to strokes/shapes/notes added without an explicit one. Default `"info"`. */
64
+ defaultTone?: MapLayerTone;
65
+ }
66
+ /**
67
+ * Player-drawn map annotation layer: freehand `strokes`, area `shapes`, and
68
+ * pinned `notes`, all world-XZ and serializable. `routes()`/`zones()` project
69
+ * strokes/shapes into the exact shapes `Minimap`/`WorldMap`/`FullscreenMap`
70
+ * already render via their `routes`/`zones` props, so drawing needs no new
71
+ * renderer. State is plain data; `snapshot`/`restore` round-trip through a save.
72
+ *
73
+ * @capability map-annotations player-drawn map annotation layer — freehand strokes, area shapes, and pinned notes, projected into the map's routes/zones props and serializable
74
+ */
75
+ export declare function createAnnotationLayer(options?: AnnotationLayerOptions): AnnotationLayer;