@jgengine/core 0.12.0 → 0.14.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 (94) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/combat/effects.d.ts +11 -4
  3. package/dist/combat/effects.js +24 -17
  4. package/dist/combat/magazine.d.ts +13 -0
  5. package/dist/combat/magazine.js +27 -0
  6. package/dist/combat/vfxPresets.d.ts +176 -0
  7. package/dist/combat/vfxPresets.js +74 -0
  8. package/dist/combat/weaponFire.d.ts +114 -0
  9. package/dist/combat/weaponFire.js +96 -0
  10. package/dist/combat.d.ts +4 -2
  11. package/dist/combat.js +3 -1
  12. package/dist/devtools/urlFlags.d.ts +22 -0
  13. package/dist/devtools/urlFlags.js +62 -0
  14. package/dist/game/clipRoles.d.ts +51 -0
  15. package/dist/game/clipRoles.js +142 -0
  16. package/dist/game/defineGame.d.ts +2 -2
  17. package/dist/game/partAnimation.d.ts +78 -0
  18. package/dist/game/partAnimation.js +147 -0
  19. package/dist/game/playableGame.d.ts +38 -5
  20. package/dist/game/progression.d.ts +2 -2
  21. package/dist/game/progression.js +4 -2
  22. package/dist/inventory/inventoryModel.d.ts +24 -8
  23. package/dist/inventory/inventoryModel.js +47 -8
  24. package/dist/meta/changelog.d.ts +1 -1
  25. package/dist/meta/changelog.js +45 -1
  26. package/dist/runtime/context/combat.js +2 -1
  27. package/dist/runtime/context/combatFx.js +15 -5
  28. package/dist/runtime/context/scene.d.ts +4 -1
  29. package/dist/runtime/context/scene.js +86 -1
  30. package/dist/runtime/gameContext.js +51 -4
  31. package/dist/runtime/gameContextTypes.d.ts +58 -4
  32. package/dist/runtime/headlessRunner.d.ts +13 -0
  33. package/dist/runtime/headlessRunner.js +14 -0
  34. package/dist/scene/assetCatalog.d.ts +2 -0
  35. package/dist/scene/builtinSceneKinds.d.ts +6 -1
  36. package/dist/scene/builtinSceneKinds.js +10 -3
  37. package/dist/scene/colliders.d.ts +41 -0
  38. package/dist/scene/colliders.js +140 -0
  39. package/dist/scene/collisionMesh.d.ts +8 -0
  40. package/dist/scene/collisionMesh.js +48 -0
  41. package/dist/scene/entityStats.js +9 -1
  42. package/dist/scene/sceneKinds.d.ts +19 -0
  43. package/dist/stats/statModifiers.d.ts +16 -0
  44. package/dist/stats/statModifiers.js +30 -1
  45. package/dist/stats/statPool.js +5 -2
  46. package/dist/ui/hudScale.d.ts +16 -1
  47. package/dist/ui/hudScale.js +22 -1
  48. package/dist/ui/panelModel.d.ts +120 -0
  49. package/dist/ui/panelModel.js +168 -0
  50. package/dist/ui.d.ts +1 -0
  51. package/dist/ui.js +1 -0
  52. package/dist/world/buildingLots.d.ts +84 -0
  53. package/dist/world/buildingLots.js +119 -0
  54. package/dist/world/buildings.d.ts +6 -0
  55. package/dist/world/cityGenerator.d.ts +41 -0
  56. package/dist/world/cityGenerator.js +58 -0
  57. package/dist/world/cityKind.d.ts +2 -2
  58. package/dist/world/cityKind.js +12 -6
  59. package/dist/world/environmentSummary.js +57 -1
  60. package/dist/world/features.d.ts +35 -2
  61. package/dist/world/features.js +5 -2
  62. package/dist/world/grassKind.js +4 -1
  63. package/dist/world/roads.d.ts +19 -2
  64. package/dist/world/roads.js +60 -3
  65. package/dist/world/scatterCoverage.d.ts +98 -0
  66. package/dist/world/scatterCoverage.js +101 -0
  67. package/dist/world/scatterRegion.js +6 -0
  68. package/dist/world/{pathNetwork.d.ts → streetGenerator.d.ts} +37 -37
  69. package/dist/world/{pathNetwork.js → streetGenerator.js} +9 -9
  70. package/dist/world/streets.d.ts +74 -1
  71. package/dist/world/streets.js +218 -0
  72. package/dist/world/terrain.d.ts +20 -1
  73. package/dist/world/terrain.js +41 -2
  74. package/dist/world.d.ts +1 -0
  75. package/dist/world.js +1 -0
  76. package/package.json +2 -2
  77. package/skills/jgengine/SKILL.md +5 -2
  78. package/skills/jgengine/api.md +13 -5
  79. package/skills/jgengine-combat/SKILL.md +15 -1
  80. package/skills/jgengine-combat/api.md +39 -2
  81. package/skills/jgengine-combat/capabilities.md +17 -1
  82. package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
  83. package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
  84. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
  85. package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
  86. package/skills/jgengine-combat/reference.md +22 -3
  87. package/skills/jgengine-gameplay/SKILL.md +8 -0
  88. package/skills/jgengine-gameplay/api.md +35 -5
  89. package/skills/jgengine-gameplay/capabilities.md +18 -0
  90. package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
  91. package/skills/jgengine-gameplay/reference-systems.md +9 -0
  92. package/skills/jgengine-world/api.md +82 -25
  93. package/skills/jgengine-world/capabilities.md +12 -4
  94. package/skills/jgengine-world/reference.md +1 -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
  }
@@ -1,61 +1,61 @@
1
1
  /** A path vertex in the volume-local XZ frame. */
2
- export type PathVec2 = readonly [number, number];
2
+ export type StreetVec2 = readonly [number, number];
3
3
  /** Road hierarchy, widest to narrowest — shared by the city fabric and the renderer. */
4
- export type PathLevel = "boulevard" | "avenue" | "street" | "lane";
4
+ export type StreetLevel = "boulevard" | "avenue" | "street" | "lane";
5
5
  /** A path feature spanning part of an edge/street: a bridge deck over a gap or a tunnel bore under a ridge. */
6
- export type PathFeatureKind = "bridge" | "tunnel";
6
+ export type StreetFeatureKind = "bridge" | "tunnel";
7
7
  /** The generator's chosen topology family: an open street `net`, or a closed `circuit` loop. */
8
- export type PathNetworkMode = "net" | "circuit";
8
+ export type StreetNetworkMode = "net" | "circuit";
9
9
  /** One graph node: a junction, a dead end, or a mid-street bend, with its connection count. */
10
- export interface PathNode {
10
+ export interface StreetNode {
11
11
  id: number;
12
12
  x: number;
13
13
  z: number;
14
14
  degree: number;
15
15
  }
16
16
  /** One atomic node-to-node edge — the fabric graph consumes these (welds at shared node coords). */
17
- export interface PathEdge {
17
+ export interface StreetEdge {
18
18
  id: number;
19
19
  /** Endpoint node ids. */
20
20
  a: number;
21
21
  b: number;
22
22
  /** Sampled centerline; `points[0]` sits exactly on node `a`, the last on node `b`. */
23
- points: PathVec2[];
23
+ points: StreetVec2[];
24
24
  width: number;
25
- level: PathLevel;
25
+ level: StreetLevel;
26
26
  /** True when this edge belongs to the main closed loop (circuit mode). */
27
27
  loop: boolean;
28
28
  }
29
29
  /** A feature span carried by a street: a `[from, to]` index window into the street's `points`. */
30
- export interface PathFeatureSpan {
31
- kind: PathFeatureKind;
30
+ export interface StreetFeatureSpan {
31
+ kind: StreetFeatureKind;
32
32
  from: number;
33
33
  to: number;
34
34
  /** Ground height at each bank/portal — the deck/floor reference the renderer drapes to. */
35
35
  bankHeight: number;
36
36
  }
37
37
  /** One chained through-street: a maximal run of edges through degree-2 nodes, for rendering + furniture. */
38
- export interface PathStreet {
38
+ export interface Street {
39
39
  id: number;
40
40
  /** Ordered node ids the chain visits; first === last when `loop`. */
41
41
  nodes: number[];
42
42
  /** Smoothed, turn-clamped centerline. */
43
- points: PathVec2[];
43
+ points: StreetVec2[];
44
44
  width: number;
45
- level: PathLevel;
45
+ level: StreetLevel;
46
46
  /** Closed chain (a circuit lap or an inner ring). */
47
47
  loop: boolean;
48
48
  /** Cul-de-sac turning bulb when the street ends at a dangling node. */
49
- bulb?: PathVec2;
49
+ bulb?: StreetVec2;
50
50
  /** Bridge/tunnel spans along this street, if any. */
51
- features: PathFeatureSpan[];
51
+ features: StreetFeatureSpan[];
52
52
  }
53
53
  /** One crossing of three or more streets: patch center/radius plus outgoing arm directions. */
54
- export interface PathJunction {
54
+ export interface StreetJunction {
55
55
  x: number;
56
56
  z: number;
57
57
  radius: number;
58
- level: PathLevel;
58
+ level: StreetLevel;
59
59
  /** Outgoing arm directions (radians) with the crossing street width, for crosswalks/patches. */
60
60
  arms: {
61
61
  angle: number;
@@ -63,7 +63,7 @@ export interface PathJunction {
63
63
  }[];
64
64
  }
65
65
  /** One dangling street end kept as a cul-de-sac: node position plus the heading pointing off the road. */
66
- export interface PathDeadEnd {
66
+ export interface StreetDeadEnd {
67
67
  node: number;
68
68
  x: number;
69
69
  z: number;
@@ -72,29 +72,29 @@ export interface PathDeadEnd {
72
72
  width: number;
73
73
  }
74
74
  /** A resolved path feature in world-of-the-volume space: a bridge deck or tunnel bore centerline. */
75
- export interface PathFeature {
76
- kind: PathFeatureKind;
77
- points: PathVec2[];
75
+ export interface StreetFeature {
76
+ kind: StreetFeatureKind;
77
+ points: StreetVec2[];
78
78
  width: number;
79
79
  bankHeight: number;
80
80
  }
81
81
  /** The fully-resolved network in volume-local coords. */
82
- export interface PathNetwork {
83
- mode: PathNetworkMode;
84
- nodes: PathNode[];
82
+ export interface StreetNetwork {
83
+ mode: StreetNetworkMode;
84
+ nodes: StreetNode[];
85
85
  /** Atomic edges — feed to the block/parcel fabric. */
86
- edges: PathEdge[];
86
+ edges: StreetEdge[];
87
87
  /** Chained through-streets — feed to the renderer, furniture, and analysis. */
88
- streets: PathStreet[];
89
- junctions: PathJunction[];
90
- deadEnds: PathDeadEnd[];
91
- bridges: PathFeature[];
92
- tunnels: PathFeature[];
88
+ streets: Street[];
89
+ junctions: StreetJunction[];
90
+ deadEnds: StreetDeadEnd[];
91
+ bridges: StreetFeature[];
92
+ tunnels: StreetFeature[];
93
93
  /** Independent cycle count (E − V + components): 0 = pure tree, ≥1 = has loops. */
94
94
  loops: number;
95
95
  }
96
96
  /** Fully-defaulted slider set the generator reads. */
97
- export interface PathNetworkRules {
97
+ export interface StreetNetworkRules {
98
98
  seed: string;
99
99
  /** 1 = regular lattice nodes; 0 = organic scatter. */
100
100
  gridness: number;
@@ -124,7 +124,7 @@ export interface PathNetworkRules {
124
124
  boulevards: number;
125
125
  }
126
126
  /** Ground sampler + feature toggles enabling bridges/tunnels; omit for a flat, feature-free network. */
127
- export interface PathNetworkContext {
127
+ export interface StreetNetworkContext {
128
128
  /** Volume-local ground height sampler. */
129
129
  heightAt?: (x: number, z: number) => number;
130
130
  /** Ground below this (local) height is a gap streets bridge over. */
@@ -143,17 +143,17 @@ export interface PathNetworkContext {
143
143
  * strictly reduces the angle. Endpoints never move, so a graph node stays welded.
144
144
  * @internal
145
145
  */
146
- export declare function clampTurns(points: readonly PathVec2[], minRad: number, maxRad: number): PathVec2[];
146
+ export declare function clampTurns(points: readonly StreetVec2[], minRad: number, maxRad: number): StreetVec2[];
147
147
  /** Decide which topology family the sliders call for. Circuit wins when loops dominate and both
148
148
  * branching and mesh connectivity are low — exactly the "race track" corner of the slider space.
149
149
  * @internal */
150
- export declare function pathNetworkMode(rules: PathNetworkRules): PathNetworkMode;
150
+ export declare function streetNetworkMode(rules: StreetNetworkRules): StreetNetworkMode;
151
151
  /**
152
152
  * Resolve a full path network from its rules inside a volume of half-extents `hx`×`hz`. Deterministic
153
- * per `(rules.seed, hx, hz, context)`. Pass a {@link PathNetworkContext} with a ground sampler to turn
153
+ * per `(rules.seed, hx, hz, context)`. Pass a {@link StreetNetworkContext} with a ground sampler to turn
154
154
  * water gaps into bridges and ridges into tunnels. Coordinates are volume-local; the caller maps to
155
155
  * world space.
156
156
  *
157
- * @capability path-network build a deterministic road/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
157
+ * @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
158
158
  */
159
- export declare function buildPathNetwork(rules: PathNetworkRules, hx: number, hz: number, context?: PathNetworkContext): PathNetwork;
159
+ export declare function generateStreets(rules: StreetNetworkRules, hx: number, hz: number, context?: StreetNetworkContext): StreetNetwork;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The unified, seed-driven procedural PATH NETWORK — one generator that grows an entire road/track
2
+ * The unified, seed-driven procedural STREET GENERATOR — one engine that grows an entire street/track
3
3
  * graph inside a box volume and answers to sliders instead of hardcoded geometry. It is deliberately
4
4
  * genre-agnostic: a city street net and a closed race circuit are the *same* engine at opposite
5
5
  * slider extremes, not two code paths. Drop a volume, pick a seed, and turn the dials:
@@ -16,14 +16,14 @@
16
16
  * BRIDGE deck; a span buried under a ridge becomes a TUNNEL bore. Both are path FEATURES on a
17
17
  * continuous edge, so a circuit stays closed as it crosses water or pierces a hill.
18
18
  *
19
- * Output is two coupled views of the graph: atomic node-to-node {@link PathEdge}s (fed straight into
19
+ * Output is two coupled views of the graph: atomic node-to-node {@link StreetEdge}s (fed straight into
20
20
  * the block/parcel fabric — a race circuit is many edges between distinct nodes, never one fragile
21
- * self-loop) and chained {@link PathStreet}s (through-streets for rendering, furniture, and
21
+ * self-loop) and chained {@link Street}s (through-streets for rendering, furniture, and
22
22
  * junctions). Pure deterministic math — same rules + seed + volume ⇒ identical network — with bounded
23
23
  * work caps so a huge volume can never generate unbounded content. Local (volume-centered) coords;
24
24
  * the caller rotates/translates into world space.
25
25
  *
26
- * @capability path-network seed-driven procedural roads and race circuits from one slider-driven engine
26
+ * @capability street-generator seed-driven procedural streets and race circuits from one slider-driven engine
27
27
  */
28
28
  import { seededStreams } from "../random/rng.js";
29
29
  const TAU = Math.PI * 2;
@@ -369,7 +369,7 @@ function buildCircuit(rules, hx, hz, streams) {
369
369
  /** Decide which topology family the sliders call for. Circuit wins when loops dominate and both
370
370
  * branching and mesh connectivity are low — exactly the "race track" corner of the slider space.
371
371
  * @internal */
372
- export function pathNetworkMode(rules) {
372
+ export function streetNetworkMode(rules) {
373
373
  const score = rules.loopiness * (1 - rules.branching * 0.7) * (1 - rules.connectivity * 0.7);
374
374
  return score >= 0.45 ? "circuit" : "net";
375
375
  }
@@ -396,15 +396,15 @@ function levelForStreets(chains, rules, rng) {
396
396
  }
397
397
  /**
398
398
  * Resolve a full path network from its rules inside a volume of half-extents `hx`×`hz`. Deterministic
399
- * per `(rules.seed, hx, hz, context)`. Pass a {@link PathNetworkContext} with a ground sampler to turn
399
+ * per `(rules.seed, hx, hz, context)`. Pass a {@link StreetNetworkContext} with a ground sampler to turn
400
400
  * water gaps into bridges and ridges into tunnels. Coordinates are volume-local; the caller maps to
401
401
  * world space.
402
402
  *
403
- * @capability path-network build a deterministic road/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
403
+ * @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
404
404
  */
405
- export function buildPathNetwork(rules, hx, hz, context = {}) {
405
+ export function generateStreets(rules, hx, hz, context = {}) {
406
406
  const streams = seededStreams(`pathnet:${rules.seed}:${Math.round(hx)}:${Math.round(hz)}`);
407
- const mode = pathNetworkMode(rules);
407
+ const mode = streetNetworkMode(rules);
408
408
  const raw = mode === "circuit" ? buildCircuit(rules, hx, hz, streams) : buildNet(rules, hx, hz, streams);
409
409
  const rawNodes = raw.nodes;
410
410
  const rawEdges = raw.edges.filter((e) => e.a !== e.b);
@@ -1,5 +1,5 @@
1
1
  import type { RoadEnvironmentDescriptor } from "./features.js";
2
- import { type RoadPoint } from "./roads.js";
2
+ import { type DashExclusion, type RoadPoint } from "./roads.js";
3
3
  /**
4
4
  * Offset a centerline sideways by a signed distance along its local perpendicular — the
5
5
  * building block for lanes, curb lines, and sidewalk paths. Positive offsets fall on the
@@ -107,3 +107,76 @@ export declare function roadSurfaceSampler(roads: readonly RoadEnvironmentDescri
107
107
  * band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
108
108
  */
109
109
  export declare function sidewalkPoint(road: RoadEnvironmentDescriptor, side: "left" | "right", fraction: number): RoadPoint | null;
110
+ /**
111
+ * The two curb lines of a road — offset polylines running along the outer edge of the asphalt on
112
+ * each side. Mesh these with a thin darker ribbon to give the road a curb/edge strip instead of a
113
+ * hard asphalt-to-terrain seam. `inset` (default 0) nudges the line inward off the raw edge so the
114
+ * strip straddles the border.
115
+ */
116
+ export declare function curbPaths(road: RoadEnvironmentDescriptor, inset?: number): readonly [readonly RoadPoint[], readonly RoadPoint[]];
117
+ /** A single arm of a junction: the road width plus the outward direction the road leaves the center. */
118
+ export interface RoadApproach {
119
+ /** Unit vector pointing from the junction center outward along the road arm. */
120
+ direction: readonly [number, number];
121
+ /** Drivable width of the road on this arm. */
122
+ width: number;
123
+ }
124
+ /** A welded road intersection: a patch center/radius plus the arms that feed it. */
125
+ export interface RoadJunction {
126
+ /** World XZ center of the intersection. */
127
+ center: RoadPoint;
128
+ /** Radius of the covering patch (covers the crossing ribbons). */
129
+ radius: number;
130
+ /** Representative surface elevation (max of the participating roads). */
131
+ elevation: number;
132
+ /** Representative asphalt color (the widest participating road's color). */
133
+ color: string;
134
+ /** Outward arms leaving the junction — one per road direction that continues past the center. */
135
+ approaches: readonly RoadApproach[];
136
+ }
137
+ /** Options for {@link findRoadJunctions}. */
138
+ export interface RoadJunctionOptions {
139
+ /** Roads weld only when their elevations differ by at most this much. Default 0.12. */
140
+ elevationBand?: number;
141
+ /** Crossing points closer than this merge into a single junction. Default 6. */
142
+ mergeDistance?: number;
143
+ /** Hard cap on junctions returned so detection stays bounded. Default 256. */
144
+ maxJunctions?: number;
145
+ /** Extra scale on the patch radius past the widest arm's half-width. Default 1.15. */
146
+ radiusScale?: number;
147
+ }
148
+ /**
149
+ * Detect welded intersections across a set of roads: wherever two road centerlines cross (or a road
150
+ * terminates on another) within the same elevation band, emit a {@link RoadJunction} whose patch
151
+ * covers the crossing ribbons. Nearby crossings merge into one junction. Deterministic and bounded —
152
+ * O(roads² · segments²) with a `maxJunctions` cap — so a scene builds its intersections once.
153
+ */
154
+ export declare function findRoadJunctions(roads: readonly RoadEnvironmentDescriptor[], options?: RoadJunctionOptions): readonly RoadJunction[];
155
+ /** Circular marking-exclusion zones for the junctions, sized to hide the center line under each patch. */
156
+ export declare function junctionExclusions(junctions: readonly RoadJunction[], pad?: number): readonly DashExclusion[];
157
+ /** Options for {@link junctionMarkings}. */
158
+ export interface JunctionMarkingOptions {
159
+ /** Emit a stop-line bar across each approach. Default true. */
160
+ stopLines?: boolean;
161
+ /** Emit crosswalk bars at each approach. Default true. */
162
+ crosswalks?: boolean;
163
+ /** Number of zebra bars per crosswalk. Default 5. */
164
+ crosswalkBars?: number;
165
+ /** Clear gap outside the patch before markings start. Default 0.6. */
166
+ gap?: number;
167
+ /** Depth (along travel) of the crosswalk band. Default 2.4. */
168
+ crosswalkDepth?: number;
169
+ }
170
+ /** Thin marking polylines for a junction: stop-line bars and crosswalk bars, ready for {@link buildRoadRibbon}. */
171
+ export interface JunctionMarkings {
172
+ /** One bar per approach, perpendicular to travel just outside the patch. */
173
+ stopLines: readonly (readonly RoadPoint[])[];
174
+ /** Continental (parallel-to-travel) zebra bars, `crosswalkBars` per approach. */
175
+ crosswalkBars: readonly (readonly RoadPoint[])[];
176
+ }
177
+ /**
178
+ * Data-driven junction entry markings: a stop-line bar across each approach and a band of crosswalk
179
+ * bars just outside the welded patch. Each returned polyline is a thin centerline meant to be meshed
180
+ * with {@link buildRoadRibbon} at a small width. Fully skippable per-feature via the options.
181
+ */
182
+ export declare function junctionMarkings(junction: RoadJunction, options?: JunctionMarkingOptions): JunctionMarkings;