@jgengine/core 0.11.0 → 0.13.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 +52 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -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 +5 -2
- package/dist/combat.js +4 -1
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +72 -440
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +22 -1
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +48 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +131 -0
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +60 -0
- package/dist/runtime/context/scene.js +329 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +133 -500
- package/dist/runtime/gameContextTypes.d.ts +500 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +17 -2
- package/dist/runtime/headlessRunner.js +15 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +7 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +79 -0
- package/dist/scene/colliders.js +170 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +19 -15
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +62 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -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/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -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/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +53 -5
- package/dist/world/features.js +15 -3
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatter.js +2 -20
- package/dist/world/streetGenerator.d.ts +159 -0
- package/dist/world/streetGenerator.js +684 -0
- package/dist/world/streets.d.ts +103 -1
- package/dist/world/streets.js +273 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +65 -1
- package/dist/world/terrain.js +119 -2
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +19 -1
- package/skills/jgengine-combat/api.md +75 -2
- package/skills/jgengine-combat/capabilities.md +61 -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-stat-pools.md +122 -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 +33 -4
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +182 -26
- package/skills/jgengine-gameplay/capabilities.md +96 -2
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +10 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/world/polyline.js
CHANGED
|
@@ -62,6 +62,10 @@ export function closestPoint(line, query) {
|
|
|
62
62
|
return { point: points[0], distanceAlong: 0, segment: 0, lateral: distance(points[0], query) };
|
|
63
63
|
}
|
|
64
64
|
let best = { point: points[0], distanceAlong: 0, segment: 0, lateral: Infinity };
|
|
65
|
+
// Track the unsigned nearest distance separately: `best.lateral` carries a *signed* offset for
|
|
66
|
+
// the caller, so comparing the next candidate's unsigned distance against it would freeze the
|
|
67
|
+
// search the moment a negative-side segment lands (unsigned >= 0 never beats a stored negative).
|
|
68
|
+
let bestDistance = Infinity;
|
|
65
69
|
for (let i = 1; i < points.length; i += 1) {
|
|
66
70
|
const a = points[i - 1];
|
|
67
71
|
const b = points[i];
|
|
@@ -71,9 +75,12 @@ export function closestPoint(line, query) {
|
|
|
71
75
|
const t = segLenSq <= 0 ? 0 : Math.max(0, Math.min(1, ((query[0] - a[0]) * abx + (query[1] - a[1]) * aby) / segLenSq));
|
|
72
76
|
const point = [a[0] + abx * t, a[1] + aby * t];
|
|
73
77
|
const lateral = distance(point, query);
|
|
74
|
-
if (lateral <
|
|
78
|
+
if (lateral < bestDistance) {
|
|
79
|
+
bestDistance = lateral;
|
|
75
80
|
const along = cumulative[i - 1] + t * Math.sqrt(segLenSq);
|
|
76
|
-
const
|
|
81
|
+
const unit = normalize([abx, aby]);
|
|
82
|
+
const normal = perp(unit);
|
|
83
|
+
const side = Math.sign(normal[0] * (query[0] - point[0]) + normal[1] * (query[1] - point[1]));
|
|
77
84
|
best = { point, distanceAlong: along, segment: i - 1, lateral: lateral * (side === 0 ? 1 : side) };
|
|
78
85
|
}
|
|
79
86
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** A quantity that is either fixed or a uniformly sampled `[min, max]` range resolved by the field rng. */
|
|
2
|
+
export type Quantity = number | readonly [number, number];
|
|
3
|
+
/** One resource kind a node can yield and its base per-harvest amount (before tool bias and harvester multiplier). */
|
|
4
|
+
export interface ResourceYield {
|
|
5
|
+
/** Generic resource kind id — "wood", "ore", "fiber", "hide". The field never interprets it. */
|
|
6
|
+
kind: string;
|
|
7
|
+
/** Base amount granted per harvest hit; a `[min, max]` range samples deterministic variance from the field rng. */
|
|
8
|
+
amount: Quantity;
|
|
9
|
+
}
|
|
10
|
+
/** A harvestable node definition: a finite budget, the resources it yields, and how it respawns. */
|
|
11
|
+
export interface ResourceNodeDef {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Total harvestable budget (hit points / finite quantity). Each harvest consumes the tool's `power`. */
|
|
14
|
+
budget: number;
|
|
15
|
+
/** Resource kinds this node can yield; a tool's bias selects and scales among them. */
|
|
16
|
+
resources: readonly ResourceYield[];
|
|
17
|
+
/** Seconds to refill after depletion. Omit for a node that never respawns (a one-shot deposit). */
|
|
18
|
+
respawn?: number;
|
|
19
|
+
/** Budget restored on respawn; defaults to `budget`. */
|
|
20
|
+
respawnBudget?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* How a tool extracts from a node — its per-hit power and per-resource yield bias. Two profiles over the
|
|
24
|
+
* same multi-resource node return different resources and amounts: the generic pick-vs-hatchet mechanic.
|
|
25
|
+
*/
|
|
26
|
+
export interface ToolProfile {
|
|
27
|
+
/** Budget consumed per harvest hit; defaults to 1. Clamped to the node's remaining budget. */
|
|
28
|
+
power?: number;
|
|
29
|
+
/** Per-resource-kind yield multiplier. A kind absent here falls back to `defaultBias`. */
|
|
30
|
+
biases?: Readonly<Record<string, number>>;
|
|
31
|
+
/** Multiplier for resource kinds not listed in `biases`; defaults to 1 (set 0 for a tool that only extracts listed kinds). */
|
|
32
|
+
defaultBias?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Options for a single harvest. */
|
|
35
|
+
export interface HarvestOptions {
|
|
36
|
+
/** Scalar applied to every granted amount — a stronger creature/harvester yields more; defaults to 1. */
|
|
37
|
+
multiplier?: number;
|
|
38
|
+
}
|
|
39
|
+
/** One resource kind granted by a harvest and its resolved amount. */
|
|
40
|
+
export interface ResourceGrant {
|
|
41
|
+
kind: string;
|
|
42
|
+
amount: number;
|
|
43
|
+
}
|
|
44
|
+
/** Outcome of one harvest attempt. */
|
|
45
|
+
export interface HarvestResult {
|
|
46
|
+
nodeId: string;
|
|
47
|
+
/** Resources granted this harvest; empty when the node was already depleted or the tool extracted nothing. */
|
|
48
|
+
granted: readonly ResourceGrant[];
|
|
49
|
+
/** Budget actually consumed (0 when the node was already depleted). */
|
|
50
|
+
spent: number;
|
|
51
|
+
/** Whether the node still had budget and produced a harvest. */
|
|
52
|
+
harvested: boolean;
|
|
53
|
+
/** Whether this harvest reduced the node to empty. */
|
|
54
|
+
depleted: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Serializable per-node runtime state — round-trips through save/load and multiplayer sync. */
|
|
57
|
+
export interface ResourceNodeState {
|
|
58
|
+
id: string;
|
|
59
|
+
/** Remaining budget. */
|
|
60
|
+
budget: number;
|
|
61
|
+
/** Budget the node refills to on respawn — the nominal "full" value for `fraction`. */
|
|
62
|
+
maxBudget: number;
|
|
63
|
+
depleted: boolean;
|
|
64
|
+
/** Seconds remaining until respawn; 0 when full or when the node never respawns. */
|
|
65
|
+
respawnTimer: number;
|
|
66
|
+
/** Explicit respawn-block flag (a structure claimed the spot); OR-ed with the field's suppression predicate. */
|
|
67
|
+
respawnBlocked: boolean;
|
|
68
|
+
/** 0..1 remaining fraction of `maxBudget`. */
|
|
69
|
+
fraction: number;
|
|
70
|
+
}
|
|
71
|
+
/** Construction options for {@link createResourceNodeField}: node definitions and an optional RNG/suppression hook. */
|
|
72
|
+
export interface ResourceNodeFieldConfig {
|
|
73
|
+
nodes: readonly ResourceNodeDef[];
|
|
74
|
+
/** Injected [0,1) generator for deterministic yield variance; defaults to `Math.random`. */
|
|
75
|
+
rng?: () => number;
|
|
76
|
+
/**
|
|
77
|
+
* Freeze respawn for a node while this returns true — a structure within radius, a nearby occupant.
|
|
78
|
+
* Consulted every tick alongside the per-node `respawnBlocked` flag (either one suppresses).
|
|
79
|
+
*/
|
|
80
|
+
respawnSuppressed?: (state: ResourceNodeState) => boolean;
|
|
81
|
+
}
|
|
82
|
+
/** A set of harvestable nodes: harvest with tool profiles, tick respawn timers, snapshot/hydrate for save-load. */
|
|
83
|
+
export interface ResourceNodeField {
|
|
84
|
+
/** Apply one harvest hit with `tool`; returns the granted resources and decrements the node's budget. */
|
|
85
|
+
harvest(nodeId: string, tool: ToolProfile, options?: HarvestOptions): HarvestResult;
|
|
86
|
+
/** Count down respawn timers by `dt`; refill any node whose timer elapses and is not suppressed. */
|
|
87
|
+
tick(dt: number): void;
|
|
88
|
+
state(nodeId: string): ResourceNodeState;
|
|
89
|
+
isDepleted(nodeId: string): boolean;
|
|
90
|
+
/** Set the explicit respawn-block flag; persisted in state and honored by `tick`. */
|
|
91
|
+
setRespawnBlocked(nodeId: string, blocked: boolean): void;
|
|
92
|
+
ids(): readonly string[];
|
|
93
|
+
snapshot(): Record<string, ResourceNodeState>;
|
|
94
|
+
/** Restore mutable per-node state (budget, depletion, timers, block flag) from a {@link snapshot}. */
|
|
95
|
+
hydrate(snapshot: Record<string, ResourceNodeState>): void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A field of depletable, respawning harvest nodes with tool-dependent yields — the generic rock / tree /
|
|
99
|
+
* bush / vein / corpse. Each node holds a finite budget across one or more resource kinds. A harvest applies
|
|
100
|
+
* a {@link ToolProfile} whose per-resource biases decide *which* resources and *how much* the same node
|
|
101
|
+
* returns (pick favors ore, hatchet favors wood), scaled by a creature/harvester multiplier and optional
|
|
102
|
+
* deterministic rng variance; the budget drops by the tool's power. When a node empties it starts a respawn
|
|
103
|
+
* timer that `tick(dt)` counts down and refills — unless respawn is suppressed by the per-node flag or the
|
|
104
|
+
* injected predicate (a structure claimed the spot). All state is plain and serializable for save-load and
|
|
105
|
+
* multiplayer sync via {@link snapshot}/{@link hydrate}.
|
|
106
|
+
*
|
|
107
|
+
* @capability resource-node depletable, respawning harvest nodes with tool-dependent yields
|
|
108
|
+
*/
|
|
109
|
+
export declare function createResourceNodeField(config: ResourceNodeFieldConfig): ResourceNodeField;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { clamp } from "../math/scalar.js";
|
|
2
|
+
function resolveQuantity(quantity, rng) {
|
|
3
|
+
if (typeof quantity === "number")
|
|
4
|
+
return quantity;
|
|
5
|
+
const [lo, hi] = quantity;
|
|
6
|
+
return lo + rng() * (hi - lo);
|
|
7
|
+
}
|
|
8
|
+
function toState(node) {
|
|
9
|
+
return {
|
|
10
|
+
id: node.def.id,
|
|
11
|
+
budget: node.budget,
|
|
12
|
+
maxBudget: node.maxBudget,
|
|
13
|
+
depleted: node.depleted,
|
|
14
|
+
respawnTimer: node.respawnTimer,
|
|
15
|
+
respawnBlocked: node.respawnBlocked,
|
|
16
|
+
fraction: node.maxBudget > 0 ? clamp(node.budget / node.maxBudget, 0, 1) : 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A field of depletable, respawning harvest nodes with tool-dependent yields — the generic rock / tree /
|
|
21
|
+
* bush / vein / corpse. Each node holds a finite budget across one or more resource kinds. A harvest applies
|
|
22
|
+
* a {@link ToolProfile} whose per-resource biases decide *which* resources and *how much* the same node
|
|
23
|
+
* returns (pick favors ore, hatchet favors wood), scaled by a creature/harvester multiplier and optional
|
|
24
|
+
* deterministic rng variance; the budget drops by the tool's power. When a node empties it starts a respawn
|
|
25
|
+
* timer that `tick(dt)` counts down and refills — unless respawn is suppressed by the per-node flag or the
|
|
26
|
+
* injected predicate (a structure claimed the spot). All state is plain and serializable for save-load and
|
|
27
|
+
* multiplayer sync via {@link snapshot}/{@link hydrate}.
|
|
28
|
+
*
|
|
29
|
+
* @capability resource-node depletable, respawning harvest nodes with tool-dependent yields
|
|
30
|
+
*/
|
|
31
|
+
export function createResourceNodeField(config) {
|
|
32
|
+
const rng = config.rng ?? Math.random;
|
|
33
|
+
const nodes = new Map();
|
|
34
|
+
const order = [];
|
|
35
|
+
for (const def of config.nodes) {
|
|
36
|
+
if (nodes.has(def.id))
|
|
37
|
+
throw new Error(`duplicate resource node "${def.id}"`);
|
|
38
|
+
const respawnBudget = def.respawnBudget ?? def.budget;
|
|
39
|
+
nodes.set(def.id, {
|
|
40
|
+
def,
|
|
41
|
+
budget: def.budget,
|
|
42
|
+
maxBudget: def.budget,
|
|
43
|
+
respawnBudget,
|
|
44
|
+
depleted: def.budget <= 0,
|
|
45
|
+
respawnTimer: 0,
|
|
46
|
+
respawnBlocked: false,
|
|
47
|
+
});
|
|
48
|
+
order.push(def.id);
|
|
49
|
+
}
|
|
50
|
+
const getNode = (id) => {
|
|
51
|
+
const node = nodes.get(id);
|
|
52
|
+
if (node === undefined)
|
|
53
|
+
throw new Error(`unknown resource node "${id}"`);
|
|
54
|
+
return node;
|
|
55
|
+
};
|
|
56
|
+
const isSuppressed = (node) => {
|
|
57
|
+
if (node.respawnBlocked)
|
|
58
|
+
return true;
|
|
59
|
+
if (config.respawnSuppressed === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
return config.respawnSuppressed(toState(node));
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
harvest(nodeId, tool, options = {}) {
|
|
65
|
+
const node = getNode(nodeId);
|
|
66
|
+
if (node.depleted || node.budget <= 0) {
|
|
67
|
+
return { nodeId, granted: [], spent: 0, harvested: false, depleted: node.depleted };
|
|
68
|
+
}
|
|
69
|
+
const power = tool.power !== undefined && tool.power > 0 ? tool.power : 1;
|
|
70
|
+
const spent = Math.min(power, node.budget);
|
|
71
|
+
const multiplier = options.multiplier ?? 1;
|
|
72
|
+
const defaultBias = tool.defaultBias ?? 1;
|
|
73
|
+
const granted = [];
|
|
74
|
+
for (const resource of node.def.resources) {
|
|
75
|
+
// Resolve variance for every resource so the rng stream stays independent of tool choice.
|
|
76
|
+
const base = resolveQuantity(resource.amount, rng);
|
|
77
|
+
const bias = tool.biases?.[resource.kind] ?? defaultBias;
|
|
78
|
+
const amount = base * bias * multiplier;
|
|
79
|
+
if (amount > 0)
|
|
80
|
+
granted.push({ kind: resource.kind, amount });
|
|
81
|
+
}
|
|
82
|
+
node.budget -= spent;
|
|
83
|
+
let depleted = false;
|
|
84
|
+
if (node.budget <= 0) {
|
|
85
|
+
node.budget = 0;
|
|
86
|
+
node.depleted = true;
|
|
87
|
+
depleted = true;
|
|
88
|
+
node.respawnTimer = node.def.respawn ?? 0;
|
|
89
|
+
}
|
|
90
|
+
return { nodeId, granted, spent, harvested: true, depleted };
|
|
91
|
+
},
|
|
92
|
+
tick(dt) {
|
|
93
|
+
if (dt <= 0)
|
|
94
|
+
return;
|
|
95
|
+
for (const id of order) {
|
|
96
|
+
const node = nodes.get(id);
|
|
97
|
+
if (!node.depleted || node.def.respawn === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
if (isSuppressed(node))
|
|
100
|
+
continue;
|
|
101
|
+
node.respawnTimer -= dt;
|
|
102
|
+
if (node.respawnTimer <= 0) {
|
|
103
|
+
node.budget = node.respawnBudget;
|
|
104
|
+
node.depleted = false;
|
|
105
|
+
node.respawnTimer = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
state(nodeId) {
|
|
110
|
+
return toState(getNode(nodeId));
|
|
111
|
+
},
|
|
112
|
+
isDepleted(nodeId) {
|
|
113
|
+
return getNode(nodeId).depleted;
|
|
114
|
+
},
|
|
115
|
+
setRespawnBlocked(nodeId, blocked) {
|
|
116
|
+
getNode(nodeId).respawnBlocked = blocked;
|
|
117
|
+
},
|
|
118
|
+
ids() {
|
|
119
|
+
return order;
|
|
120
|
+
},
|
|
121
|
+
snapshot() {
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const id of order)
|
|
124
|
+
out[id] = toState(nodes.get(id));
|
|
125
|
+
return out;
|
|
126
|
+
},
|
|
127
|
+
hydrate(snapshot) {
|
|
128
|
+
for (const id of order) {
|
|
129
|
+
const saved = snapshot[id];
|
|
130
|
+
if (saved === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
const node = nodes.get(id);
|
|
133
|
+
node.budget = saved.budget;
|
|
134
|
+
node.depleted = saved.depleted;
|
|
135
|
+
node.respawnTimer = saved.respawnTimer;
|
|
136
|
+
node.respawnBlocked = saved.respawnBlocked;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/world/roads.d.ts
CHANGED
|
@@ -29,12 +29,29 @@ export interface RoadRibbonOptions {
|
|
|
29
29
|
* into a mesh, and tests can assert on it directly.
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildRoadRibbon(path: readonly RoadPoint[], width: number, sampleHeight: (x: number, z: number) => number, options?: RoadRibbonOptions): RoadRibbon;
|
|
32
|
+
/** A circular exclusion zone: dashes whose midpoint falls inside are dropped (e.g. junction patches). */
|
|
33
|
+
export interface DashExclusion {
|
|
34
|
+
center: RoadPoint;
|
|
35
|
+
radius: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a flat, ground-draped disc patch centered on `center` — the welded junction surface that
|
|
39
|
+
* covers crossing road ribbons so they read as one intersection instead of stacking/z-fighting.
|
|
40
|
+
* A triangle fan of `segments` sides (default 16), every vertex draped at `sampleHeight + elevation`.
|
|
41
|
+
* Pure geometry; the shell meshes the result with the asphalt material.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildJunctionPatch(center: RoadPoint, radius: number, sampleHeight: (x: number, z: number) => number, options?: {
|
|
44
|
+
elevation?: number;
|
|
45
|
+
segments?: number;
|
|
46
|
+
}): RoadRibbon;
|
|
32
47
|
/**
|
|
33
48
|
* Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line,
|
|
34
49
|
* `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back
|
|
35
|
-
* through {@link buildRoadRibbon} with a thin width to mesh the dashes.
|
|
50
|
+
* through {@link buildRoadRibbon} with a thin width to mesh the dashes. Pass `exclude` circles
|
|
51
|
+
* (junction patches) to interrupt the center line through intersections — any dash whose midpoint
|
|
52
|
+
* lands inside an exclusion is dropped.
|
|
36
53
|
*/
|
|
37
|
-
export declare function dashSegments(path: readonly RoadPoint[], dashLength?: number, gapLength?: number): readonly (readonly RoadPoint[])[];
|
|
54
|
+
export declare function dashSegments(path: readonly RoadPoint[], dashLength?: number, gapLength?: number, exclude?: readonly DashExclusion[]): readonly (readonly RoadPoint[])[];
|
|
38
55
|
/** Result of {@link nearestOnPath}: closest point on the centerline plus distance and tangent. */
|
|
39
56
|
export interface RoadSample {
|
|
40
57
|
/** Closest point on the centerline. */
|
package/dist/world/roads.js
CHANGED
|
@@ -102,12 +102,48 @@ export function buildRoadRibbon(path, width, sampleHeight, options = {}) {
|
|
|
102
102
|
}
|
|
103
103
|
return { positions, indices };
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a flat, ground-draped disc patch centered on `center` — the welded junction surface that
|
|
107
|
+
* covers crossing road ribbons so they read as one intersection instead of stacking/z-fighting.
|
|
108
|
+
* A triangle fan of `segments` sides (default 16), every vertex draped at `sampleHeight + elevation`.
|
|
109
|
+
* Pure geometry; the shell meshes the result with the asphalt material.
|
|
110
|
+
*/
|
|
111
|
+
export function buildJunctionPatch(center, radius, sampleHeight, options = {}) {
|
|
112
|
+
const elevation = options.elevation ?? 0.08;
|
|
113
|
+
const segments = Math.max(3, Math.floor(options.segments ?? 16));
|
|
114
|
+
if (radius <= 0)
|
|
115
|
+
return { positions: new Float32Array(0), indices: new Uint32Array(0) };
|
|
116
|
+
const [cx, cz] = center;
|
|
117
|
+
// Center vertex + one ring vertex per segment.
|
|
118
|
+
const positions = new Float32Array((segments + 1) * 3);
|
|
119
|
+
positions[0] = cx;
|
|
120
|
+
positions[1] = sampleHeight(cx, cz) + elevation;
|
|
121
|
+
positions[2] = cz;
|
|
122
|
+
for (let i = 0; i < segments; i += 1) {
|
|
123
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
124
|
+
const x = cx + Math.cos(angle) * radius;
|
|
125
|
+
const z = cz + Math.sin(angle) * radius;
|
|
126
|
+
const base = (i + 1) * 3;
|
|
127
|
+
positions[base] = x;
|
|
128
|
+
positions[base + 1] = sampleHeight(x, z) + elevation;
|
|
129
|
+
positions[base + 2] = z;
|
|
130
|
+
}
|
|
131
|
+
const indices = new Uint32Array(segments * 3);
|
|
132
|
+
for (let i = 0; i < segments; i += 1) {
|
|
133
|
+
indices[i * 3] = 0;
|
|
134
|
+
indices[i * 3 + 1] = i + 1;
|
|
135
|
+
indices[i * 3 + 2] = ((i + 1) % segments) + 1;
|
|
136
|
+
}
|
|
137
|
+
return { positions, indices };
|
|
138
|
+
}
|
|
105
139
|
/**
|
|
106
140
|
* Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line,
|
|
107
141
|
* `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back
|
|
108
|
-
* through {@link buildRoadRibbon} with a thin width to mesh the dashes.
|
|
142
|
+
* through {@link buildRoadRibbon} with a thin width to mesh the dashes. Pass `exclude` circles
|
|
143
|
+
* (junction patches) to interrupt the center line through intersections — any dash whose midpoint
|
|
144
|
+
* lands inside an exclusion is dropped.
|
|
109
145
|
*/
|
|
110
|
-
export function dashSegments(path, dashLength = 3, gapLength = 3) {
|
|
146
|
+
export function dashSegments(path, dashLength = 3, gapLength = 3, exclude = []) {
|
|
111
147
|
if (path.length < 2 || dashLength <= 0 || gapLength < 0)
|
|
112
148
|
return [];
|
|
113
149
|
const cumulative = [0];
|
|
@@ -136,7 +172,28 @@ export function dashSegments(path, dashLength = 3, gapLength = 3) {
|
|
|
136
172
|
const end = Math.min(total, start + dashLength);
|
|
137
173
|
if (end - start < 1e-6)
|
|
138
174
|
break;
|
|
139
|
-
|
|
175
|
+
const a = pointAt(start);
|
|
176
|
+
const b = pointAt(end);
|
|
177
|
+
if (exclude.length > 0) {
|
|
178
|
+
const mx = (a[0] + b[0]) / 2;
|
|
179
|
+
const mz = (a[1] + b[1]) / 2;
|
|
180
|
+
let excluded = false;
|
|
181
|
+
for (let e = 0; e < exclude.length; e += 1) {
|
|
182
|
+
const zone = exclude[e];
|
|
183
|
+
const dx = mx - zone.center[0];
|
|
184
|
+
const dz = mz - zone.center[1];
|
|
185
|
+
if (dx * dx + dz * dz <= zone.radius * zone.radius) {
|
|
186
|
+
excluded = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (excluded) {
|
|
191
|
+
if (period <= 0)
|
|
192
|
+
break;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
dashes.push([a, b]);
|
|
140
197
|
if (period <= 0)
|
|
141
198
|
break;
|
|
142
199
|
}
|
package/dist/world/scatter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expandAabb, pointInAabb } from "./geometry.js";
|
|
2
|
+
import { seededRng } from "../random/rng.js";
|
|
2
3
|
function isAabb(area) {
|
|
3
4
|
return (typeof area.minX === "number" &&
|
|
4
5
|
typeof area.minZ === "number" &&
|
|
@@ -15,25 +16,6 @@ export function scatterAabb(area) {
|
|
|
15
16
|
const halfD = area.d / 2;
|
|
16
17
|
return { minX: cx - halfW, minZ: cz - halfD, maxX: cx + halfW, maxZ: cz + halfD };
|
|
17
18
|
}
|
|
18
|
-
function hashSeed(seed) {
|
|
19
|
-
const text = typeof seed === "number" ? seed.toString() : seed;
|
|
20
|
-
let h = 2166136261 >>> 0;
|
|
21
|
-
for (let i = 0; i < text.length; i++) {
|
|
22
|
-
h ^= text.charCodeAt(i);
|
|
23
|
-
h = Math.imul(h, 16777619);
|
|
24
|
-
}
|
|
25
|
-
return h >>> 0;
|
|
26
|
-
}
|
|
27
|
-
function mulberry32(state) {
|
|
28
|
-
let a = state >>> 0;
|
|
29
|
-
return () => {
|
|
30
|
-
a = (a + 0x6d2b79f5) >>> 0;
|
|
31
|
-
let t = a;
|
|
32
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
33
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
34
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
19
|
export function scatter(config) {
|
|
38
20
|
const aabb = scatterAabb(config.area);
|
|
39
21
|
const width = aabb.maxX - aabb.minX;
|
|
@@ -47,7 +29,7 @@ export function scatter(config) {
|
|
|
47
29
|
const jitter = config.jitter ?? 1;
|
|
48
30
|
const maxAttempts = config.maxAttempts ?? target * 30;
|
|
49
31
|
const avoid = (config.avoid ?? []).map((a) => (avoidMargin !== 0 ? expandAabb(a, avoidMargin) : a));
|
|
50
|
-
const rng =
|
|
32
|
+
const rng = seededRng(config.seed ?? "scatter");
|
|
51
33
|
const useDistance = minDistance > 0;
|
|
52
34
|
const buckets = new Map();
|
|
53
35
|
const bucketKey = (x, z) => `${Math.floor(x / minDistance)}:${Math.floor(z / minDistance)}`;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** A path vertex in the volume-local XZ frame. */
|
|
2
|
+
export type StreetVec2 = readonly [number, number];
|
|
3
|
+
/** Road hierarchy, widest to narrowest — shared by the city fabric and the renderer. */
|
|
4
|
+
export type StreetLevel = "boulevard" | "avenue" | "street" | "lane";
|
|
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 StreetFeatureKind = "bridge" | "tunnel";
|
|
7
|
+
/** The generator's chosen topology family: an open street `net`, or a closed `circuit` loop. */
|
|
8
|
+
export type StreetNetworkMode = "net" | "circuit";
|
|
9
|
+
/** One graph node: a junction, a dead end, or a mid-street bend, with its connection count. */
|
|
10
|
+
export interface StreetNode {
|
|
11
|
+
id: number;
|
|
12
|
+
x: number;
|
|
13
|
+
z: number;
|
|
14
|
+
degree: number;
|
|
15
|
+
}
|
|
16
|
+
/** One atomic node-to-node edge — the fabric graph consumes these (welds at shared node coords). */
|
|
17
|
+
export interface StreetEdge {
|
|
18
|
+
id: number;
|
|
19
|
+
/** Endpoint node ids. */
|
|
20
|
+
a: number;
|
|
21
|
+
b: number;
|
|
22
|
+
/** Sampled centerline; `points[0]` sits exactly on node `a`, the last on node `b`. */
|
|
23
|
+
points: StreetVec2[];
|
|
24
|
+
width: number;
|
|
25
|
+
level: StreetLevel;
|
|
26
|
+
/** True when this edge belongs to the main closed loop (circuit mode). */
|
|
27
|
+
loop: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** A feature span carried by a street: a `[from, to]` index window into the street's `points`. */
|
|
30
|
+
export interface StreetFeatureSpan {
|
|
31
|
+
kind: StreetFeatureKind;
|
|
32
|
+
from: number;
|
|
33
|
+
to: number;
|
|
34
|
+
/** Ground height at each bank/portal — the deck/floor reference the renderer drapes to. */
|
|
35
|
+
bankHeight: number;
|
|
36
|
+
}
|
|
37
|
+
/** One chained through-street: a maximal run of edges through degree-2 nodes, for rendering + furniture. */
|
|
38
|
+
export interface Street {
|
|
39
|
+
id: number;
|
|
40
|
+
/** Ordered node ids the chain visits; first === last when `loop`. */
|
|
41
|
+
nodes: number[];
|
|
42
|
+
/** Smoothed, turn-clamped centerline. */
|
|
43
|
+
points: StreetVec2[];
|
|
44
|
+
width: number;
|
|
45
|
+
level: StreetLevel;
|
|
46
|
+
/** Closed chain (a circuit lap or an inner ring). */
|
|
47
|
+
loop: boolean;
|
|
48
|
+
/** Cul-de-sac turning bulb when the street ends at a dangling node. */
|
|
49
|
+
bulb?: StreetVec2;
|
|
50
|
+
/** Bridge/tunnel spans along this street, if any. */
|
|
51
|
+
features: StreetFeatureSpan[];
|
|
52
|
+
}
|
|
53
|
+
/** One crossing of three or more streets: patch center/radius plus outgoing arm directions. */
|
|
54
|
+
export interface StreetJunction {
|
|
55
|
+
x: number;
|
|
56
|
+
z: number;
|
|
57
|
+
radius: number;
|
|
58
|
+
level: StreetLevel;
|
|
59
|
+
/** Outgoing arm directions (radians) with the crossing street width, for crosswalks/patches. */
|
|
60
|
+
arms: {
|
|
61
|
+
angle: number;
|
|
62
|
+
width: number;
|
|
63
|
+
}[];
|
|
64
|
+
}
|
|
65
|
+
/** One dangling street end kept as a cul-de-sac: node position plus the heading pointing off the road. */
|
|
66
|
+
export interface StreetDeadEnd {
|
|
67
|
+
node: number;
|
|
68
|
+
x: number;
|
|
69
|
+
z: number;
|
|
70
|
+
/** Yaw (radians) facing outward, away from the network. */
|
|
71
|
+
heading: number;
|
|
72
|
+
width: number;
|
|
73
|
+
}
|
|
74
|
+
/** A resolved path feature in world-of-the-volume space: a bridge deck or tunnel bore centerline. */
|
|
75
|
+
export interface StreetFeature {
|
|
76
|
+
kind: StreetFeatureKind;
|
|
77
|
+
points: StreetVec2[];
|
|
78
|
+
width: number;
|
|
79
|
+
bankHeight: number;
|
|
80
|
+
}
|
|
81
|
+
/** The fully-resolved network in volume-local coords. */
|
|
82
|
+
export interface StreetNetwork {
|
|
83
|
+
mode: StreetNetworkMode;
|
|
84
|
+
nodes: StreetNode[];
|
|
85
|
+
/** Atomic edges — feed to the block/parcel fabric. */
|
|
86
|
+
edges: StreetEdge[];
|
|
87
|
+
/** Chained through-streets — feed to the renderer, furniture, and analysis. */
|
|
88
|
+
streets: Street[];
|
|
89
|
+
junctions: StreetJunction[];
|
|
90
|
+
deadEnds: StreetDeadEnd[];
|
|
91
|
+
bridges: StreetFeature[];
|
|
92
|
+
tunnels: StreetFeature[];
|
|
93
|
+
/** Independent cycle count (E − V + components): 0 = pure tree, ≥1 = has loops. */
|
|
94
|
+
loops: number;
|
|
95
|
+
}
|
|
96
|
+
/** Fully-defaulted slider set the generator reads. */
|
|
97
|
+
export interface StreetNetworkRules {
|
|
98
|
+
seed: string;
|
|
99
|
+
/** 1 = regular lattice nodes; 0 = organic scatter. */
|
|
100
|
+
gridness: number;
|
|
101
|
+
/** 0 = tree (dead-ends); 1 = a single closed circuit. */
|
|
102
|
+
loopiness: number;
|
|
103
|
+
/** Extra chord edges between neighbors → mesh density. */
|
|
104
|
+
connectivity: number;
|
|
105
|
+
/** Spur-lane density forking off the mains. */
|
|
106
|
+
branching: number;
|
|
107
|
+
/** Fraction of dangling ends kept as cul-de-sacs (vs reconnected). */
|
|
108
|
+
deadEnds: number;
|
|
109
|
+
/** Target node spacing / block size, world units. */
|
|
110
|
+
segmentLength: number;
|
|
111
|
+
/** Cross-axis spacing multiplier (≥1 = long skinny Manhattan blocks). */
|
|
112
|
+
aspect: number;
|
|
113
|
+
/** Sideways wander amplitude, 0..1. */
|
|
114
|
+
winding: number;
|
|
115
|
+
/** Minimum curve radius (m) — caps wander curvature and corner tightness. */
|
|
116
|
+
minCurveRadius: number;
|
|
117
|
+
/** Shallowest corner (degrees) a street keeps — gentler bends are straightened. */
|
|
118
|
+
minTurnAngle: number;
|
|
119
|
+
/** Sharpest corner (degrees) a street may take — tighter ones are beveled away. */
|
|
120
|
+
maxTurnAngle: number;
|
|
121
|
+
/** Base street width; the hierarchy scales off it. */
|
|
122
|
+
width: number;
|
|
123
|
+
/** Share of avenues upgraded to boulevards, 0..1. */
|
|
124
|
+
boulevards: number;
|
|
125
|
+
}
|
|
126
|
+
/** Ground sampler + feature toggles enabling bridges/tunnels; omit for a flat, feature-free network. */
|
|
127
|
+
export interface StreetNetworkContext {
|
|
128
|
+
/** Volume-local ground height sampler. */
|
|
129
|
+
heightAt?: (x: number, z: number) => number;
|
|
130
|
+
/** Ground below this (local) height is a gap streets bridge over. */
|
|
131
|
+
minElevation?: number;
|
|
132
|
+
/** Emit bridge decks over sub-`minElevation` gaps. */
|
|
133
|
+
bridges?: boolean;
|
|
134
|
+
/** Emit tunnel bores where the ground rises this far above a span's banks. */
|
|
135
|
+
tunnels?: boolean;
|
|
136
|
+
/** Ridge height above the banks that triggers a tunnel. Default 6. */
|
|
137
|
+
tunnelClearance?: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Hard turn-angle clamp on a polyline with fixed endpoints: interior corners gentler than `minRad`
|
|
141
|
+
* are straightened away (deliberate winding only, no micro-wiggle) and corners sharper than `maxRad`
|
|
142
|
+
* are beveled — the corner vertex is replaced by two points pulled back along each leg, which
|
|
143
|
+
* strictly reduces the angle. Endpoints never move, so a graph node stays welded.
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
export declare function clampTurns(points: readonly StreetVec2[], minRad: number, maxRad: number): StreetVec2[];
|
|
147
|
+
/** Decide which topology family the sliders call for. Circuit wins when loops dominate and both
|
|
148
|
+
* branching and mesh connectivity are low — exactly the "race track" corner of the slider space.
|
|
149
|
+
* @internal */
|
|
150
|
+
export declare function streetNetworkMode(rules: StreetNetworkRules): StreetNetworkMode;
|
|
151
|
+
/**
|
|
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 StreetNetworkContext} with a ground sampler to turn
|
|
154
|
+
* water gaps into bridges and ridges into tunnels. Coordinates are volume-local; the caller maps to
|
|
155
|
+
* world space.
|
|
156
|
+
*
|
|
157
|
+
* @capability street-generator build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
|
|
158
|
+
*/
|
|
159
|
+
export declare function generateStreets(rules: StreetNetworkRules, hx: number, hz: number, context?: StreetNetworkContext): StreetNetwork;
|