@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.
- package/CHANGELOG.md +529 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +21 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +48 -1
- package/dist/runtime/gameContext.js +8 -2
- package/dist/runtime/gameContextTypes.d.ts +39 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/colliders.d.ts +20 -0
- package/dist/scene/colliders.js +78 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +37 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +9 -46
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grassKind.js +4 -1
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- 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/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +18 -2
- package/dist/world.js +18 -2
- package/package.json +2 -2
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- 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,
|
|
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
|
|
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)
|
|
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
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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;
|