@jgengine/core 0.18.0 → 0.19.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 +265 -2
- package/dist/ai/decisionGraph.d.ts +96 -0
- package/dist/ai/decisionGraph.js +212 -0
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +155 -0
- package/dist/anim/animGraph.js +414 -0
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +35 -0
- package/dist/anim/locomotionGraph.js +62 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +12 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +92 -1
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +24 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +20 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +59 -1
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +14 -1
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +167 -6
- package/dist/game/systemRuntime.js +6 -1
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/input/actionBindings.d.ts +17 -2
- package/dist/input/actionBindings.js +24 -11
- package/dist/input/actionContexts.d.ts +45 -0
- package/dist/input/actionContexts.js +113 -0
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +79 -0
- package/dist/input/gamepadModel.js +108 -0
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +196 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +30 -3
- package/dist/movement/playerMovement.js +304 -7
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +39 -8
- package/dist/movement/solidObstacles.js +71 -6
- package/dist/movement/voxelController.js +7 -7
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +74 -0
- package/dist/nav/navMesh.js +570 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +29 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +37 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +203 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +151 -0
- package/dist/physics/physicsWorldBackend.d.ts +45 -0
- package/dist/physics/physicsWorldBackend.js +518 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +128 -0
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +19 -6
- package/dist/render/lookPreset.js +56 -4
- package/dist/render/postProcessing.d.ts +40 -1
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +52 -0
- package/dist/runtime/commandInput.js +71 -0
- package/dist/runtime/commandRunner.d.ts +39 -7
- package/dist/runtime/commandRunner.js +35 -5
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +18 -1
- package/dist/runtime/gameContextTypes.d.ts +33 -7
- package/dist/runtime/gameRuntime.d.ts +23 -5
- package/dist/runtime/gameRuntime.js +21 -4
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +14 -0
- package/dist/runtime/inputSnapshot.js +11 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +41 -2
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +29 -1
- package/dist/scene/behaviorRuntime.js +71 -4
- package/dist/scene/behaviors.d.ts +12 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +70 -0
- package/dist/sensor/perception.js +189 -0
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +21 -0
- package/dist/settings/graphicsProfile.js +48 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +46 -2
- package/dist/world/buildings.js +53 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +139 -0
- package/dist/world/cityKind.js +367 -16
- package/dist/world/features.d.ts +56 -3
- package/dist/world/features.js +13 -2
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +501 -453
- package/skills/jgengine-gameplay/capabilities.md +36 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
package/dist/world/cityKind.js
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* (`cityContent`) — setback tower tiers, gabled houses, gambrel barns, domed silos. Blocks left
|
|
19
19
|
* unbuilt become plazas/greens/meadows by band, or crop-row farm fields; slivers and block
|
|
20
20
|
* interiors classify as buffers and courtyards, never leftover rectangles. Street
|
|
21
|
-
* furniture (lights, species-mixed trees, estate hedges, driveways, parking lots
|
|
22
|
-
* bounded, seeded data hooked to the network.
|
|
21
|
+
* furniture (lights, species-mixed trees, estate hedges, driveways, parking lots, parked vehicles,
|
|
22
|
+
* junction signals and sign posts) resolves as bounded, seeded data hooked to the network. Turn `race` on and the district also hands back a
|
|
23
|
+
* {@link ResolvedRace}: a closed lap lifted out of its OWN streets with every side street leaving it
|
|
24
|
+
* sealed off — the Monaco / open-world street-race model, not a separate track. Child `cityzone` volumes override the band/mix
|
|
23
25
|
* locally, so hand-placed intent wins over the profile. Named presets (Manhattan, Los Angeles,
|
|
24
26
|
* Beverly Hills, rural Ohio…) ship as plain slider bundles on the schema. Pure config + resolver
|
|
25
27
|
* here; the instanced renderer lives in the `shell`. Everything is deterministic per seed and
|
|
@@ -31,7 +33,8 @@ import { seededStreams } from "../random/rng.js";
|
|
|
31
33
|
import { parseParams, registerSceneKind, } from "../scene/sceneKinds.js";
|
|
32
34
|
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_STYLE } from "./buildings.js";
|
|
33
35
|
import { buildLotPieces, pickClass, pickSpecies, rollClassPlacement, zoneBand, zoneMetric, } from "./cityContent.js";
|
|
34
|
-
import {
|
|
36
|
+
import { circuitKerbs, extractCircuitRoute, } from "./raceCircuit.js";
|
|
37
|
+
import { furnitureSpots, parkingSpots } from "./streets.js";
|
|
35
38
|
import { CITY_BUILDING_BUDGET, budgetWarning, placedCoverage } from "./scatterCoverage.js";
|
|
36
39
|
import { generateStreets, streetNetworkMode, } from "./streetGenerator.js";
|
|
37
40
|
import { buildablePolygon, carveCorridors, conformPolygonToRing, cutParcel, extractBlocks, isSliverBlock, pointsInPolygon, RingWalker, } from "./cityBlocks.js";
|
|
@@ -51,6 +54,11 @@ export const CITY_DEFAULTS = {
|
|
|
51
54
|
minCurveRadius: 24,
|
|
52
55
|
minTurnAngle: 0,
|
|
53
56
|
maxTurnAngle: 150,
|
|
57
|
+
outline: 0,
|
|
58
|
+
residentialBranches: 0,
|
|
59
|
+
elevation: 0,
|
|
60
|
+
maxGrade: 0.07,
|
|
61
|
+
compactness: 0,
|
|
54
62
|
fabric: true,
|
|
55
63
|
tunnels: true,
|
|
56
64
|
blockSize: 48,
|
|
@@ -92,6 +100,8 @@ export const CITY_DEFAULTS = {
|
|
|
92
100
|
{ item: "conifer", weight: 1 },
|
|
93
101
|
],
|
|
94
102
|
lightDensity: 0.5,
|
|
103
|
+
parkedCars: 0.55,
|
|
104
|
+
signals: 0.7,
|
|
95
105
|
hedges: true,
|
|
96
106
|
driveways: true,
|
|
97
107
|
parking: true,
|
|
@@ -102,6 +112,14 @@ export const CITY_DEFAULTS = {
|
|
|
102
112
|
bridgeStyle: "arch",
|
|
103
113
|
sidewalks: true,
|
|
104
114
|
sidewalkWidth: 1.9,
|
|
115
|
+
race: false,
|
|
116
|
+
raceLapLength: 1200,
|
|
117
|
+
raceArterial: 0.7,
|
|
118
|
+
raceCheckpoints: 12,
|
|
119
|
+
raceLaps: 3,
|
|
120
|
+
raceSeals: true,
|
|
121
|
+
raceGridSlots: 12,
|
|
122
|
+
raceCornerRadius: 9,
|
|
105
123
|
style: DEFAULT_BUILDING_STYLE,
|
|
106
124
|
buildingKit: "",
|
|
107
125
|
seed: "",
|
|
@@ -110,6 +128,7 @@ export const CITY_DEFAULTS = {
|
|
|
110
128
|
export const CITY_SCHEMA = {
|
|
111
129
|
groups: [
|
|
112
130
|
{ id: "layout", label: "Layout" },
|
|
131
|
+
{ id: "race", label: "Race" },
|
|
113
132
|
{ id: "placement", label: "Placement" },
|
|
114
133
|
{ id: "zoning", label: "Zoning" },
|
|
115
134
|
{ id: "buildings", label: "Buildings" },
|
|
@@ -125,6 +144,11 @@ export const CITY_SCHEMA = {
|
|
|
125
144
|
{ type: "range", key: "minCurveRadius", label: "min curve radius", group: "layout", min: 6, max: 120, step: 1, default: CITY_DEFAULTS.minCurveRadius, unit: "m" },
|
|
126
145
|
{ type: "range", key: "minTurnAngle", label: "min turn angle", group: "layout", min: 0, max: 60, step: 1, default: CITY_DEFAULTS.minTurnAngle, unit: "°" },
|
|
127
146
|
{ type: "range", key: "maxTurnAngle", label: "max turn angle", group: "layout", min: 30, max: 180, step: 1, default: CITY_DEFAULTS.maxTurnAngle, unit: "°" },
|
|
147
|
+
{ type: "range", key: "outline", label: "outline", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.outline },
|
|
148
|
+
{ type: "range", key: "residentialBranches", label: "residential branches", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.residentialBranches },
|
|
149
|
+
{ type: "range", key: "elevation", label: "road elevation", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.elevation },
|
|
150
|
+
{ type: "range", key: "maxGrade", label: "max grade", group: "layout", min: 0.01, max: 0.3, step: 0.005, default: CITY_DEFAULTS.maxGrade },
|
|
151
|
+
{ type: "range", key: "compactness", label: "circuit compactness", group: "layout", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.compactness },
|
|
128
152
|
{ type: "range", key: "blockSize", label: "block size", group: "layout", min: 20, max: 140, step: 1, default: CITY_DEFAULTS.blockSize, unit: "m" },
|
|
129
153
|
{ type: "range", key: "blockAspect", label: "block aspect", group: "layout", min: 1, max: 3, step: 0.05, default: CITY_DEFAULTS.blockAspect },
|
|
130
154
|
{ type: "range", key: "openSpace", label: "open space", group: "layout", min: 0, max: 0.9, step: 0.01, default: CITY_DEFAULTS.openSpace },
|
|
@@ -140,6 +164,7 @@ export const CITY_SCHEMA = {
|
|
|
140
164
|
options: [{ value: "auto" }, { value: "gravel" }],
|
|
141
165
|
},
|
|
142
166
|
{ type: "bool", key: "fabric", label: "buildings & parcels", group: "layout", default: CITY_DEFAULTS.fabric },
|
|
167
|
+
{ type: "bool", key: "solid", label: "solid buildings", group: "layout", default: true },
|
|
143
168
|
{ type: "bool", key: "bridges", label: "bridges over water", group: "layout", default: CITY_DEFAULTS.bridges },
|
|
144
169
|
{ type: "bool", key: "tunnels", label: "tunnels through ridges", group: "layout", default: CITY_DEFAULTS.tunnels },
|
|
145
170
|
{
|
|
@@ -153,6 +178,14 @@ export const CITY_SCHEMA = {
|
|
|
153
178
|
{ type: "bool", key: "sidewalks", label: "sidewalks", group: "layout", default: CITY_DEFAULTS.sidewalks },
|
|
154
179
|
{ type: "range", key: "sidewalkWidth", label: "sidewalk width", group: "layout", min: 1, max: 4, step: 0.1, default: CITY_DEFAULTS.sidewalkWidth, unit: "m" },
|
|
155
180
|
{ type: "action", key: "layoutRandomize", label: "randomize layout", group: "layout", action: "randomize" },
|
|
181
|
+
{ type: "bool", key: "race", label: "street race", group: "race", default: CITY_DEFAULTS.race },
|
|
182
|
+
{ type: "range", key: "raceLapLength", label: "lap length", group: "race", min: 200, max: 6000, step: 50, default: CITY_DEFAULTS.raceLapLength, unit: "m" },
|
|
183
|
+
{ type: "range", key: "raceArterial", label: "arterial bias", group: "race", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.raceArterial },
|
|
184
|
+
{ type: "range", key: "raceCheckpoints", label: "checkpoints", group: "race", min: 2, max: 48, step: 1, default: CITY_DEFAULTS.raceCheckpoints },
|
|
185
|
+
{ type: "range", key: "raceLaps", label: "laps", group: "race", min: 1, max: 20, step: 1, default: CITY_DEFAULTS.raceLaps },
|
|
186
|
+
{ type: "bool", key: "raceSeals", label: "seal side streets", group: "race", default: CITY_DEFAULTS.raceSeals },
|
|
187
|
+
{ type: "range", key: "raceGridSlots", label: "grid slots", group: "race", min: 0, max: 40, step: 1, default: CITY_DEFAULTS.raceGridSlots },
|
|
188
|
+
{ type: "range", key: "raceCornerRadius", label: "corner radius", group: "race", min: 3, max: 40, step: 1, default: CITY_DEFAULTS.raceCornerRadius, unit: "m" },
|
|
156
189
|
{
|
|
157
190
|
type: "select",
|
|
158
191
|
key: "profile",
|
|
@@ -189,6 +222,8 @@ export const CITY_SCHEMA = {
|
|
|
189
222
|
{ type: "range", key: "treeDensity", label: "tree density", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.treeDensity },
|
|
190
223
|
{ type: "weightedList", key: "treeMix", label: "tree mix", group: "greenery", itemLabel: "species", default: CITY_DEFAULTS.treeMix },
|
|
191
224
|
{ type: "range", key: "lightDensity", label: "street lights", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.lightDensity },
|
|
225
|
+
{ type: "range", key: "parkedCars", label: "parked cars", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.parkedCars },
|
|
226
|
+
{ type: "range", key: "signals", label: "traffic signals", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.signals },
|
|
192
227
|
{ type: "bool", key: "hedges", label: "estate hedges", group: "greenery", default: CITY_DEFAULTS.hedges },
|
|
193
228
|
{ type: "bool", key: "driveways", label: "driveways", group: "greenery", default: CITY_DEFAULTS.driveways },
|
|
194
229
|
{ type: "bool", key: "parking", label: "parking lots", group: "greenery", default: CITY_DEFAULTS.parking },
|
|
@@ -208,8 +243,8 @@ export const CITY_SCHEMA = {
|
|
|
208
243
|
openSpace: 0.05,
|
|
209
244
|
roadsideOccupancy: 0.97,
|
|
210
245
|
blockDensity: 0.85,
|
|
211
|
-
buildingRoadSetback: 0.
|
|
212
|
-
buildingSpacing: 0.
|
|
246
|
+
buildingRoadSetback: 0.55,
|
|
247
|
+
buildingSpacing: 0.55,
|
|
213
248
|
clusterStrength: 0.25,
|
|
214
249
|
streetWidth: 9,
|
|
215
250
|
boulevards: 0.5,
|
|
@@ -235,6 +270,8 @@ export const CITY_SCHEMA = {
|
|
|
235
270
|
treeDensity: 0.3,
|
|
236
271
|
treeMix: [{ item: "broadleaf", weight: 1 }],
|
|
237
272
|
lightDensity: 0.8,
|
|
273
|
+
parkedCars: 0.95,
|
|
274
|
+
signals: 1,
|
|
238
275
|
hedges: false,
|
|
239
276
|
driveways: false,
|
|
240
277
|
parking: false,
|
|
@@ -288,6 +325,8 @@ export const CITY_SCHEMA = {
|
|
|
288
325
|
{ item: "cypress", weight: 1 },
|
|
289
326
|
],
|
|
290
327
|
lightDensity: 0.6,
|
|
328
|
+
parkedCars: 0.7,
|
|
329
|
+
signals: 0.8,
|
|
291
330
|
hedges: false,
|
|
292
331
|
driveways: true,
|
|
293
332
|
parking: true,
|
|
@@ -339,6 +378,8 @@ export const CITY_SCHEMA = {
|
|
|
339
378
|
{ item: "broadleaf", weight: 2 },
|
|
340
379
|
],
|
|
341
380
|
lightDensity: 0.35,
|
|
381
|
+
parkedCars: 0.2,
|
|
382
|
+
signals: 0.12,
|
|
342
383
|
hedges: true,
|
|
343
384
|
driveways: true,
|
|
344
385
|
parking: false,
|
|
@@ -392,6 +433,8 @@ export const CITY_SCHEMA = {
|
|
|
392
433
|
{ item: "conifer", weight: 2 },
|
|
393
434
|
],
|
|
394
435
|
lightDensity: 0.1,
|
|
436
|
+
parkedCars: 0.04,
|
|
437
|
+
signals: 0,
|
|
395
438
|
hedges: false,
|
|
396
439
|
driveways: true,
|
|
397
440
|
parking: false,
|
|
@@ -413,6 +456,9 @@ export const CITY_SCHEMA = {
|
|
|
413
456
|
minCurveRadius: 34,
|
|
414
457
|
minTurnAngle: 0,
|
|
415
458
|
maxTurnAngle: 95,
|
|
459
|
+
compactness: 0.5,
|
|
460
|
+
elevation: 0.3,
|
|
461
|
+
maxGrade: 0.06,
|
|
416
462
|
blockSize: 95,
|
|
417
463
|
blockAspect: 1,
|
|
418
464
|
openSpace: 0,
|
|
@@ -426,6 +472,8 @@ export const CITY_SCHEMA = {
|
|
|
426
472
|
sidewalks: false,
|
|
427
473
|
treeDensity: 0.2,
|
|
428
474
|
lightDensity: 0.25,
|
|
475
|
+
parkedCars: 0,
|
|
476
|
+
signals: 0,
|
|
429
477
|
hedges: false,
|
|
430
478
|
driveways: false,
|
|
431
479
|
parking: false,
|
|
@@ -446,6 +494,9 @@ export const CITY_SCHEMA = {
|
|
|
446
494
|
minCurveRadius: 12,
|
|
447
495
|
minTurnAngle: 0,
|
|
448
496
|
maxTurnAngle: 150,
|
|
497
|
+
outline: 0.5,
|
|
498
|
+
elevation: 0.8,
|
|
499
|
+
maxGrade: 0.14,
|
|
449
500
|
blockSize: 72,
|
|
450
501
|
blockAspect: 1,
|
|
451
502
|
openSpace: 0,
|
|
@@ -459,6 +510,8 @@ export const CITY_SCHEMA = {
|
|
|
459
510
|
sidewalks: false,
|
|
460
511
|
treeDensity: 0.65,
|
|
461
512
|
lightDensity: 0.05,
|
|
513
|
+
parkedCars: 0,
|
|
514
|
+
signals: 0,
|
|
462
515
|
hedges: false,
|
|
463
516
|
driveways: false,
|
|
464
517
|
parking: false,
|
|
@@ -490,6 +543,8 @@ export const CITY_SCHEMA = {
|
|
|
490
543
|
floorsMax: 34,
|
|
491
544
|
treeDensity: 0.55,
|
|
492
545
|
lightDensity: 0.6,
|
|
546
|
+
parkedCars: 0.85,
|
|
547
|
+
signals: 0.9,
|
|
493
548
|
style: "generic",
|
|
494
549
|
},
|
|
495
550
|
},
|
|
@@ -527,6 +582,11 @@ export function readCityRules(meta) {
|
|
|
527
582
|
minCurveRadius: params["minCurveRadius"],
|
|
528
583
|
minTurnAngle: params["minTurnAngle"],
|
|
529
584
|
maxTurnAngle: params["maxTurnAngle"],
|
|
585
|
+
outline: params["outline"],
|
|
586
|
+
residentialBranches: params["residentialBranches"],
|
|
587
|
+
elevation: params["elevation"],
|
|
588
|
+
maxGrade: params["maxGrade"],
|
|
589
|
+
compactness: params["compactness"],
|
|
530
590
|
fabric: params["fabric"],
|
|
531
591
|
tunnels: params["tunnels"],
|
|
532
592
|
blockSize: params["blockSize"],
|
|
@@ -554,6 +614,8 @@ export function readCityRules(meta) {
|
|
|
554
614
|
treeDensity: params["treeDensity"],
|
|
555
615
|
treeMix: params["treeMix"],
|
|
556
616
|
lightDensity: params["lightDensity"],
|
|
617
|
+
parkedCars: params["parkedCars"],
|
|
618
|
+
signals: params["signals"],
|
|
557
619
|
hedges: params["hedges"],
|
|
558
620
|
driveways: params["driveways"],
|
|
559
621
|
parking: params["parking"],
|
|
@@ -564,11 +626,68 @@ export function readCityRules(meta) {
|
|
|
564
626
|
bridgeStyle: params["bridgeStyle"],
|
|
565
627
|
sidewalks: params["sidewalks"],
|
|
566
628
|
sidewalkWidth: params["sidewalkWidth"],
|
|
629
|
+
race: params["race"],
|
|
630
|
+
raceLapLength: params["raceLapLength"],
|
|
631
|
+
raceArterial: params["raceArterial"],
|
|
632
|
+
raceCheckpoints: params["raceCheckpoints"],
|
|
633
|
+
raceLaps: params["raceLaps"],
|
|
634
|
+
raceSeals: params["raceSeals"],
|
|
635
|
+
raceGridSlots: params["raceGridSlots"],
|
|
636
|
+
raceCornerRadius: params["raceCornerRadius"],
|
|
567
637
|
style: params["style"],
|
|
568
638
|
buildingKit: params["buildingKit"] ?? CITY_DEFAULTS.buildingKit,
|
|
569
639
|
seed: params["seed"],
|
|
570
640
|
};
|
|
571
641
|
}
|
|
642
|
+
/** Lot pieces that block: walls and roofs. Trim and accents (cornices, awnings) stay passable. */
|
|
643
|
+
const SOLID_PIECE_ROLES = new Set(["wall", "roof"]);
|
|
644
|
+
/**
|
|
645
|
+
* One solid per wall and roof piece of every lot, grounded the way the city renderer grounds it:
|
|
646
|
+
* grade at the highest lot corner, foundations down to half a metre under the lowest.
|
|
647
|
+
* @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
|
|
648
|
+
*/
|
|
649
|
+
export function citySolids(city, sampleHeight = () => 0) {
|
|
650
|
+
const solids = [];
|
|
651
|
+
for (const lot of city.lots) {
|
|
652
|
+
const c = Math.cos(lot.rotationY);
|
|
653
|
+
const s = Math.sin(lot.rotationY);
|
|
654
|
+
const hw = lot.size[0] / 2;
|
|
655
|
+
const hd = lot.size[1] / 2;
|
|
656
|
+
let minY = Infinity;
|
|
657
|
+
let maxY = -Infinity;
|
|
658
|
+
for (const [dx, dz] of [
|
|
659
|
+
[hw, hd],
|
|
660
|
+
[hw, -hd],
|
|
661
|
+
[-hw, hd],
|
|
662
|
+
[-hw, -hd],
|
|
663
|
+
]) {
|
|
664
|
+
const y = sampleHeight(lot.center[0] + dx * c + dz * s, lot.center[1] - dx * s + dz * c);
|
|
665
|
+
minY = Math.min(minY, y);
|
|
666
|
+
maxY = Math.max(maxY, y);
|
|
667
|
+
}
|
|
668
|
+
const grade = maxY;
|
|
669
|
+
const foundationBase = minY - 0.5;
|
|
670
|
+
for (const piece of lot.pieces) {
|
|
671
|
+
if (!SOLID_PIECE_ROLES.has(piece.role))
|
|
672
|
+
continue;
|
|
673
|
+
const bottom = piece.grounded ? foundationBase : grade + piece.offset[1];
|
|
674
|
+
const top = piece.grounded ? grade + piece.size[1] : grade + piece.offset[1] + piece.size[1];
|
|
675
|
+
if (top - bottom <= 0 || piece.size[0] <= 0 || piece.size[2] <= 0)
|
|
676
|
+
continue;
|
|
677
|
+
const yaw = lot.rotationY + piece.rotationY;
|
|
678
|
+
solids.push({
|
|
679
|
+
center: [
|
|
680
|
+
lot.center[0] + piece.offset[0] * c + piece.offset[2] * s,
|
|
681
|
+
(bottom + top) / 2,
|
|
682
|
+
lot.center[1] - piece.offset[0] * s + piece.offset[2] * c,
|
|
683
|
+
],
|
|
684
|
+
halfExtents: [piece.size[0] / 2, (top - bottom) / 2, piece.size[2] / 2],
|
|
685
|
+
...(yaw === 0 ? {} : { rotationY: yaw }),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return solids;
|
|
690
|
+
}
|
|
572
691
|
/** Bounded-work caps so a huge volume can never generate unbounded content. */
|
|
573
692
|
const MAX_STREETS = 320;
|
|
574
693
|
/** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
|
|
@@ -578,7 +697,19 @@ const MAX_TREES = 3200;
|
|
|
578
697
|
const MAX_LIGHTS = 1200;
|
|
579
698
|
const MAX_FIELD_ROWS = 2400;
|
|
580
699
|
const MAX_PARKING = 380;
|
|
700
|
+
const MAX_VEHICLES = 900;
|
|
701
|
+
const MAX_SIGNALS = 640;
|
|
702
|
+
const MAX_RACE_SEALS = 400;
|
|
703
|
+
const MAX_RACE_KERBS = 320;
|
|
581
704
|
const TAU = Math.PI * 2;
|
|
705
|
+
/** Body footprint per silhouette as `[half width across, half length along the heading]`. */
|
|
706
|
+
const VEHICLE_HALF_EXTENTS = {
|
|
707
|
+
car: [0.95, 2.25],
|
|
708
|
+
van: [1.05, 2.65],
|
|
709
|
+
truck: [1.2, 3.5],
|
|
710
|
+
};
|
|
711
|
+
/** Paint slots the resolver spreads vehicles across; the renderer owns the actual colors. */
|
|
712
|
+
const VEHICLE_PAINT_COUNT = 8;
|
|
582
713
|
function axisValue(value) {
|
|
583
714
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
584
715
|
}
|
|
@@ -818,7 +949,8 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
|
|
|
818
949
|
if (prevClass !== null && stickRoll < 0.45 + rules.clusterStrength * 0.4 && mix.some((entry) => entry.item === prevClass && entry.weight > 0)) {
|
|
819
950
|
cls = prevClass;
|
|
820
951
|
}
|
|
821
|
-
const
|
|
952
|
+
const stretchCommercial = rules.blockDensity > 0.75 && (cls === "tower" || cls === "slab" || cls === "shop" || cls === "rowhouse");
|
|
953
|
+
const placement = rollClassPlacement(cls, lotRng, rules.lotScale, rules.floorsMin, rules.floorsMax, rules.buildingRoadSetback, rules.buildingSpacing, stretchCommercial ? 1 + rules.blockDensity * 0.35 : 1);
|
|
822
954
|
const frontW = placement.width + placement.gap;
|
|
823
955
|
if (cursor + frontW > walker.total - 0.5)
|
|
824
956
|
break;
|
|
@@ -904,18 +1036,21 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
|
|
|
904
1036
|
prevClass = null;
|
|
905
1037
|
continue;
|
|
906
1038
|
}
|
|
907
|
-
const
|
|
908
|
-
const
|
|
1039
|
+
const commercial = cls === "tower" || cls === "slab" || cls === "shop" || cls === "rowhouse";
|
|
1040
|
+
const dense = rules.blockDensity > 0.75 && commercial;
|
|
1041
|
+
const sideGap = Math.max(dense ? (cls === "shop" || cls === "rowhouse" ? 0.08 : 0.15) : 0.3, (placement.gap / 2) * (dense ? 0.45 : 1));
|
|
1042
|
+
const frontSetback = Math.max(0.05, placement.setback * (dense ? 0.35 : 1));
|
|
1043
|
+
const buildable = buildablePolygon(poly, A, B, frontSetback, sideGap, 1.2, usedDepth);
|
|
909
1044
|
parcel.buildable = buildable;
|
|
910
1045
|
let placedLot = false;
|
|
911
1046
|
if (buildable.length >= 3) {
|
|
912
1047
|
const outDir = [-midS.normal[0], -midS.normal[1]];
|
|
913
1048
|
const rotationY = Math.atan2(outDir[0], outDir[1]);
|
|
914
|
-
const maxW = Math.max(3, Math.min(placement.width, frontW - placement.gap));
|
|
915
|
-
const availDepth = usedDepth -
|
|
916
|
-
const maxD = Math.max(3, Math.min(placement.depth, availDepth));
|
|
917
|
-
const cx0 = midS.p[0] + midS.normal[0] * (
|
|
918
|
-
const cz0 = midS.p[1] + midS.normal[1] * (
|
|
1049
|
+
const maxW = Math.max(3, Math.min(dense ? Math.max(placement.width, frontW * 0.9) : placement.width, frontW - Math.max(0.15, placement.gap * (dense ? 0.35 : 1))));
|
|
1050
|
+
const availDepth = usedDepth - frontSetback - 1.2;
|
|
1051
|
+
const maxD = Math.max(3, Math.min(dense ? Math.max(placement.depth, availDepth * 0.85) : placement.depth, availDepth));
|
|
1052
|
+
const cx0 = midS.p[0] + midS.normal[0] * (frontSetback + maxD / 2);
|
|
1053
|
+
const cz0 = midS.p[1] + midS.normal[1] * (frontSetback + maxD / 2);
|
|
919
1054
|
const fit = fitRectInPolygon(buildable, cx0, cz0, maxW, maxD, rotationY, 0.45);
|
|
920
1055
|
if (fit !== null && fit.w >= 3 && fit.d >= 2.6) {
|
|
921
1056
|
const slopeRoll = rng();
|
|
@@ -1022,8 +1157,8 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
|
|
|
1022
1157
|
// elsewhere) when it is big enough to read as a place rather than back yards.
|
|
1023
1158
|
if (maxDepthUsed > 0) {
|
|
1024
1159
|
const core = insetRingUniform(land, maxDepthUsed + 0.5);
|
|
1025
|
-
if (core.length >= 3 && polygonArea(core) >
|
|
1026
|
-
const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.
|
|
1160
|
+
if (core.length >= 3 && polygonArea(core) > 120) {
|
|
1161
|
+
const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.55 ? "courtyard" : "green") : "meadow";
|
|
1027
1162
|
result.parks.push(parkFromRing(`court:${bi}`, type, core, parkJitter));
|
|
1028
1163
|
}
|
|
1029
1164
|
}
|
|
@@ -1071,6 +1206,89 @@ function buildFieldRows(parks, placed, streams) {
|
|
|
1071
1206
|
park.rows = rows;
|
|
1072
1207
|
}
|
|
1073
1208
|
}
|
|
1209
|
+
/** Ids of the through-streets carrying a lap's graph edges, in lap order and without repeats. */
|
|
1210
|
+
function lapStreetIds(network, edges, streetCount) {
|
|
1211
|
+
const pairKey = (a, b) => (a < b ? `${a}:${b}` : `${b}:${a}`);
|
|
1212
|
+
const owner = new Map();
|
|
1213
|
+
for (let index = 0; index < Math.min(network.streets.length, streetCount); index += 1) {
|
|
1214
|
+
const nodes = network.streets[index].nodes;
|
|
1215
|
+
for (let i = 0; i + 1 < nodes.length; i += 1)
|
|
1216
|
+
owner.set(pairKey(nodes[i], nodes[i + 1]), index);
|
|
1217
|
+
}
|
|
1218
|
+
const byId = new Map(network.edges.map((edge) => [edge.id, edge]));
|
|
1219
|
+
const ids = [];
|
|
1220
|
+
const seen = new Set();
|
|
1221
|
+
for (const id of edges) {
|
|
1222
|
+
const edge = byId.get(id);
|
|
1223
|
+
if (edge === undefined)
|
|
1224
|
+
continue;
|
|
1225
|
+
const index = owner.get(pairKey(edge.a, edge.b));
|
|
1226
|
+
if (index === undefined || seen.has(index))
|
|
1227
|
+
continue;
|
|
1228
|
+
seen.add(index);
|
|
1229
|
+
ids.push(`street:${index}`);
|
|
1230
|
+
}
|
|
1231
|
+
return ids;
|
|
1232
|
+
}
|
|
1233
|
+
/** Rotate and translate a volume-local circuit route into the world frame the rest of the plan uses. */
|
|
1234
|
+
function raceToWorld(route, streets, seals, toWorld, rotationY) {
|
|
1235
|
+
// `toWorld` rotates a local direction BY `rotationY`, so a heading turns with it rather than against.
|
|
1236
|
+
const heading = (local) => local + rotationY;
|
|
1237
|
+
// A checkpoint is an AABB with no yaw of its own; grow it to the bounds of the rotated gate.
|
|
1238
|
+
const gateX = Math.abs(Math.cos(rotationY));
|
|
1239
|
+
const gateZ = Math.abs(Math.sin(rotationY));
|
|
1240
|
+
const [sx, sz] = toWorld(route.start.x, route.start.z);
|
|
1241
|
+
return {
|
|
1242
|
+
centerline: route.centerline.map(([x, z]) => toWorld(x, z)),
|
|
1243
|
+
widths: [...route.widths],
|
|
1244
|
+
...(route.heights === undefined ? {} : { heights: [...route.heights] }),
|
|
1245
|
+
length: route.length,
|
|
1246
|
+
streets,
|
|
1247
|
+
seals: seals
|
|
1248
|
+
? route.seals.slice(0, MAX_RACE_SEALS).map((seal) => {
|
|
1249
|
+
const [x, z] = toWorld(seal.x, seal.z);
|
|
1250
|
+
return { x, z, heading: heading(seal.heading), width: seal.width, kind: seal.kind };
|
|
1251
|
+
})
|
|
1252
|
+
: [],
|
|
1253
|
+
corners: route.corners.map((corner) => ({ ...corner })),
|
|
1254
|
+
kerbs: circuitKerbs(route)
|
|
1255
|
+
.slice(0, MAX_RACE_KERBS)
|
|
1256
|
+
.map((kerb) => ({
|
|
1257
|
+
points: kerb.points.map(([x, z]) => toWorld(x, z)),
|
|
1258
|
+
width: kerb.width,
|
|
1259
|
+
side: kerb.side,
|
|
1260
|
+
stripe: kerb.stripe,
|
|
1261
|
+
corner: kerb.corner,
|
|
1262
|
+
})),
|
|
1263
|
+
checkpoints: route.checkpoints.map((gate) => {
|
|
1264
|
+
const [x, z] = toWorld(gate.center[0], gate.center[2]);
|
|
1265
|
+
return {
|
|
1266
|
+
id: gate.id,
|
|
1267
|
+
center: [x, gate.center[1], z],
|
|
1268
|
+
half: [
|
|
1269
|
+
gate.half[0] * gateX + gate.half[2] * gateZ,
|
|
1270
|
+
gate.half[1],
|
|
1271
|
+
gate.half[0] * gateZ + gate.half[2] * gateX,
|
|
1272
|
+
],
|
|
1273
|
+
};
|
|
1274
|
+
}),
|
|
1275
|
+
laps: route.laps,
|
|
1276
|
+
start: {
|
|
1277
|
+
x: sx,
|
|
1278
|
+
z: sz,
|
|
1279
|
+
heading: heading(route.start.heading),
|
|
1280
|
+
line: [toWorld(route.start.line[0][0], route.start.line[0][1]), toWorld(route.start.line[1][0], route.start.line[1][1])],
|
|
1281
|
+
distance: route.start.distance,
|
|
1282
|
+
width: route.start.width,
|
|
1283
|
+
},
|
|
1284
|
+
grid: route.grid.map((slot) => {
|
|
1285
|
+
const [x, z] = toWorld(slot.x, slot.z);
|
|
1286
|
+
return { x, z, heading: heading(slot.heading) };
|
|
1287
|
+
}),
|
|
1288
|
+
sectors: [...route.sectors],
|
|
1289
|
+
tightestRadius: route.tightestRadius,
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1074
1292
|
/**
|
|
1075
1293
|
* Synthesize the deterministic city plan for one `city` volume: streets → bridges → parks → zoned
|
|
1076
1294
|
* frontage lots with massing pieces → furniture, all in the volume's local frame and then
|
|
@@ -1109,6 +1327,8 @@ export function resolveCityObject(object, context) {
|
|
|
1109
1327
|
hedges: [],
|
|
1110
1328
|
driveways: [],
|
|
1111
1329
|
parkingLots: [],
|
|
1330
|
+
vehicles: [],
|
|
1331
|
+
signals: [],
|
|
1112
1332
|
};
|
|
1113
1333
|
if (hx < rules.blockSize * 0.75 || hz < rules.blockSize * 0.75)
|
|
1114
1334
|
return empty;
|
|
@@ -1132,7 +1352,7 @@ export function resolveCityObject(object, context) {
|
|
|
1132
1352
|
return Math.hypot(dx, dz) / (2 * d);
|
|
1133
1353
|
};
|
|
1134
1354
|
// Child `cityzone` volumes override the band (and optionally the class mix) for lots inside them.
|
|
1135
|
-
const overrides = (context?.zoneOverrides ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
|
|
1355
|
+
const overrides = (context?.zoneOverrides ?? context?.objects ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
|
|
1136
1356
|
const overrideAt = overrides.length === 0
|
|
1137
1357
|
? null
|
|
1138
1358
|
: (x, z) => {
|
|
@@ -1163,10 +1383,12 @@ export function resolveCityObject(object, context) {
|
|
|
1163
1383
|
// net and a closed race circuit are the SAME generator at opposite slider extremes.
|
|
1164
1384
|
const netRules = {
|
|
1165
1385
|
seed: rules.seed.length > 0 ? rules.seed : object.id,
|
|
1386
|
+
outline: rules.outline,
|
|
1166
1387
|
gridness: rules.gridness,
|
|
1167
1388
|
loopiness: rules.loopiness,
|
|
1168
1389
|
connectivity: rules.connectivity,
|
|
1169
1390
|
branching: rules.branching,
|
|
1391
|
+
residentialBranches: rules.residentialBranches,
|
|
1170
1392
|
deadEnds: rules.deadEnds,
|
|
1171
1393
|
segmentLength: rules.blockSize,
|
|
1172
1394
|
aspect: rules.blockAspect,
|
|
@@ -1176,6 +1398,10 @@ export function resolveCityObject(object, context) {
|
|
|
1176
1398
|
maxTurnAngle: rules.maxTurnAngle,
|
|
1177
1399
|
width: rules.streetWidth,
|
|
1178
1400
|
boulevards: rules.boulevards,
|
|
1401
|
+
sidewalkWidth: rules.sidewalkWidth,
|
|
1402
|
+
elevation: rules.elevation,
|
|
1403
|
+
maxGrade: rules.maxGrade,
|
|
1404
|
+
compactness: rules.compactness,
|
|
1179
1405
|
};
|
|
1180
1406
|
const network = generateStreets(netRules, hx, hz, {
|
|
1181
1407
|
heightAt: heightAt ?? undefined,
|
|
@@ -1193,6 +1419,7 @@ export function resolveCityObject(object, context) {
|
|
|
1193
1419
|
level: street.level,
|
|
1194
1420
|
surface,
|
|
1195
1421
|
sidewalk: rules.sidewalks && surface === "asphalt",
|
|
1422
|
+
...(street.heights === undefined ? {} : { heights: [...street.heights] }),
|
|
1196
1423
|
...(street.bulb === undefined ? {} : { bulb: [street.bulb[0], street.bulb[1]] }),
|
|
1197
1424
|
...(street.features.length === 0 ? {} : { features: street.features }),
|
|
1198
1425
|
};
|
|
@@ -1212,6 +1439,19 @@ export function resolveCityObject(object, context) {
|
|
|
1212
1439
|
width: t.width,
|
|
1213
1440
|
bankHeight: t.bankHeight,
|
|
1214
1441
|
}));
|
|
1442
|
+
// The race is a SECTION OF THIS CITY blocked off: the same streets the district already grew,
|
|
1443
|
+
// with a closed lap lifted out of them. A district with no drivable cycle simply has no race.
|
|
1444
|
+
const route = rules.race
|
|
1445
|
+
? extractCircuitRoute(network, {
|
|
1446
|
+
seed: netRules.seed,
|
|
1447
|
+
lapLength: rules.raceLapLength,
|
|
1448
|
+
arterialBias: rules.raceArterial,
|
|
1449
|
+
minCornerRadius: rules.raceCornerRadius,
|
|
1450
|
+
checkpoints: rules.raceCheckpoints,
|
|
1451
|
+
laps: rules.raceLaps,
|
|
1452
|
+
gridSlots: rules.raceGridSlots,
|
|
1453
|
+
})
|
|
1454
|
+
: null;
|
|
1215
1455
|
// Fabric (block/parcel/building subdivision) is optional: a bare road or race track leaves it off,
|
|
1216
1456
|
// so a circuit never carries forced-on plots — parcels and buildings appear only when you want it.
|
|
1217
1457
|
const emptyFabric = { blocks: [], parcels: [], parks: [], lots: [], placed: new LotIndex(), hedges: [], driveways: [] };
|
|
@@ -1372,6 +1612,99 @@ export function resolveCityObject(object, context) {
|
|
|
1372
1612
|
parking.push({ center: [x, z], size: [lot.size[0] * 1.05, depth], rotationY: lot.rotationY });
|
|
1373
1613
|
}
|
|
1374
1614
|
}
|
|
1615
|
+
// Parked vehicles: curbside anchors come from the shared street helper, pad stalls from the
|
|
1616
|
+
// pads laid above. Every candidate clears other streets, footprints, and the cars already parked.
|
|
1617
|
+
const vehicles = [];
|
|
1618
|
+
if (rules.parkedCars > 0) {
|
|
1619
|
+
const carRng = streams("vehicles");
|
|
1620
|
+
const carIndex = new LotIndex();
|
|
1621
|
+
const park = (x, z, heading, mix, stall, street) => {
|
|
1622
|
+
const kind = mix < 0.68 ? "car" : mix < 0.9 ? "van" : "truck";
|
|
1623
|
+
const half = VEHICLE_HALF_EXTENTS[kind];
|
|
1624
|
+
const rect = { x, z, hw: half[0], hd: half[1], angle: heading };
|
|
1625
|
+
if (!insideBounds(x, z))
|
|
1626
|
+
return;
|
|
1627
|
+
if (index.clearance(x, z, stall === "curb" ? 0.6 : 0.5, street) < 0)
|
|
1628
|
+
return;
|
|
1629
|
+
if (lotResult.placed.overlapsAny(rect, 0.4))
|
|
1630
|
+
return;
|
|
1631
|
+
if (carIndex.overlapsAny(rect, 0.35))
|
|
1632
|
+
return;
|
|
1633
|
+
carIndex.add(rect);
|
|
1634
|
+
vehicles.push({ x, z, rotationY: heading, kind, paint: Math.floor(mix * 9973) % VEHICLE_PAINT_COUNT, stall });
|
|
1635
|
+
};
|
|
1636
|
+
const spacing = Math.max(7, 26 - 19 * rules.parkedCars);
|
|
1637
|
+
for (let si = 0; si < local.length && vehicles.length < MAX_VEHICLES; si += 1) {
|
|
1638
|
+
const street = local[si];
|
|
1639
|
+
// Gravel shoulders and narrow lanes have no curb lane: `parkingSpots` would seat the body
|
|
1640
|
+
// over the centreline of anything under ~6.5 m.
|
|
1641
|
+
if (street.surface === "gravel" || street.width < 6.5)
|
|
1642
|
+
continue;
|
|
1643
|
+
for (const spot of parkingSpots(asDescriptor(street), { spacing })) {
|
|
1644
|
+
if (vehicles.length >= MAX_VEHICLES)
|
|
1645
|
+
break;
|
|
1646
|
+
const mix = carRng();
|
|
1647
|
+
if (carRng() > rules.parkedCars)
|
|
1648
|
+
continue;
|
|
1649
|
+
const [x, z] = spot.position;
|
|
1650
|
+
if (nearIntersection(x, z, 4))
|
|
1651
|
+
continue;
|
|
1652
|
+
park(x, z, spot.heading, mix, "curb", si);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
for (const pad of parking) {
|
|
1656
|
+
if (vehicles.length >= MAX_VEHICLES)
|
|
1657
|
+
break;
|
|
1658
|
+
const ca = Math.cos(pad.rotationY);
|
|
1659
|
+
const sa = Math.sin(pad.rotationY);
|
|
1660
|
+
const rows = pad.size[1] >= 11 ? 2 : 1;
|
|
1661
|
+
const stalls = Math.max(1, Math.floor(pad.size[0] / 2.9));
|
|
1662
|
+
for (let row = 0; row < rows; row += 1) {
|
|
1663
|
+
const lz = rows === 1 ? 0 : (row === 0 ? -1 : 1) * pad.size[1] * 0.25;
|
|
1664
|
+
for (let s = 0; s < stalls; s += 1) {
|
|
1665
|
+
if (vehicles.length >= MAX_VEHICLES)
|
|
1666
|
+
break;
|
|
1667
|
+
const mix = carRng();
|
|
1668
|
+
if (carRng() > rules.parkedCars * 0.92)
|
|
1669
|
+
continue;
|
|
1670
|
+
const lx = ((s + 0.5) / stalls - 0.5) * pad.size[0];
|
|
1671
|
+
// Facing rows nose in on the aisle between them.
|
|
1672
|
+
park(pad.center[0] + lx * ca + lz * sa, pad.center[1] - lx * sa + lz * ca, pad.rotationY + row * Math.PI, mix, "lot", -1);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
// Junction control, one device per approach arm: arterial crossings get a signal on a mast arm,
|
|
1678
|
+
// everything above lane level gets a sign post instead.
|
|
1679
|
+
const signals = [];
|
|
1680
|
+
if (rules.signals > 0) {
|
|
1681
|
+
const signalRng = streams("signals");
|
|
1682
|
+
for (const cross of intersections) {
|
|
1683
|
+
if (signals.length >= MAX_SIGNALS)
|
|
1684
|
+
break;
|
|
1685
|
+
if (cross.arms.length < 3 || cross.level === "lane")
|
|
1686
|
+
continue;
|
|
1687
|
+
const arterial = LEVEL_RANK[cross.level] >= LEVEL_RANK.avenue;
|
|
1688
|
+
if (signalRng() > rules.signals * (arterial ? 1 : 0.75))
|
|
1689
|
+
continue;
|
|
1690
|
+
for (const arm of cross.arms) {
|
|
1691
|
+
if (signals.length >= MAX_SIGNALS)
|
|
1692
|
+
break;
|
|
1693
|
+
const dir = [Math.sin(arm.angle), Math.cos(arm.angle)];
|
|
1694
|
+
// Right-hand traffic: the pole stands on the near-right corner of the approach.
|
|
1695
|
+
const perp = [-dir[1], dir[0]];
|
|
1696
|
+
const out = cross.radius + 1.8;
|
|
1697
|
+
const side = arm.width / 2 + 1;
|
|
1698
|
+
const x = cross.x + dir[0] * out + perp[0] * side;
|
|
1699
|
+
const z = cross.z + dir[1] * out + perp[1] * side;
|
|
1700
|
+
if (!insideBounds(x, z))
|
|
1701
|
+
continue;
|
|
1702
|
+
if (index.clearance(x, z, 0.3) < 0)
|
|
1703
|
+
continue;
|
|
1704
|
+
signals.push({ x, z, heading: arm.angle, kind: arterial ? "signal" : "sign", reach: arm.width / 2 + 0.6 });
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1375
1708
|
return {
|
|
1376
1709
|
center: [center.x, center.y, center.z],
|
|
1377
1710
|
size: [hx * 2, hz * 2],
|
|
@@ -1384,6 +1717,7 @@ export function resolveCityObject(object, context) {
|
|
|
1384
1717
|
level: street.level,
|
|
1385
1718
|
surface: street.surface,
|
|
1386
1719
|
sidewalk: street.sidewalk,
|
|
1720
|
+
...(street.heights === undefined ? {} : { heights: [...street.heights] }),
|
|
1387
1721
|
...(street.bulb === undefined ? {} : { bulb: toWorld(street.bulb[0], street.bulb[1]) }),
|
|
1388
1722
|
...(street.features === undefined ? {} : { features: street.features.map((f) => ({ ...f })) }),
|
|
1389
1723
|
})),
|
|
@@ -1481,6 +1815,17 @@ export function resolveCityObject(object, context) {
|
|
|
1481
1815
|
const [x, z] = toWorld(pad.center[0], pad.center[1]);
|
|
1482
1816
|
return { center: [x, z], size: pad.size, rotationY: pad.rotationY - rotationY };
|
|
1483
1817
|
}),
|
|
1818
|
+
vehicles: vehicles.map((vehicle) => {
|
|
1819
|
+
const [x, z] = toWorld(vehicle.x, vehicle.z);
|
|
1820
|
+
return { x, z, rotationY: vehicle.rotationY + rotationY, kind: vehicle.kind, paint: vehicle.paint, stall: vehicle.stall };
|
|
1821
|
+
}),
|
|
1822
|
+
signals: signals.map((signal) => {
|
|
1823
|
+
const [x, z] = toWorld(signal.x, signal.z);
|
|
1824
|
+
return { x, z, heading: signal.heading + rotationY, kind: signal.kind, reach: signal.reach };
|
|
1825
|
+
}),
|
|
1826
|
+
...(route === null
|
|
1827
|
+
? {}
|
|
1828
|
+
: { race: raceToWorld(route, lapStreetIds(network, route.edges, local.length), rules.raceSeals, toWorld, rotationY) }),
|
|
1484
1829
|
};
|
|
1485
1830
|
}
|
|
1486
1831
|
/** Registers the `city` + `cityzone` scene kinds (schema + resolver). Called by {@link registerBuiltinSceneKinds}. @internal */
|
|
@@ -1494,6 +1839,7 @@ export function registerCityKind() {
|
|
|
1494
1839
|
schema: CITY_SCHEMA,
|
|
1495
1840
|
coverage: { spec: "city", densityKey: "roadsideOccupancy" },
|
|
1496
1841
|
resolve: (object, _params, context) => resolveCityObject(object, context),
|
|
1842
|
+
solids: (resolved, _object, params, context) => resolved === null || params["solid"] === false ? [] : citySolids(resolved, context.sampleHeight),
|
|
1497
1843
|
note: (object) => {
|
|
1498
1844
|
const resolved = resolveCityObject(object);
|
|
1499
1845
|
if (resolved === null)
|
|
@@ -1519,6 +1865,11 @@ export function registerCityKind() {
|
|
|
1519
1865
|
const feats = [
|
|
1520
1866
|
resolved.bridges.length > 0 ? `${resolved.bridges.length} bridges` : "",
|
|
1521
1867
|
resolved.tunnels.length > 0 ? `${resolved.tunnels.length} tunnels` : "",
|
|
1868
|
+
resolved.rules.race
|
|
1869
|
+
? resolved.race === undefined
|
|
1870
|
+
? "no raceable lap in these streets"
|
|
1871
|
+
: `${Math.round(resolved.race.length)} m lap · ${resolved.race.seals.length} seals`
|
|
1872
|
+
: "",
|
|
1522
1873
|
].filter((s) => s.length > 0);
|
|
1523
1874
|
const head = mode === "circuit" ? `circuit · ${resolved.streets.length} streets` : `${resolved.streets.length} streets`;
|
|
1524
1875
|
// Shared clamp-and-warn: when the block/parcel pipeline hits the building budget, surface it in
|