@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
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The shared "how much stuff grows here" contract for scatterable scene kinds
3
+ * (`grass_field` / `scatter` / `city`). Every scatterable kind fills an authored AREA with instances
4
+ * at a DENSITY, and every kind caps the placed count at a bounded BUDGET so a huge or dense request
5
+ * can never generate unbounded work. Before this module each kind hand-rolled its own readout and
6
+ * clamp story (grass silently capped in the renderer, scatter never capped, city had no budget note);
7
+ * this is the one place that
8
+ *
9
+ * 1. names the per-kind unit noun + placement budget ({@link SCATTER_COVERAGE_SPECS}),
10
+ * 2. turns an area + density into a `requested` / placed `count` / `capped` triple
11
+ * ({@link densityCoverage}, {@link placedCoverage}), and
12
+ * 3. renders the identical clamp-and-warn clause the inspector surfaces —
13
+ * `requested N, capped at M (budget)` — via {@link budgetWarning} / {@link describeScatterCoverage}.
14
+ *
15
+ * Pure data + string helpers, no rendering. Each kind's `note` and the editor's shared
16
+ * `CoverageSection` consume it, so grass, scatter, and city tell one coverage story.
17
+ *
18
+ * @capability scatter-coverage shared density/budget semantics for scatterable kinds
19
+ */
20
+ /** The scatterable kinds that share the coverage contract. */
21
+ export type ScatterCoverageKind = "grass_field" | "scatter" | "city";
22
+ /**
23
+ * Max blade/placement instances a single `grass_field` or `scatter` patch places. Mirrors the grass
24
+ * renderer's instance-buffer ceiling; the scatter resolver truncates to it too, so a patch never
25
+ * renders more than this many placements regardless of density × area.
26
+ */
27
+ export declare const SCATTER_INSTANCE_BUDGET = 250000;
28
+ /** Max buildings a single `city` district generates — the block/parcel pipeline's bounded lot cap. */
29
+ export declare const CITY_BUILDING_BUDGET = 2600;
30
+ /** Per-kind coverage descriptor: the unit noun, whether density is per-m², and the placement budget. */
31
+ export interface ScatterCoverageSpec {
32
+ kind: ScatterCoverageKind;
33
+ /** Plural noun for one placed instance: `blades` | `placements` | `buildings`. */
34
+ unit: string;
35
+ /** Density is per-square-meter (grass/scatter). City fills by street fabric, not by a per-m² dial. */
36
+ perArea: boolean;
37
+ /** Hard cap on placed instances for one patch/district. */
38
+ budget: number;
39
+ }
40
+ /** The coverage spec per scatterable kind — the single source for its unit + budget. */
41
+ export declare const SCATTER_COVERAGE_SPECS: Record<ScatterCoverageKind, ScatterCoverageSpec>;
42
+ /** The coverage spec for a scatterable kind, or `null` when the kind is not scatterable. */
43
+ export declare function scatterCoverageSpec(kind: string): ScatterCoverageSpec | null;
44
+ /**
45
+ * A resolved coverage readout: how many instances an authored patch WANTS (`requested`, when it can
46
+ * be computed cheaply from geometry × density), how many it will actually place after the budget
47
+ * clamp (`count`), the `budget` ceiling, and whether the request hit it (`capped`). `requested` is
48
+ * `null` for kinds whose pre-cap ask is not knowable without running the generator (city, where the
49
+ * count comes from the resolved street fabric).
50
+ */
51
+ export interface ScatterCoverage {
52
+ kind: ScatterCoverageKind;
53
+ unit: string;
54
+ /** Footprint area in m² (0 when unknown / not applicable). */
55
+ area: number;
56
+ /** Instances the density asks for before the budget clamp; `null` when only the placed count is known. */
57
+ requested: number | null;
58
+ /** Instances actually placed: `min(requested, budget)`, or the generator's reported count. */
59
+ count: number;
60
+ budget: number;
61
+ capped: boolean;
62
+ }
63
+ /**
64
+ * Footprint area (m²) of a scatterable object: a closed path polygon (shoelace), a volume box
65
+ * (`halfExtents`), or a disc (`radius`). Structural so it accepts a `SceneKindObject`, an editor
66
+ * volume, or a scatter path without a hard type dependency.
67
+ */
68
+ export declare function scatterFootprintArea(object: {
69
+ points?: readonly {
70
+ x: number;
71
+ z: number;
72
+ }[];
73
+ halfExtents?: {
74
+ x: number;
75
+ z: number;
76
+ };
77
+ radius?: number;
78
+ }): number;
79
+ /**
80
+ * Coverage for a per-m² kind (`grass_field` / `scatter`): `requested = floor(density × area)`, clamped
81
+ * to the kind's budget. `capped` is true when the request exceeds the budget.
82
+ */
83
+ export declare function densityCoverage(kind: "grass_field" | "scatter", area: number, density: number): ScatterCoverage;
84
+ /**
85
+ * Coverage for a kind whose placed `count` is already known from its generator (city, from its
86
+ * resolved lots). No pre-cap `requested` is available — the generator stops at the budget — so
87
+ * `capped` is inferred from the placed count reaching the ceiling.
88
+ */
89
+ export declare function placedCoverage(kind: ScatterCoverageKind, area: number, placed: number): ScatterCoverage;
90
+ /**
91
+ * The shared clamp-and-warn clause every scatterable kind appends, worded identically: `""` when
92
+ * under budget, else ` · requested N, capped at M (budget)` when the pre-cap ask is known, or
93
+ * ` · capped at M (budget)` when only the ceiling is (city). This is the "surfaced, never silent"
94
+ * budget signal from #1112.
95
+ */
96
+ export declare function budgetWarning(coverage: ScatterCoverage): string;
97
+ /** The full one-line coverage readout: `≈ N unit · A m²` plus the {@link budgetWarning} when capped. */
98
+ export declare function describeScatterCoverage(coverage: ScatterCoverage): string;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The shared "how much stuff grows here" contract for scatterable scene kinds
3
+ * (`grass_field` / `scatter` / `city`). Every scatterable kind fills an authored AREA with instances
4
+ * at a DENSITY, and every kind caps the placed count at a bounded BUDGET so a huge or dense request
5
+ * can never generate unbounded work. Before this module each kind hand-rolled its own readout and
6
+ * clamp story (grass silently capped in the renderer, scatter never capped, city had no budget note);
7
+ * this is the one place that
8
+ *
9
+ * 1. names the per-kind unit noun + placement budget ({@link SCATTER_COVERAGE_SPECS}),
10
+ * 2. turns an area + density into a `requested` / placed `count` / `capped` triple
11
+ * ({@link densityCoverage}, {@link placedCoverage}), and
12
+ * 3. renders the identical clamp-and-warn clause the inspector surfaces —
13
+ * `requested N, capped at M (budget)` — via {@link budgetWarning} / {@link describeScatterCoverage}.
14
+ *
15
+ * Pure data + string helpers, no rendering. Each kind's `note` and the editor's shared
16
+ * `CoverageSection` consume it, so grass, scatter, and city tell one coverage story.
17
+ *
18
+ * @capability scatter-coverage shared density/budget semantics for scatterable kinds
19
+ */
20
+ /**
21
+ * Max blade/placement instances a single `grass_field` or `scatter` patch places. Mirrors the grass
22
+ * renderer's instance-buffer ceiling; the scatter resolver truncates to it too, so a patch never
23
+ * renders more than this many placements regardless of density × area.
24
+ */
25
+ export const SCATTER_INSTANCE_BUDGET = 250_000;
26
+ /** Max buildings a single `city` district generates — the block/parcel pipeline's bounded lot cap. */
27
+ export const CITY_BUILDING_BUDGET = 2_600;
28
+ /** The coverage spec per scatterable kind — the single source for its unit + budget. */
29
+ export const SCATTER_COVERAGE_SPECS = {
30
+ grass_field: { kind: "grass_field", unit: "blades", perArea: true, budget: SCATTER_INSTANCE_BUDGET },
31
+ scatter: { kind: "scatter", unit: "placements", perArea: true, budget: SCATTER_INSTANCE_BUDGET },
32
+ city: { kind: "city", unit: "buildings", perArea: false, budget: CITY_BUILDING_BUDGET },
33
+ };
34
+ /** The coverage spec for a scatterable kind, or `null` when the kind is not scatterable. */
35
+ export function scatterCoverageSpec(kind) {
36
+ return SCATTER_COVERAGE_SPECS[kind] ?? null;
37
+ }
38
+ const nonNegative = (n) => (Number.isFinite(n) && n > 0 ? n : 0);
39
+ /**
40
+ * Footprint area (m²) of a scatterable object: a closed path polygon (shoelace), a volume box
41
+ * (`halfExtents`), or a disc (`radius`). Structural so it accepts a `SceneKindObject`, an editor
42
+ * volume, or a scatter path without a hard type dependency.
43
+ */
44
+ export function scatterFootprintArea(object) {
45
+ const points = object.points;
46
+ if (points !== undefined && points.length >= 3) {
47
+ let sum = 0;
48
+ for (let i = 0, j = points.length - 1; i < points.length; j = i, i += 1) {
49
+ sum += points[j].x * points[i].z - points[i].x * points[j].z;
50
+ }
51
+ return Math.abs(sum) / 2;
52
+ }
53
+ const he = object.halfExtents;
54
+ if (he !== undefined)
55
+ return nonNegative(Math.abs(he.x) * 2) * nonNegative(Math.abs(he.z) * 2);
56
+ if (object.radius !== undefined)
57
+ return Math.PI * nonNegative(object.radius) ** 2;
58
+ return 0;
59
+ }
60
+ /**
61
+ * Coverage for a per-m² kind (`grass_field` / `scatter`): `requested = floor(density × area)`, clamped
62
+ * to the kind's budget. `capped` is true when the request exceeds the budget.
63
+ */
64
+ export function densityCoverage(kind, area, density) {
65
+ const spec = SCATTER_COVERAGE_SPECS[kind];
66
+ const a = nonNegative(area);
67
+ const requested = Math.floor(nonNegative(density) * a);
68
+ const count = Math.min(requested, spec.budget);
69
+ return { kind, unit: spec.unit, area: a, requested, count, budget: spec.budget, capped: requested > spec.budget };
70
+ }
71
+ /**
72
+ * Coverage for a kind whose placed `count` is already known from its generator (city, from its
73
+ * resolved lots). No pre-cap `requested` is available — the generator stops at the budget — so
74
+ * `capped` is inferred from the placed count reaching the ceiling.
75
+ */
76
+ export function placedCoverage(kind, area, placed) {
77
+ const spec = SCATTER_COVERAGE_SPECS[kind];
78
+ const count = Math.max(0, Math.floor(nonNegative(placed)));
79
+ return { kind, unit: spec.unit, area: nonNegative(area), requested: null, count, budget: spec.budget, capped: count >= spec.budget };
80
+ }
81
+ const fmt = (n) => Math.round(n).toLocaleString();
82
+ /**
83
+ * The shared clamp-and-warn clause every scatterable kind appends, worded identically: `""` when
84
+ * under budget, else ` · requested N, capped at M (budget)` when the pre-cap ask is known, or
85
+ * ` · capped at M (budget)` when only the ceiling is (city). This is the "surfaced, never silent"
86
+ * budget signal from #1112.
87
+ */
88
+ export function budgetWarning(coverage) {
89
+ if (!coverage.capped)
90
+ return "";
91
+ if (coverage.requested !== null && coverage.requested > coverage.budget) {
92
+ return ` · requested ${fmt(coverage.requested)}, capped at ${fmt(coverage.budget)} (budget)`;
93
+ }
94
+ return ` · capped at ${fmt(coverage.budget)} (budget)`;
95
+ }
96
+ /** The full one-line coverage readout: `≈ N unit · A m²` plus the {@link budgetWarning} when capped. */
97
+ export function describeScatterCoverage(coverage) {
98
+ const head = `≈ ${fmt(coverage.count)} ${coverage.unit}`;
99
+ const area = coverage.area > 0 ? ` · ${fmt(coverage.area)} m²` : "";
100
+ return `${head}${area}${budgetWarning(coverage)}`;
101
+ }
@@ -1,5 +1,6 @@
1
1
  import { masksInfluence, } from "./geometry.js";
2
2
  import { scatter } from "./scatter.js";
3
+ import { SCATTER_INSTANCE_BUDGET } from "./scatterCoverage.js";
3
4
  /** The editor path kind that marks a closed polyline as a foliage/scatter region. */
4
5
  export const SCATTER_PATH_KIND = "scatter";
5
6
  /** Defaults a bare scatter region fills with: sparse grass, lightly spaced. */
@@ -192,6 +193,11 @@ export function resolveScatterRegion(region, terrain, avoid) {
192
193
  scale: rules.minScale + (rules.maxScale - rules.minScale) * scaleRoll,
193
194
  ...(normal === undefined ? {} : { normal }),
194
195
  });
196
+ // Bounded work: a single region never emits more than the shared instance budget, matching the
197
+ // grass patch cap so the inspector's "capped at N (budget)" note is truthful. Placements are
198
+ // generated in deterministic scatter order, so the truncated set is deterministic too.
199
+ if (instances.length >= SCATTER_INSTANCE_BUDGET)
200
+ break;
195
201
  }
196
202
  return instances;
197
203
  }
@@ -0,0 +1,84 @@
1
+ import type { AnnotationLayer } from "./mapAnnotations.js";
2
+ import type { MapLayerTone, MapZoneShape } from "./mapLayers.js";
3
+ import type { WorldXZ } from "./minimap.js";
4
+ /** Feed action shared drawings ride by default (pair with the party feed, like pings). */
5
+ export declare const ANNOTATION_FEED_ACTION = "map.annotation";
6
+ /** A serializable map-annotation edit broadcast to other players. */
7
+ export type AnnotationBroadcast = {
8
+ op: "stroke";
9
+ from: string;
10
+ id: string;
11
+ points: readonly WorldXZ[];
12
+ tone?: MapLayerTone;
13
+ width?: number;
14
+ } | {
15
+ op: "shape";
16
+ from: string;
17
+ id: string;
18
+ shape: MapZoneShape;
19
+ tone?: MapLayerTone;
20
+ label?: string;
21
+ } | {
22
+ op: "note";
23
+ from: string;
24
+ id: string;
25
+ position: WorldXZ;
26
+ text: string;
27
+ tone?: MapLayerTone;
28
+ } | {
29
+ op: "remove";
30
+ from: string;
31
+ id: string;
32
+ } | {
33
+ op: "clear";
34
+ from: string;
35
+ };
36
+ /** The feed sink shared annotations push to — an alias of the ping/feed `push` seam. */
37
+ export interface AnnotationFeedSink {
38
+ push(action: string, entry: unknown): void;
39
+ }
40
+ /** Construction options for {@link createSharedAnnotations}. */
41
+ export interface SharedAnnotationsDeps {
42
+ /** The local annotation layer edits apply to (and inbound edits mirror into). */
43
+ layer: AnnotationLayer;
44
+ /** Feed to broadcast local edits on (typically `ctx.game.feed`). */
45
+ feed: AnnotationFeedSink;
46
+ /** This player's id — stamped on broadcasts and used to drop our own echoes. */
47
+ from: string;
48
+ /** Feed action to publish/consume. Default {@link ANNOTATION_FEED_ACTION}. */
49
+ action?: string;
50
+ }
51
+ /**
52
+ * Local annotation edits that also broadcast, plus `apply` for inbound edits.
53
+ * `add*`/`remove`/`clear` mirror {@link AnnotationLayer} but return globally-unique
54
+ * ids so two clients never collide.
55
+ */
56
+ export interface SharedAnnotations {
57
+ addStroke(points: readonly WorldXZ[], options?: {
58
+ tone?: MapLayerTone;
59
+ width?: number;
60
+ }): string;
61
+ addShape(shape: MapZoneShape, options?: {
62
+ tone?: MapLayerTone;
63
+ label?: string;
64
+ }): string;
65
+ addNote(position: WorldXZ, text: string, options?: {
66
+ tone?: MapLayerTone;
67
+ }): string;
68
+ remove(id: string): boolean;
69
+ clear(): void;
70
+ /** Mirror an inbound broadcast from another player; own echoes (matching `from`) are ignored. */
71
+ apply(payload: AnnotationBroadcast): void;
72
+ feedAction: string;
73
+ }
74
+ /**
75
+ * Make a map annotation layer collaborative: local `addStroke`/`addShape`/
76
+ * `addNote`/`remove`/`clear` apply then broadcast a serializable edit over the
77
+ * party feed (the same seam `createPingSystem` uses), and `apply` mirrors inbound
78
+ * edits from other players — dropping our own echoes. Ids are globally unique per
79
+ * client so two players' strokes never collide. Transport-agnostic: wire `feed`
80
+ * to any replicated feed and route received entries into `apply`.
81
+ *
82
+ * @capability shared-annotations broadcast local map-drawing edits over the party feed and mirror inbound edits — collaborative annotations over any replicated feed
83
+ */
84
+ export declare function createSharedAnnotations(deps: SharedAnnotationsDeps): SharedAnnotations;
@@ -0,0 +1,76 @@
1
+ /** Feed action shared drawings ride by default (pair with the party feed, like pings). */
2
+ export const ANNOTATION_FEED_ACTION = "map.annotation";
3
+ /**
4
+ * Make a map annotation layer collaborative: local `addStroke`/`addShape`/
5
+ * `addNote`/`remove`/`clear` apply then broadcast a serializable edit over the
6
+ * party feed (the same seam `createPingSystem` uses), and `apply` mirrors inbound
7
+ * edits from other players — dropping our own echoes. Ids are globally unique per
8
+ * client so two players' strokes never collide. Transport-agnostic: wire `feed`
9
+ * to any replicated feed and route received entries into `apply`.
10
+ *
11
+ * @capability shared-annotations broadcast local map-drawing edits over the party feed and mirror inbound edits — collaborative annotations over any replicated feed
12
+ */
13
+ export function createSharedAnnotations(deps) {
14
+ const { layer, feed, from } = deps;
15
+ const action = deps.action ?? ANNOTATION_FEED_ACTION;
16
+ let counter = 0;
17
+ function nextId() {
18
+ counter += 1;
19
+ return `${from}:${counter}`;
20
+ }
21
+ function broadcast(payload) {
22
+ feed.push(action, payload);
23
+ }
24
+ return {
25
+ feedAction: action,
26
+ addStroke(points, options = {}) {
27
+ const id = nextId();
28
+ layer.addStroke(points, { id, ...options });
29
+ broadcast({ op: "stroke", from, id, points: points.map((p) => [p[0], p[1]]), ...options });
30
+ return id;
31
+ },
32
+ addShape(shape, options = {}) {
33
+ const id = nextId();
34
+ layer.addShape(shape, { id, ...options });
35
+ broadcast({ op: "shape", from, id, shape, ...options });
36
+ return id;
37
+ },
38
+ addNote(position, text, options = {}) {
39
+ const id = nextId();
40
+ layer.addNote(position, text, { id, ...options });
41
+ broadcast({ op: "note", from, id, position: [position[0], position[1]], text, ...options });
42
+ return id;
43
+ },
44
+ remove(id) {
45
+ const existed = layer.remove(id);
46
+ if (existed)
47
+ broadcast({ op: "remove", from, id });
48
+ return existed;
49
+ },
50
+ clear() {
51
+ layer.clear();
52
+ broadcast({ op: "clear", from });
53
+ },
54
+ apply(payload) {
55
+ if (payload.from === from)
56
+ return; // ignore our own echo
57
+ switch (payload.op) {
58
+ case "stroke":
59
+ layer.addStroke(payload.points, { id: payload.id, tone: payload.tone, width: payload.width });
60
+ return;
61
+ case "shape":
62
+ layer.addShape(payload.shape, { id: payload.id, tone: payload.tone, label: payload.label });
63
+ return;
64
+ case "note":
65
+ layer.addNote(payload.position, payload.text, { id: payload.id, tone: payload.tone });
66
+ return;
67
+ case "remove":
68
+ layer.remove(payload.id);
69
+ return;
70
+ case "clear":
71
+ layer.clear();
72
+ return;
73
+ }
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-session player-spawn override — a capture-harness ergonomics seam that lets a single
3
+ * screenshot or drive run place the player somewhere other than the authored `player_spawn`
4
+ * marker *without mutating `editor.scene.json`*. The runner installs one from the `?spawn=` URL
5
+ * overlay (mirroring `?cam=`); {@link authoredSpawnPosition} consults it for the default
6
+ * player-spawn resolution only, so authored content is untouched and the override evaporates the
7
+ * moment the page reloads without the param.
8
+ *
9
+ * Genre-agnostic: it moves whatever a game treats as its player spawn. It never changes markers of
10
+ * other kinds or an explicit marker `id`, so an override taken for a close-up cannot perturb enemy
11
+ * or prop placement queries that share {@link authoredSpawnPosition}.
12
+ *
13
+ * Install-style module state is the established runner pattern here (`installAssetBase`,
14
+ * `installEditorHost`, `installSaveEndpoint`); the default is `null`, so nothing changes and every
15
+ * pure query stays deterministic until a runner explicitly installs an override.
16
+ */
17
+ /** A spawn point that replaces the authored player spawn for one capture session. */
18
+ export interface SpawnOverride {
19
+ x: number;
20
+ y: number;
21
+ z: number;
22
+ /** Optional facing (yaw radians); when omitted the authored/`0` rotation is kept. */
23
+ rotationY?: number;
24
+ }
25
+ /** Install (or, with `null`, clear) the active player-spawn override. Idempotent; last write wins. */
26
+ export declare function installSpawnOverride(override: SpawnOverride | null): void;
27
+ /** Clear any installed player-spawn override. */
28
+ export declare function clearSpawnOverride(): void;
29
+ /** The currently installed player-spawn override, or `null` when none is active. */
30
+ export declare function readSpawnOverride(): SpawnOverride | null;
31
+ /**
32
+ * Parse a `?spawn=` / `--spawn` value into a {@link SpawnOverride}, or `null` when it is absent or
33
+ * malformed. Accepts `"x,y,z"` and `"x,y,z,yaw"` (yaw in radians); whitespace around components is
34
+ * tolerated. A non-finite or short tuple yields `null` so a typo never silently teleports the
35
+ * player to `NaN`.
36
+ */
37
+ export declare function parseSpawnOverride(raw: string | null | undefined): SpawnOverride | null;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Per-session player-spawn override — a capture-harness ergonomics seam that lets a single
3
+ * screenshot or drive run place the player somewhere other than the authored `player_spawn`
4
+ * marker *without mutating `editor.scene.json`*. The runner installs one from the `?spawn=` URL
5
+ * overlay (mirroring `?cam=`); {@link authoredSpawnPosition} consults it for the default
6
+ * player-spawn resolution only, so authored content is untouched and the override evaporates the
7
+ * moment the page reloads without the param.
8
+ *
9
+ * Genre-agnostic: it moves whatever a game treats as its player spawn. It never changes markers of
10
+ * other kinds or an explicit marker `id`, so an override taken for a close-up cannot perturb enemy
11
+ * or prop placement queries that share {@link authoredSpawnPosition}.
12
+ *
13
+ * Install-style module state is the established runner pattern here (`installAssetBase`,
14
+ * `installEditorHost`, `installSaveEndpoint`); the default is `null`, so nothing changes and every
15
+ * pure query stays deterministic until a runner explicitly installs an override.
16
+ */
17
+ let installed = null;
18
+ /** Install (or, with `null`, clear) the active player-spawn override. Idempotent; last write wins. */
19
+ export function installSpawnOverride(override) {
20
+ installed = override;
21
+ }
22
+ /** Clear any installed player-spawn override. */
23
+ export function clearSpawnOverride() {
24
+ installed = null;
25
+ }
26
+ /** The currently installed player-spawn override, or `null` when none is active. */
27
+ export function readSpawnOverride() {
28
+ return installed;
29
+ }
30
+ /**
31
+ * Parse a `?spawn=` / `--spawn` value into a {@link SpawnOverride}, or `null` when it is absent or
32
+ * malformed. Accepts `"x,y,z"` and `"x,y,z,yaw"` (yaw in radians); whitespace around components is
33
+ * tolerated. A non-finite or short tuple yields `null` so a typo never silently teleports the
34
+ * player to `NaN`.
35
+ */
36
+ export function parseSpawnOverride(raw) {
37
+ if (raw === null || raw === undefined)
38
+ return null;
39
+ const parts = raw
40
+ .split(",")
41
+ .map((part) => part.trim())
42
+ .filter((part) => part.length > 0);
43
+ if (parts.length < 3 || parts.length > 4)
44
+ return null;
45
+ const nums = parts.map((part) => Number(part));
46
+ if (nums.some((value) => !Number.isFinite(value)))
47
+ return null;
48
+ const [x, y, z, rotationY] = nums;
49
+ return rotationY === undefined ? { x, y, z } : { x, y, z, rotationY };
50
+ }
@@ -25,6 +25,10 @@ export interface StreetEdge {
25
25
  level: StreetLevel;
26
26
  /** True when this edge belongs to the main closed loop (circuit mode). */
27
27
  loop: boolean;
28
+ /** Per-point road-surface height (one per `points`), present only when `elevation > 0`. Raw
29
+ * (uncapped) samples of {@link StreetNetwork.elevationAt}; the grade-limited profile lives on the
30
+ * owning {@link Street.heights}. */
31
+ heights?: number[];
28
32
  }
29
33
  /** A feature span carried by a street: a `[from, to]` index window into the street's `points`. */
30
34
  export interface StreetFeatureSpan {
@@ -34,12 +38,19 @@ export interface StreetFeatureSpan {
34
38
  /** Ground height at each bank/portal — the deck/floor reference the renderer drapes to. */
35
39
  bankHeight: number;
36
40
  }
41
+ /** Left/right pedestrian bands flanking a street's asphalt, as offset polylines. */
42
+ export interface StreetSidewalks {
43
+ /** Polyline offset to the left of travel (parallel to `points`). */
44
+ left: StreetVec2[];
45
+ /** Polyline offset to the right of travel. */
46
+ right: StreetVec2[];
47
+ }
37
48
  /** One chained through-street: a maximal run of edges through degree-2 nodes, for rendering + furniture. */
38
49
  export interface Street {
39
50
  id: number;
40
51
  /** Ordered node ids the chain visits; first === last when `loop`. */
41
52
  nodes: number[];
42
- /** Smoothed, turn-clamped centerline. */
53
+ /** Smoothed, arc-filleted centerline. */
43
54
  points: StreetVec2[];
44
55
  width: number;
45
56
  level: StreetLevel;
@@ -49,6 +60,12 @@ export interface Street {
49
60
  bulb?: StreetVec2;
50
61
  /** Bridge/tunnel spans along this street, if any. */
51
62
  features: StreetFeatureSpan[];
63
+ /** Flanking pedestrian bands, present on boulevard/avenue/street levels (not lanes). */
64
+ sidewalks?: StreetSidewalks;
65
+ /** Per-point road-surface height (one per `points`), present only when `elevation > 0`. Grade-limited
66
+ * so no consecutive pair exceeds `maxGrade`; a loop stays continuous (`heights[0] === heights.at(-1)`).
67
+ * This is ROAD elevation, not the `context.heightAt` terrain that drives bridges/tunnels. */
68
+ heights?: number[];
52
69
  }
53
70
  /** One crossing of three or more streets: patch center/radius plus outgoing arm directions. */
54
71
  export interface StreetJunction {
@@ -92,36 +109,60 @@ export interface StreetNetwork {
92
109
  tunnels: StreetFeature[];
93
110
  /** Independent cycle count (E − V + components): 0 = pure tree, ≥1 = has loops. */
94
111
  loops: number;
112
+ /** The shared road-surface height FIELD (volume-local), present only when `elevation > 0`. Sample it
113
+ * to drape junction welds, sidewalks, and building bases so every surface meets the road continuously
114
+ * — the streets/edges sample this exact field. Distinct from the `context.heightAt` terrain sampler. */
115
+ elevationAt?: (x: number, z: number) => number;
95
116
  }
96
117
  /** Fully-defaulted slider set the generator reads. */
97
118
  export interface StreetNetworkRules {
98
119
  seed: string;
120
+ /** Organic boundary clipping amount, when supported by the caller. */
121
+ outline?: number;
99
122
  /** 1 = regular lattice nodes; 0 = organic scatter. */
100
123
  gridness: number;
101
124
  /** 0 = tree (dead-ends); 1 = a single closed circuit. */
102
125
  loopiness: number;
103
126
  /** Extra chord edges between neighbors → mesh density. */
104
127
  connectivity: number;
105
- /** Spur-lane density forking off the mains. */
128
+ /** Spur-lane density forking off the mains (and, in a circuit, the pit-lane trigger). */
106
129
  branching: number;
130
+ /** Share of branch streets that use residential side-street rules. */
131
+ residentialBranches?: number;
107
132
  /** Fraction of dangling ends kept as cul-de-sacs (vs reconnected). */
108
133
  deadEnds: number;
109
134
  /** Target node spacing / block size, world units. */
110
135
  segmentLength: number;
111
136
  /** Cross-axis spacing multiplier (≥1 = long skinny Manhattan blocks). */
112
137
  aspect: number;
113
- /** Sideways wander amplitude, 0..1. */
138
+ /** Sideways wander amplitude, 0..1 (and circuit corner-template density). */
114
139
  winding: number;
115
- /** Minimum curve radius (m) — caps wander curvature and corner tightness. */
140
+ /** Minimum curve radius (m) — caps wander curvature and sets the corner-fillet radius. */
116
141
  minCurveRadius: number;
117
142
  /** Shallowest corner (degrees) a street keeps — gentler bends are straightened. */
118
143
  minTurnAngle: number;
119
- /** Sharpest corner (degrees) a street may take — tighter ones are beveled away. */
144
+ /** Sharpest corner (degrees) between consecutive centerline samples — sharper corners are filleted. */
120
145
  maxTurnAngle: number;
121
146
  /** Base street width; the hierarchy scales off it. */
122
147
  width: number;
123
148
  /** Share of avenues upgraded to boulevards, 0..1. */
124
149
  boulevards: number;
150
+ /** Pedestrian band width paved on each side of boulevard/avenue/street levels. Default ~2. */
151
+ sidewalkWidth?: number;
152
+ /** Road-surface relief amplitude, 0..1. 0 (default) = dead flat and byte-identical to a pre-elevation
153
+ * network; 1 ≈ 22 m of relief across a ~520 m volume (scaled to the footprint). */
154
+ elevation?: number;
155
+ /** Maximum road slope (rise/run) the grade cap enforces on every street's height sequence. Default 0.07. */
156
+ maxGrade?: number;
157
+ /** SPACE-FILLING circuit layout dial, 0..1. 0 (default) = the hull construction (a flowing loop around
158
+ * an empty middle), BYTE-IDENTICAL to a pre-compactness network. Rising values switch the circuit
159
+ * LAYOUT stage to a grid spanning-tree CYCLE that folds back through its own interior: parallel
160
+ * adjacent runs one corridor pitch apart, switchback esses, consecutive hairpins, and long straights
161
+ * from straight tree branches, with footprint coverage rising with the dial (~45% of grid cells at
162
+ * 0.5, ~85% at 1). Everything downstream — spline fit, curvature floor, straight designation,
163
+ * self-clearance, edge distribution, elevation, pit lane — is reused unchanged. Only meaningful in
164
+ * circuit mode; ignored by the street net. */
165
+ compactness?: number;
125
166
  }
126
167
  /** Ground sampler + feature toggles enabling bridges/tunnels; omit for a flat, feature-free network. */
127
168
  export interface StreetNetworkContext {
@@ -137,13 +178,21 @@ export interface StreetNetworkContext {
137
178
  tunnelClearance?: number;
138
179
  }
139
180
  /**
140
- * Hard turn-angle clamp on a polyline with fixed endpoints: interior corners gentler than `minRad`
141
- * are straightened away (deliberate winding only, no micro-wiggle) and corners sharper than `maxRad`
142
- * are beveled — the corner vertex is replaced by two points pulled back along each leg, which
143
- * strictly reduces the angle. Endpoints never move, so a graph node stays welded.
181
+ * Replace every real corner of a polyline with a sampled **circular-arc fillet** so bends read as
182
+ * smooth curves instead of polygonal notches, while enforcing two hard limits with fixed endpoints:
183
+ * interior corners gentler than `minRad` are straightened away (deliberate winding only, no
184
+ * micro-wiggle), and each remaining corner is rounded with an arc of radius `curveRadius` (clamped to
185
+ * half the shorter adjacent leg) sampled finely enough that no discrete turn between consecutive
186
+ * samples exceeds `maxRad` — a corner sharper than the ceiling is still reduced, now to a fillet
187
+ * rather than a bevel. Open-chain endpoints never move (a graph node stays welded); pass `closed` for
188
+ * a ring (first === last) so its wrap corner is filleted too and the loop stays closed.
189
+ *
190
+ * Pass `radiusFor` to vary the fillet radius PER CORNER (returning the target radius from the corner's
191
+ * deflection and adjacent leg lengths) instead of the single `curveRadius` — the circuit loop uses this
192
+ * to mix hairpins, standard corners, and sweepers; every other caller keeps the scalar radius.
144
193
  * @internal
145
194
  */
146
- export declare function clampTurns(points: readonly StreetVec2[], minRad: number, maxRad: number): StreetVec2[];
195
+ export declare function clampTurns(points: readonly StreetVec2[], minRad: number, maxRad: number, curveRadius?: number, closed?: boolean, radiusFor?: (deflection: number, la: number, lb: number) => number): StreetVec2[];
147
196
  /** Decide which topology family the sliders call for. Circuit wins when loops dominate and both
148
197
  * branching and mesh connectivity are low — exactly the "race track" corner of the slider space.
149
198
  * @internal */
@@ -154,6 +203,6 @@ export declare function streetNetworkMode(rules: StreetNetworkRules): StreetNetw
154
203
  * water gaps into bridges and ridges into tunnels. Coordinates are volume-local; the caller maps to
155
204
  * world space.
156
205
  *
157
- * @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
206
+ * @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, sidewalks, bridges, tunnels) from sliders
158
207
  */
159
208
  export declare function generateStreets(rules: StreetNetworkRules, hx: number, hz: number, context?: StreetNetworkContext): StreetNetwork;