@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.
- package/CHANGELOG.md +73 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +3 -1
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +2 -2
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +38 -5
- package/dist/game/progression.d.ts +2 -2
- package/dist/game/progression.js +4 -2
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +45 -1
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/scene.d.ts +4 -1
- package/dist/runtime/context/scene.js +86 -1
- package/dist/runtime/gameContext.js +51 -4
- package/dist/runtime/gameContextTypes.d.ts +58 -4
- package/dist/runtime/headlessRunner.d.ts +13 -0
- package/dist/runtime/headlessRunner.js +14 -0
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/colliders.d.ts +41 -0
- package/dist/scene/colliders.js +140 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- package/dist/scene/entityStats.js +9 -1
- package/dist/scene/sceneKinds.d.ts +19 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.js +5 -2
- package/dist/ui/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityKind.d.ts +2 -2
- package/dist/world/cityKind.js +12 -6
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +35 -2
- package/dist/world/features.js +5 -2
- package/dist/world/grassKind.js +4 -1
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatterCoverage.d.ts +98 -0
- package/dist/world/scatterCoverage.js +101 -0
- package/dist/world/scatterRegion.js +6 -0
- package/dist/world/{pathNetwork.d.ts → streetGenerator.d.ts} +37 -37
- package/dist/world/{pathNetwork.js → streetGenerator.js} +9 -9
- package/dist/world/streets.d.ts +74 -1
- package/dist/world/streets.js +218 -0
- package/dist/world/terrain.d.ts +20 -1
- package/dist/world/terrain.js +41 -2
- package/dist/world.d.ts +1 -0
- package/dist/world.js +1 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +5 -2
- package/skills/jgengine/api.md +13 -5
- package/skills/jgengine-combat/SKILL.md +15 -1
- package/skills/jgengine-combat/api.md +39 -2
- package/skills/jgengine-combat/capabilities.md +17 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +22 -3
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +35 -5
- package/skills/jgengine-gameplay/capabilities.md +18 -0
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +9 -0
- package/skills/jgengine-world/api.md +82 -25
- package/skills/jgengine-world/capabilities.md +12 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
23
|
+
points: StreetVec2[];
|
|
24
24
|
width: number;
|
|
25
|
-
level:
|
|
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
|
|
31
|
-
kind:
|
|
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
|
|
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:
|
|
43
|
+
points: StreetVec2[];
|
|
44
44
|
width: number;
|
|
45
|
-
level:
|
|
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?:
|
|
49
|
+
bulb?: StreetVec2;
|
|
50
50
|
/** Bridge/tunnel spans along this street, if any. */
|
|
51
|
-
features:
|
|
51
|
+
features: StreetFeatureSpan[];
|
|
52
52
|
}
|
|
53
53
|
/** One crossing of three or more streets: patch center/radius plus outgoing arm directions. */
|
|
54
|
-
export interface
|
|
54
|
+
export interface StreetJunction {
|
|
55
55
|
x: number;
|
|
56
56
|
z: number;
|
|
57
57
|
radius: number;
|
|
58
|
-
level:
|
|
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
|
|
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
|
|
76
|
-
kind:
|
|
77
|
-
points:
|
|
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
|
|
83
|
-
mode:
|
|
84
|
-
nodes:
|
|
82
|
+
export interface StreetNetwork {
|
|
83
|
+
mode: StreetNetworkMode;
|
|
84
|
+
nodes: StreetNode[];
|
|
85
85
|
/** Atomic edges — feed to the block/parcel fabric. */
|
|
86
|
-
edges:
|
|
86
|
+
edges: StreetEdge[];
|
|
87
87
|
/** Chained through-streets — feed to the renderer, furniture, and analysis. */
|
|
88
|
-
streets:
|
|
89
|
-
junctions:
|
|
90
|
-
deadEnds:
|
|
91
|
-
bridges:
|
|
92
|
-
tunnels:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
157
|
+
* @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
|
|
158
158
|
*/
|
|
159
|
-
export declare function
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
403
|
+
* @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
|
|
404
404
|
*/
|
|
405
|
-
export function
|
|
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 =
|
|
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);
|
package/dist/world/streets.d.ts
CHANGED
|
@@ -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;
|