@jgengine/core 0.17.0 → 0.18.1
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 +130 -2
- package/dist/ai/decisionGraph.d.ts +52 -0
- package/dist/ai/decisionGraph.js +82 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/anim/animGraph.d.ts +144 -0
- package/dist/anim/animGraph.js +286 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +26 -0
- package/dist/anim/locomotionGraph.js +33 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +10 -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 +89 -0
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +16 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +16 -0
- package/dist/game/cameraConfig.d.ts +8 -0
- 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 +8 -1
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +97 -3
- package/dist/game/systemRuntime.js +6 -1
- package/dist/input/actionBindings.d.ts +2 -1
- package/dist/input/actionBindings.js +10 -8
- package/dist/input/actionContexts.d.ts +21 -0
- package/dist/input/actionContexts.js +56 -0
- package/dist/input/gamepadModel.d.ts +45 -0
- package/dist/input/gamepadModel.js +69 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +103 -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/playerMovement.d.ts +10 -2
- package/dist/movement/playerMovement.js +218 -4
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +30 -7
- package/dist/movement/solidObstacles.js +27 -4
- 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/navMesh.d.ts +33 -0
- package/dist/nav/navMesh.js +164 -0
- package/dist/physics/flightDynamics.d.ts +8 -0
- package/dist/physics/kinematicVehicle.d.ts +6 -1
- package/dist/physics/physicsBackend.d.ts +191 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +117 -0
- package/dist/physics/physicsWorldBackend.d.ts +44 -0
- package/dist/physics/physicsWorldBackend.js +485 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +96 -0
- 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 +11 -5
- package/dist/render/lookPreset.js +29 -4
- package/dist/render/postProcessing.d.ts +10 -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/commandInput.d.ts +15 -0
- package/dist/runtime/commandInput.js +13 -0
- package/dist/runtime/commandRunner.d.ts +44 -3
- package/dist/runtime/commandRunner.js +41 -3
- package/dist/runtime/gameContext.js +11 -1
- package/dist/runtime/gameContextTypes.d.ts +14 -4
- package/dist/runtime/gameRuntime.d.ts +33 -4
- package/dist/runtime/gameRuntime.js +26 -5
- 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 +8 -0
- package/dist/runtime/inputSnapshot.js +1 -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 +38 -2
- 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 +19 -1
- package/dist/scene/behaviorRuntime.js +55 -4
- package/dist/scene/behaviors.d.ts +8 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/sensor/perception.d.ts +60 -0
- package/dist/sensor/perception.js +142 -0
- package/dist/settings/graphicsProfile.d.ts +19 -0
- package/dist/settings/graphicsProfile.js +36 -0
- 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/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/world/buildings.d.ts +12 -0
- package/dist/world/buildings.js +43 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +132 -0
- package/dist/world/cityKind.js +315 -15
- package/dist/world/features.d.ts +27 -1
- package/dist/world/features.js +7 -2
- 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/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.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +98 -22
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +40 -18
- package/skills/jgengine-gameplay/capabilities.md +12 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -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 },
|
|
@@ -153,6 +177,14 @@ export const CITY_SCHEMA = {
|
|
|
153
177
|
{ type: "bool", key: "sidewalks", label: "sidewalks", group: "layout", default: CITY_DEFAULTS.sidewalks },
|
|
154
178
|
{ type: "range", key: "sidewalkWidth", label: "sidewalk width", group: "layout", min: 1, max: 4, step: 0.1, default: CITY_DEFAULTS.sidewalkWidth, unit: "m" },
|
|
155
179
|
{ type: "action", key: "layoutRandomize", label: "randomize layout", group: "layout", action: "randomize" },
|
|
180
|
+
{ type: "bool", key: "race", label: "street race", group: "race", default: CITY_DEFAULTS.race },
|
|
181
|
+
{ type: "range", key: "raceLapLength", label: "lap length", group: "race", min: 200, max: 6000, step: 50, default: CITY_DEFAULTS.raceLapLength, unit: "m" },
|
|
182
|
+
{ type: "range", key: "raceArterial", label: "arterial bias", group: "race", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.raceArterial },
|
|
183
|
+
{ type: "range", key: "raceCheckpoints", label: "checkpoints", group: "race", min: 2, max: 48, step: 1, default: CITY_DEFAULTS.raceCheckpoints },
|
|
184
|
+
{ type: "range", key: "raceLaps", label: "laps", group: "race", min: 1, max: 20, step: 1, default: CITY_DEFAULTS.raceLaps },
|
|
185
|
+
{ type: "bool", key: "raceSeals", label: "seal side streets", group: "race", default: CITY_DEFAULTS.raceSeals },
|
|
186
|
+
{ type: "range", key: "raceGridSlots", label: "grid slots", group: "race", min: 0, max: 40, step: 1, default: CITY_DEFAULTS.raceGridSlots },
|
|
187
|
+
{ type: "range", key: "raceCornerRadius", label: "corner radius", group: "race", min: 3, max: 40, step: 1, default: CITY_DEFAULTS.raceCornerRadius, unit: "m" },
|
|
156
188
|
{
|
|
157
189
|
type: "select",
|
|
158
190
|
key: "profile",
|
|
@@ -189,6 +221,8 @@ export const CITY_SCHEMA = {
|
|
|
189
221
|
{ type: "range", key: "treeDensity", label: "tree density", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.treeDensity },
|
|
190
222
|
{ type: "weightedList", key: "treeMix", label: "tree mix", group: "greenery", itemLabel: "species", default: CITY_DEFAULTS.treeMix },
|
|
191
223
|
{ type: "range", key: "lightDensity", label: "street lights", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.lightDensity },
|
|
224
|
+
{ type: "range", key: "parkedCars", label: "parked cars", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.parkedCars },
|
|
225
|
+
{ type: "range", key: "signals", label: "traffic signals", group: "greenery", min: 0, max: 1, step: 0.01, default: CITY_DEFAULTS.signals },
|
|
192
226
|
{ type: "bool", key: "hedges", label: "estate hedges", group: "greenery", default: CITY_DEFAULTS.hedges },
|
|
193
227
|
{ type: "bool", key: "driveways", label: "driveways", group: "greenery", default: CITY_DEFAULTS.driveways },
|
|
194
228
|
{ type: "bool", key: "parking", label: "parking lots", group: "greenery", default: CITY_DEFAULTS.parking },
|
|
@@ -208,8 +242,8 @@ export const CITY_SCHEMA = {
|
|
|
208
242
|
openSpace: 0.05,
|
|
209
243
|
roadsideOccupancy: 0.97,
|
|
210
244
|
blockDensity: 0.85,
|
|
211
|
-
buildingRoadSetback: 0.
|
|
212
|
-
buildingSpacing: 0.
|
|
245
|
+
buildingRoadSetback: 0.55,
|
|
246
|
+
buildingSpacing: 0.55,
|
|
213
247
|
clusterStrength: 0.25,
|
|
214
248
|
streetWidth: 9,
|
|
215
249
|
boulevards: 0.5,
|
|
@@ -235,6 +269,8 @@ export const CITY_SCHEMA = {
|
|
|
235
269
|
treeDensity: 0.3,
|
|
236
270
|
treeMix: [{ item: "broadleaf", weight: 1 }],
|
|
237
271
|
lightDensity: 0.8,
|
|
272
|
+
parkedCars: 0.95,
|
|
273
|
+
signals: 1,
|
|
238
274
|
hedges: false,
|
|
239
275
|
driveways: false,
|
|
240
276
|
parking: false,
|
|
@@ -288,6 +324,8 @@ export const CITY_SCHEMA = {
|
|
|
288
324
|
{ item: "cypress", weight: 1 },
|
|
289
325
|
],
|
|
290
326
|
lightDensity: 0.6,
|
|
327
|
+
parkedCars: 0.7,
|
|
328
|
+
signals: 0.8,
|
|
291
329
|
hedges: false,
|
|
292
330
|
driveways: true,
|
|
293
331
|
parking: true,
|
|
@@ -339,6 +377,8 @@ export const CITY_SCHEMA = {
|
|
|
339
377
|
{ item: "broadleaf", weight: 2 },
|
|
340
378
|
],
|
|
341
379
|
lightDensity: 0.35,
|
|
380
|
+
parkedCars: 0.2,
|
|
381
|
+
signals: 0.12,
|
|
342
382
|
hedges: true,
|
|
343
383
|
driveways: true,
|
|
344
384
|
parking: false,
|
|
@@ -392,6 +432,8 @@ export const CITY_SCHEMA = {
|
|
|
392
432
|
{ item: "conifer", weight: 2 },
|
|
393
433
|
],
|
|
394
434
|
lightDensity: 0.1,
|
|
435
|
+
parkedCars: 0.04,
|
|
436
|
+
signals: 0,
|
|
395
437
|
hedges: false,
|
|
396
438
|
driveways: true,
|
|
397
439
|
parking: false,
|
|
@@ -413,6 +455,9 @@ export const CITY_SCHEMA = {
|
|
|
413
455
|
minCurveRadius: 34,
|
|
414
456
|
minTurnAngle: 0,
|
|
415
457
|
maxTurnAngle: 95,
|
|
458
|
+
compactness: 0.5,
|
|
459
|
+
elevation: 0.3,
|
|
460
|
+
maxGrade: 0.06,
|
|
416
461
|
blockSize: 95,
|
|
417
462
|
blockAspect: 1,
|
|
418
463
|
openSpace: 0,
|
|
@@ -426,6 +471,8 @@ export const CITY_SCHEMA = {
|
|
|
426
471
|
sidewalks: false,
|
|
427
472
|
treeDensity: 0.2,
|
|
428
473
|
lightDensity: 0.25,
|
|
474
|
+
parkedCars: 0,
|
|
475
|
+
signals: 0,
|
|
429
476
|
hedges: false,
|
|
430
477
|
driveways: false,
|
|
431
478
|
parking: false,
|
|
@@ -446,6 +493,9 @@ export const CITY_SCHEMA = {
|
|
|
446
493
|
minCurveRadius: 12,
|
|
447
494
|
minTurnAngle: 0,
|
|
448
495
|
maxTurnAngle: 150,
|
|
496
|
+
outline: 0.5,
|
|
497
|
+
elevation: 0.8,
|
|
498
|
+
maxGrade: 0.14,
|
|
449
499
|
blockSize: 72,
|
|
450
500
|
blockAspect: 1,
|
|
451
501
|
openSpace: 0,
|
|
@@ -459,6 +509,8 @@ export const CITY_SCHEMA = {
|
|
|
459
509
|
sidewalks: false,
|
|
460
510
|
treeDensity: 0.65,
|
|
461
511
|
lightDensity: 0.05,
|
|
512
|
+
parkedCars: 0,
|
|
513
|
+
signals: 0,
|
|
462
514
|
hedges: false,
|
|
463
515
|
driveways: false,
|
|
464
516
|
parking: false,
|
|
@@ -490,6 +542,8 @@ export const CITY_SCHEMA = {
|
|
|
490
542
|
floorsMax: 34,
|
|
491
543
|
treeDensity: 0.55,
|
|
492
544
|
lightDensity: 0.6,
|
|
545
|
+
parkedCars: 0.85,
|
|
546
|
+
signals: 0.9,
|
|
493
547
|
style: "generic",
|
|
494
548
|
},
|
|
495
549
|
},
|
|
@@ -527,6 +581,11 @@ export function readCityRules(meta) {
|
|
|
527
581
|
minCurveRadius: params["minCurveRadius"],
|
|
528
582
|
minTurnAngle: params["minTurnAngle"],
|
|
529
583
|
maxTurnAngle: params["maxTurnAngle"],
|
|
584
|
+
outline: params["outline"],
|
|
585
|
+
residentialBranches: params["residentialBranches"],
|
|
586
|
+
elevation: params["elevation"],
|
|
587
|
+
maxGrade: params["maxGrade"],
|
|
588
|
+
compactness: params["compactness"],
|
|
530
589
|
fabric: params["fabric"],
|
|
531
590
|
tunnels: params["tunnels"],
|
|
532
591
|
blockSize: params["blockSize"],
|
|
@@ -554,6 +613,8 @@ export function readCityRules(meta) {
|
|
|
554
613
|
treeDensity: params["treeDensity"],
|
|
555
614
|
treeMix: params["treeMix"],
|
|
556
615
|
lightDensity: params["lightDensity"],
|
|
616
|
+
parkedCars: params["parkedCars"],
|
|
617
|
+
signals: params["signals"],
|
|
557
618
|
hedges: params["hedges"],
|
|
558
619
|
driveways: params["driveways"],
|
|
559
620
|
parking: params["parking"],
|
|
@@ -564,6 +625,14 @@ export function readCityRules(meta) {
|
|
|
564
625
|
bridgeStyle: params["bridgeStyle"],
|
|
565
626
|
sidewalks: params["sidewalks"],
|
|
566
627
|
sidewalkWidth: params["sidewalkWidth"],
|
|
628
|
+
race: params["race"],
|
|
629
|
+
raceLapLength: params["raceLapLength"],
|
|
630
|
+
raceArterial: params["raceArterial"],
|
|
631
|
+
raceCheckpoints: params["raceCheckpoints"],
|
|
632
|
+
raceLaps: params["raceLaps"],
|
|
633
|
+
raceSeals: params["raceSeals"],
|
|
634
|
+
raceGridSlots: params["raceGridSlots"],
|
|
635
|
+
raceCornerRadius: params["raceCornerRadius"],
|
|
567
636
|
style: params["style"],
|
|
568
637
|
buildingKit: params["buildingKit"] ?? CITY_DEFAULTS.buildingKit,
|
|
569
638
|
seed: params["seed"],
|
|
@@ -578,7 +647,19 @@ const MAX_TREES = 3200;
|
|
|
578
647
|
const MAX_LIGHTS = 1200;
|
|
579
648
|
const MAX_FIELD_ROWS = 2400;
|
|
580
649
|
const MAX_PARKING = 380;
|
|
650
|
+
const MAX_VEHICLES = 900;
|
|
651
|
+
const MAX_SIGNALS = 640;
|
|
652
|
+
const MAX_RACE_SEALS = 400;
|
|
653
|
+
const MAX_RACE_KERBS = 320;
|
|
581
654
|
const TAU = Math.PI * 2;
|
|
655
|
+
/** Body footprint per silhouette as `[half width across, half length along the heading]`. */
|
|
656
|
+
const VEHICLE_HALF_EXTENTS = {
|
|
657
|
+
car: [0.95, 2.25],
|
|
658
|
+
van: [1.05, 2.65],
|
|
659
|
+
truck: [1.2, 3.5],
|
|
660
|
+
};
|
|
661
|
+
/** Paint slots the resolver spreads vehicles across; the renderer owns the actual colors. */
|
|
662
|
+
const VEHICLE_PAINT_COUNT = 8;
|
|
582
663
|
function axisValue(value) {
|
|
583
664
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
584
665
|
}
|
|
@@ -818,7 +899,8 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
|
|
|
818
899
|
if (prevClass !== null && stickRoll < 0.45 + rules.clusterStrength * 0.4 && mix.some((entry) => entry.item === prevClass && entry.weight > 0)) {
|
|
819
900
|
cls = prevClass;
|
|
820
901
|
}
|
|
821
|
-
const
|
|
902
|
+
const stretchCommercial = rules.blockDensity > 0.75 && (cls === "tower" || cls === "slab" || cls === "shop" || cls === "rowhouse");
|
|
903
|
+
const placement = rollClassPlacement(cls, lotRng, rules.lotScale, rules.floorsMin, rules.floorsMax, rules.buildingRoadSetback, rules.buildingSpacing, stretchCommercial ? 1 + rules.blockDensity * 0.35 : 1);
|
|
822
904
|
const frontW = placement.width + placement.gap;
|
|
823
905
|
if (cursor + frontW > walker.total - 0.5)
|
|
824
906
|
break;
|
|
@@ -904,18 +986,21 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
|
|
|
904
986
|
prevClass = null;
|
|
905
987
|
continue;
|
|
906
988
|
}
|
|
907
|
-
const
|
|
908
|
-
const
|
|
989
|
+
const commercial = cls === "tower" || cls === "slab" || cls === "shop" || cls === "rowhouse";
|
|
990
|
+
const dense = rules.blockDensity > 0.75 && commercial;
|
|
991
|
+
const sideGap = Math.max(dense ? (cls === "shop" || cls === "rowhouse" ? 0.08 : 0.15) : 0.3, (placement.gap / 2) * (dense ? 0.45 : 1));
|
|
992
|
+
const frontSetback = Math.max(0.05, placement.setback * (dense ? 0.35 : 1));
|
|
993
|
+
const buildable = buildablePolygon(poly, A, B, frontSetback, sideGap, 1.2, usedDepth);
|
|
909
994
|
parcel.buildable = buildable;
|
|
910
995
|
let placedLot = false;
|
|
911
996
|
if (buildable.length >= 3) {
|
|
912
997
|
const outDir = [-midS.normal[0], -midS.normal[1]];
|
|
913
998
|
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] * (
|
|
999
|
+
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))));
|
|
1000
|
+
const availDepth = usedDepth - frontSetback - 1.2;
|
|
1001
|
+
const maxD = Math.max(3, Math.min(dense ? Math.max(placement.depth, availDepth * 0.85) : placement.depth, availDepth));
|
|
1002
|
+
const cx0 = midS.p[0] + midS.normal[0] * (frontSetback + maxD / 2);
|
|
1003
|
+
const cz0 = midS.p[1] + midS.normal[1] * (frontSetback + maxD / 2);
|
|
919
1004
|
const fit = fitRectInPolygon(buildable, cx0, cz0, maxW, maxD, rotationY, 0.45);
|
|
920
1005
|
if (fit !== null && fit.w >= 3 && fit.d >= 2.6) {
|
|
921
1006
|
const slopeRoll = rng();
|
|
@@ -1022,8 +1107,8 @@ function buildFabric(rules, streams, streets, index, hx, hz, slopeAt, heightAt,
|
|
|
1022
1107
|
// elsewhere) when it is big enough to read as a place rather than back yards.
|
|
1023
1108
|
if (maxDepthUsed > 0) {
|
|
1024
1109
|
const core = insetRingUniform(land, maxDepthUsed + 0.5);
|
|
1025
|
-
if (core.length >= 3 && polygonArea(core) >
|
|
1026
|
-
const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.
|
|
1110
|
+
if (core.length >= 3 && polygonArea(core) > 120) {
|
|
1111
|
+
const type = band === "core" ? "courtyard" : band === "mid" ? (parkJitter < 0.55 ? "courtyard" : "green") : "meadow";
|
|
1027
1112
|
result.parks.push(parkFromRing(`court:${bi}`, type, core, parkJitter));
|
|
1028
1113
|
}
|
|
1029
1114
|
}
|
|
@@ -1071,6 +1156,89 @@ function buildFieldRows(parks, placed, streams) {
|
|
|
1071
1156
|
park.rows = rows;
|
|
1072
1157
|
}
|
|
1073
1158
|
}
|
|
1159
|
+
/** Ids of the through-streets carrying a lap's graph edges, in lap order and without repeats. */
|
|
1160
|
+
function lapStreetIds(network, edges, streetCount) {
|
|
1161
|
+
const pairKey = (a, b) => (a < b ? `${a}:${b}` : `${b}:${a}`);
|
|
1162
|
+
const owner = new Map();
|
|
1163
|
+
for (let index = 0; index < Math.min(network.streets.length, streetCount); index += 1) {
|
|
1164
|
+
const nodes = network.streets[index].nodes;
|
|
1165
|
+
for (let i = 0; i + 1 < nodes.length; i += 1)
|
|
1166
|
+
owner.set(pairKey(nodes[i], nodes[i + 1]), index);
|
|
1167
|
+
}
|
|
1168
|
+
const byId = new Map(network.edges.map((edge) => [edge.id, edge]));
|
|
1169
|
+
const ids = [];
|
|
1170
|
+
const seen = new Set();
|
|
1171
|
+
for (const id of edges) {
|
|
1172
|
+
const edge = byId.get(id);
|
|
1173
|
+
if (edge === undefined)
|
|
1174
|
+
continue;
|
|
1175
|
+
const index = owner.get(pairKey(edge.a, edge.b));
|
|
1176
|
+
if (index === undefined || seen.has(index))
|
|
1177
|
+
continue;
|
|
1178
|
+
seen.add(index);
|
|
1179
|
+
ids.push(`street:${index}`);
|
|
1180
|
+
}
|
|
1181
|
+
return ids;
|
|
1182
|
+
}
|
|
1183
|
+
/** Rotate and translate a volume-local circuit route into the world frame the rest of the plan uses. */
|
|
1184
|
+
function raceToWorld(route, streets, seals, toWorld, rotationY) {
|
|
1185
|
+
// `toWorld` rotates a local direction BY `rotationY`, so a heading turns with it rather than against.
|
|
1186
|
+
const heading = (local) => local + rotationY;
|
|
1187
|
+
// A checkpoint is an AABB with no yaw of its own; grow it to the bounds of the rotated gate.
|
|
1188
|
+
const gateX = Math.abs(Math.cos(rotationY));
|
|
1189
|
+
const gateZ = Math.abs(Math.sin(rotationY));
|
|
1190
|
+
const [sx, sz] = toWorld(route.start.x, route.start.z);
|
|
1191
|
+
return {
|
|
1192
|
+
centerline: route.centerline.map(([x, z]) => toWorld(x, z)),
|
|
1193
|
+
widths: [...route.widths],
|
|
1194
|
+
...(route.heights === undefined ? {} : { heights: [...route.heights] }),
|
|
1195
|
+
length: route.length,
|
|
1196
|
+
streets,
|
|
1197
|
+
seals: seals
|
|
1198
|
+
? route.seals.slice(0, MAX_RACE_SEALS).map((seal) => {
|
|
1199
|
+
const [x, z] = toWorld(seal.x, seal.z);
|
|
1200
|
+
return { x, z, heading: heading(seal.heading), width: seal.width, kind: seal.kind };
|
|
1201
|
+
})
|
|
1202
|
+
: [],
|
|
1203
|
+
corners: route.corners.map((corner) => ({ ...corner })),
|
|
1204
|
+
kerbs: circuitKerbs(route)
|
|
1205
|
+
.slice(0, MAX_RACE_KERBS)
|
|
1206
|
+
.map((kerb) => ({
|
|
1207
|
+
points: kerb.points.map(([x, z]) => toWorld(x, z)),
|
|
1208
|
+
width: kerb.width,
|
|
1209
|
+
side: kerb.side,
|
|
1210
|
+
stripe: kerb.stripe,
|
|
1211
|
+
corner: kerb.corner,
|
|
1212
|
+
})),
|
|
1213
|
+
checkpoints: route.checkpoints.map((gate) => {
|
|
1214
|
+
const [x, z] = toWorld(gate.center[0], gate.center[2]);
|
|
1215
|
+
return {
|
|
1216
|
+
id: gate.id,
|
|
1217
|
+
center: [x, gate.center[1], z],
|
|
1218
|
+
half: [
|
|
1219
|
+
gate.half[0] * gateX + gate.half[2] * gateZ,
|
|
1220
|
+
gate.half[1],
|
|
1221
|
+
gate.half[0] * gateZ + gate.half[2] * gateX,
|
|
1222
|
+
],
|
|
1223
|
+
};
|
|
1224
|
+
}),
|
|
1225
|
+
laps: route.laps,
|
|
1226
|
+
start: {
|
|
1227
|
+
x: sx,
|
|
1228
|
+
z: sz,
|
|
1229
|
+
heading: heading(route.start.heading),
|
|
1230
|
+
line: [toWorld(route.start.line[0][0], route.start.line[0][1]), toWorld(route.start.line[1][0], route.start.line[1][1])],
|
|
1231
|
+
distance: route.start.distance,
|
|
1232
|
+
width: route.start.width,
|
|
1233
|
+
},
|
|
1234
|
+
grid: route.grid.map((slot) => {
|
|
1235
|
+
const [x, z] = toWorld(slot.x, slot.z);
|
|
1236
|
+
return { x, z, heading: heading(slot.heading) };
|
|
1237
|
+
}),
|
|
1238
|
+
sectors: [...route.sectors],
|
|
1239
|
+
tightestRadius: route.tightestRadius,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1074
1242
|
/**
|
|
1075
1243
|
* Synthesize the deterministic city plan for one `city` volume: streets → bridges → parks → zoned
|
|
1076
1244
|
* frontage lots with massing pieces → furniture, all in the volume's local frame and then
|
|
@@ -1109,6 +1277,8 @@ export function resolveCityObject(object, context) {
|
|
|
1109
1277
|
hedges: [],
|
|
1110
1278
|
driveways: [],
|
|
1111
1279
|
parkingLots: [],
|
|
1280
|
+
vehicles: [],
|
|
1281
|
+
signals: [],
|
|
1112
1282
|
};
|
|
1113
1283
|
if (hx < rules.blockSize * 0.75 || hz < rules.blockSize * 0.75)
|
|
1114
1284
|
return empty;
|
|
@@ -1163,10 +1333,12 @@ export function resolveCityObject(object, context) {
|
|
|
1163
1333
|
// net and a closed race circuit are the SAME generator at opposite slider extremes.
|
|
1164
1334
|
const netRules = {
|
|
1165
1335
|
seed: rules.seed.length > 0 ? rules.seed : object.id,
|
|
1336
|
+
outline: rules.outline,
|
|
1166
1337
|
gridness: rules.gridness,
|
|
1167
1338
|
loopiness: rules.loopiness,
|
|
1168
1339
|
connectivity: rules.connectivity,
|
|
1169
1340
|
branching: rules.branching,
|
|
1341
|
+
residentialBranches: rules.residentialBranches,
|
|
1170
1342
|
deadEnds: rules.deadEnds,
|
|
1171
1343
|
segmentLength: rules.blockSize,
|
|
1172
1344
|
aspect: rules.blockAspect,
|
|
@@ -1176,6 +1348,10 @@ export function resolveCityObject(object, context) {
|
|
|
1176
1348
|
maxTurnAngle: rules.maxTurnAngle,
|
|
1177
1349
|
width: rules.streetWidth,
|
|
1178
1350
|
boulevards: rules.boulevards,
|
|
1351
|
+
sidewalkWidth: rules.sidewalkWidth,
|
|
1352
|
+
elevation: rules.elevation,
|
|
1353
|
+
maxGrade: rules.maxGrade,
|
|
1354
|
+
compactness: rules.compactness,
|
|
1179
1355
|
};
|
|
1180
1356
|
const network = generateStreets(netRules, hx, hz, {
|
|
1181
1357
|
heightAt: heightAt ?? undefined,
|
|
@@ -1193,6 +1369,7 @@ export function resolveCityObject(object, context) {
|
|
|
1193
1369
|
level: street.level,
|
|
1194
1370
|
surface,
|
|
1195
1371
|
sidewalk: rules.sidewalks && surface === "asphalt",
|
|
1372
|
+
...(street.heights === undefined ? {} : { heights: [...street.heights] }),
|
|
1196
1373
|
...(street.bulb === undefined ? {} : { bulb: [street.bulb[0], street.bulb[1]] }),
|
|
1197
1374
|
...(street.features.length === 0 ? {} : { features: street.features }),
|
|
1198
1375
|
};
|
|
@@ -1212,6 +1389,19 @@ export function resolveCityObject(object, context) {
|
|
|
1212
1389
|
width: t.width,
|
|
1213
1390
|
bankHeight: t.bankHeight,
|
|
1214
1391
|
}));
|
|
1392
|
+
// The race is a SECTION OF THIS CITY blocked off: the same streets the district already grew,
|
|
1393
|
+
// with a closed lap lifted out of them. A district with no drivable cycle simply has no race.
|
|
1394
|
+
const route = rules.race
|
|
1395
|
+
? extractCircuitRoute(network, {
|
|
1396
|
+
seed: netRules.seed,
|
|
1397
|
+
lapLength: rules.raceLapLength,
|
|
1398
|
+
arterialBias: rules.raceArterial,
|
|
1399
|
+
minCornerRadius: rules.raceCornerRadius,
|
|
1400
|
+
checkpoints: rules.raceCheckpoints,
|
|
1401
|
+
laps: rules.raceLaps,
|
|
1402
|
+
gridSlots: rules.raceGridSlots,
|
|
1403
|
+
})
|
|
1404
|
+
: null;
|
|
1215
1405
|
// Fabric (block/parcel/building subdivision) is optional: a bare road or race track leaves it off,
|
|
1216
1406
|
// so a circuit never carries forced-on plots — parcels and buildings appear only when you want it.
|
|
1217
1407
|
const emptyFabric = { blocks: [], parcels: [], parks: [], lots: [], placed: new LotIndex(), hedges: [], driveways: [] };
|
|
@@ -1372,6 +1562,99 @@ export function resolveCityObject(object, context) {
|
|
|
1372
1562
|
parking.push({ center: [x, z], size: [lot.size[0] * 1.05, depth], rotationY: lot.rotationY });
|
|
1373
1563
|
}
|
|
1374
1564
|
}
|
|
1565
|
+
// Parked vehicles: curbside anchors come from the shared street helper, pad stalls from the
|
|
1566
|
+
// pads laid above. Every candidate clears other streets, footprints, and the cars already parked.
|
|
1567
|
+
const vehicles = [];
|
|
1568
|
+
if (rules.parkedCars > 0) {
|
|
1569
|
+
const carRng = streams("vehicles");
|
|
1570
|
+
const carIndex = new LotIndex();
|
|
1571
|
+
const park = (x, z, heading, mix, stall, street) => {
|
|
1572
|
+
const kind = mix < 0.68 ? "car" : mix < 0.9 ? "van" : "truck";
|
|
1573
|
+
const half = VEHICLE_HALF_EXTENTS[kind];
|
|
1574
|
+
const rect = { x, z, hw: half[0], hd: half[1], angle: heading };
|
|
1575
|
+
if (!insideBounds(x, z))
|
|
1576
|
+
return;
|
|
1577
|
+
if (index.clearance(x, z, stall === "curb" ? 0.6 : 0.5, street) < 0)
|
|
1578
|
+
return;
|
|
1579
|
+
if (lotResult.placed.overlapsAny(rect, 0.4))
|
|
1580
|
+
return;
|
|
1581
|
+
if (carIndex.overlapsAny(rect, 0.35))
|
|
1582
|
+
return;
|
|
1583
|
+
carIndex.add(rect);
|
|
1584
|
+
vehicles.push({ x, z, rotationY: heading, kind, paint: Math.floor(mix * 9973) % VEHICLE_PAINT_COUNT, stall });
|
|
1585
|
+
};
|
|
1586
|
+
const spacing = Math.max(7, 26 - 19 * rules.parkedCars);
|
|
1587
|
+
for (let si = 0; si < local.length && vehicles.length < MAX_VEHICLES; si += 1) {
|
|
1588
|
+
const street = local[si];
|
|
1589
|
+
// Gravel shoulders and narrow lanes have no curb lane: `parkingSpots` would seat the body
|
|
1590
|
+
// over the centreline of anything under ~6.5 m.
|
|
1591
|
+
if (street.surface === "gravel" || street.width < 6.5)
|
|
1592
|
+
continue;
|
|
1593
|
+
for (const spot of parkingSpots(asDescriptor(street), { spacing })) {
|
|
1594
|
+
if (vehicles.length >= MAX_VEHICLES)
|
|
1595
|
+
break;
|
|
1596
|
+
const mix = carRng();
|
|
1597
|
+
if (carRng() > rules.parkedCars)
|
|
1598
|
+
continue;
|
|
1599
|
+
const [x, z] = spot.position;
|
|
1600
|
+
if (nearIntersection(x, z, 4))
|
|
1601
|
+
continue;
|
|
1602
|
+
park(x, z, spot.heading, mix, "curb", si);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
for (const pad of parking) {
|
|
1606
|
+
if (vehicles.length >= MAX_VEHICLES)
|
|
1607
|
+
break;
|
|
1608
|
+
const ca = Math.cos(pad.rotationY);
|
|
1609
|
+
const sa = Math.sin(pad.rotationY);
|
|
1610
|
+
const rows = pad.size[1] >= 11 ? 2 : 1;
|
|
1611
|
+
const stalls = Math.max(1, Math.floor(pad.size[0] / 2.9));
|
|
1612
|
+
for (let row = 0; row < rows; row += 1) {
|
|
1613
|
+
const lz = rows === 1 ? 0 : (row === 0 ? -1 : 1) * pad.size[1] * 0.25;
|
|
1614
|
+
for (let s = 0; s < stalls; s += 1) {
|
|
1615
|
+
if (vehicles.length >= MAX_VEHICLES)
|
|
1616
|
+
break;
|
|
1617
|
+
const mix = carRng();
|
|
1618
|
+
if (carRng() > rules.parkedCars * 0.92)
|
|
1619
|
+
continue;
|
|
1620
|
+
const lx = ((s + 0.5) / stalls - 0.5) * pad.size[0];
|
|
1621
|
+
// Facing rows nose in on the aisle between them.
|
|
1622
|
+
park(pad.center[0] + lx * ca + lz * sa, pad.center[1] - lx * sa + lz * ca, pad.rotationY + row * Math.PI, mix, "lot", -1);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Junction control, one device per approach arm: arterial crossings get a signal on a mast arm,
|
|
1628
|
+
// everything above lane level gets a sign post instead.
|
|
1629
|
+
const signals = [];
|
|
1630
|
+
if (rules.signals > 0) {
|
|
1631
|
+
const signalRng = streams("signals");
|
|
1632
|
+
for (const cross of intersections) {
|
|
1633
|
+
if (signals.length >= MAX_SIGNALS)
|
|
1634
|
+
break;
|
|
1635
|
+
if (cross.arms.length < 3 || cross.level === "lane")
|
|
1636
|
+
continue;
|
|
1637
|
+
const arterial = LEVEL_RANK[cross.level] >= LEVEL_RANK.avenue;
|
|
1638
|
+
if (signalRng() > rules.signals * (arterial ? 1 : 0.75))
|
|
1639
|
+
continue;
|
|
1640
|
+
for (const arm of cross.arms) {
|
|
1641
|
+
if (signals.length >= MAX_SIGNALS)
|
|
1642
|
+
break;
|
|
1643
|
+
const dir = [Math.sin(arm.angle), Math.cos(arm.angle)];
|
|
1644
|
+
// Right-hand traffic: the pole stands on the near-right corner of the approach.
|
|
1645
|
+
const perp = [-dir[1], dir[0]];
|
|
1646
|
+
const out = cross.radius + 1.8;
|
|
1647
|
+
const side = arm.width / 2 + 1;
|
|
1648
|
+
const x = cross.x + dir[0] * out + perp[0] * side;
|
|
1649
|
+
const z = cross.z + dir[1] * out + perp[1] * side;
|
|
1650
|
+
if (!insideBounds(x, z))
|
|
1651
|
+
continue;
|
|
1652
|
+
if (index.clearance(x, z, 0.3) < 0)
|
|
1653
|
+
continue;
|
|
1654
|
+
signals.push({ x, z, heading: arm.angle, kind: arterial ? "signal" : "sign", reach: arm.width / 2 + 0.6 });
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1375
1658
|
return {
|
|
1376
1659
|
center: [center.x, center.y, center.z],
|
|
1377
1660
|
size: [hx * 2, hz * 2],
|
|
@@ -1384,6 +1667,7 @@ export function resolveCityObject(object, context) {
|
|
|
1384
1667
|
level: street.level,
|
|
1385
1668
|
surface: street.surface,
|
|
1386
1669
|
sidewalk: street.sidewalk,
|
|
1670
|
+
...(street.heights === undefined ? {} : { heights: [...street.heights] }),
|
|
1387
1671
|
...(street.bulb === undefined ? {} : { bulb: toWorld(street.bulb[0], street.bulb[1]) }),
|
|
1388
1672
|
...(street.features === undefined ? {} : { features: street.features.map((f) => ({ ...f })) }),
|
|
1389
1673
|
})),
|
|
@@ -1481,6 +1765,17 @@ export function resolveCityObject(object, context) {
|
|
|
1481
1765
|
const [x, z] = toWorld(pad.center[0], pad.center[1]);
|
|
1482
1766
|
return { center: [x, z], size: pad.size, rotationY: pad.rotationY - rotationY };
|
|
1483
1767
|
}),
|
|
1768
|
+
vehicles: vehicles.map((vehicle) => {
|
|
1769
|
+
const [x, z] = toWorld(vehicle.x, vehicle.z);
|
|
1770
|
+
return { x, z, rotationY: vehicle.rotationY + rotationY, kind: vehicle.kind, paint: vehicle.paint, stall: vehicle.stall };
|
|
1771
|
+
}),
|
|
1772
|
+
signals: signals.map((signal) => {
|
|
1773
|
+
const [x, z] = toWorld(signal.x, signal.z);
|
|
1774
|
+
return { x, z, heading: signal.heading + rotationY, kind: signal.kind, reach: signal.reach };
|
|
1775
|
+
}),
|
|
1776
|
+
...(route === null
|
|
1777
|
+
? {}
|
|
1778
|
+
: { race: raceToWorld(route, lapStreetIds(network, route.edges, local.length), rules.raceSeals, toWorld, rotationY) }),
|
|
1484
1779
|
};
|
|
1485
1780
|
}
|
|
1486
1781
|
/** Registers the `city` + `cityzone` scene kinds (schema + resolver). Called by {@link registerBuiltinSceneKinds}. @internal */
|
|
@@ -1519,6 +1814,11 @@ export function registerCityKind() {
|
|
|
1519
1814
|
const feats = [
|
|
1520
1815
|
resolved.bridges.length > 0 ? `${resolved.bridges.length} bridges` : "",
|
|
1521
1816
|
resolved.tunnels.length > 0 ? `${resolved.tunnels.length} tunnels` : "",
|
|
1817
|
+
resolved.rules.race
|
|
1818
|
+
? resolved.race === undefined
|
|
1819
|
+
? "no raceable lap in these streets"
|
|
1820
|
+
: `${Math.round(resolved.race.length)} m lap · ${resolved.race.seals.length} seals`
|
|
1821
|
+
: "",
|
|
1522
1822
|
].filter((s) => s.length > 0);
|
|
1523
1823
|
const head = mode === "circuit" ? `circuit · ${resolved.streets.length} streets` : `${resolved.streets.length} streets`;
|
|
1524
1824
|
// Shared clamp-and-warn: when the block/parcel pipeline hits the building budget, surface it in
|
package/dist/world/features.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AvoidZone } from "./geometry.js";
|
|
|
4
4
|
import type { TerrainPathProfile } from "./pathTerrain.js";
|
|
5
5
|
import type { PlaceWorldFeature } from "./place.js";
|
|
6
6
|
import type { TerraformSnapshot } from "./terraform.js";
|
|
7
|
+
import type { SpriteAtlas } from "../assets/spriteAtlas.js";
|
|
7
8
|
import type { VolumetricCloudsConfig } from "./volumetricClouds.js";
|
|
8
9
|
export interface WorldBounds {
|
|
9
10
|
w: number;
|
|
@@ -48,6 +49,16 @@ export interface TerrainDetailConfig {
|
|
|
48
49
|
roughness?: number;
|
|
49
50
|
/** Overall detail intensity 0..1. Default 1. */
|
|
50
51
|
strength?: number;
|
|
52
|
+
/**
|
|
53
|
+
* RGB multipliers for the two macro colour sweeps painted over the base ground. `dry` tints the
|
|
54
|
+
* sun-bleached patches, `wet` the cooler pockets between them. The defaults ([1.14, 1.05, 0.76]
|
|
55
|
+
* and [0.78, 1.0, 0.86]) suit temperate meadows; the green-leaning `wet` default paints mossy
|
|
56
|
+
* pockets onto any biome that keeps it, so arid/ashen worlds should supply warm or neutral tints.
|
|
57
|
+
*/
|
|
58
|
+
sweeps?: {
|
|
59
|
+
dry?: readonly [number, number, number];
|
|
60
|
+
wet?: readonly [number, number, number];
|
|
61
|
+
};
|
|
51
62
|
/** Real PBR texture maps blended over the procedural detail — the seam for `@jgengine/assets` `buildMaterialCatalog` output. Omit to keep the procedural-only look. */
|
|
52
63
|
material?: TerrainDetailMaterialConfig;
|
|
53
64
|
}
|
|
@@ -259,6 +270,14 @@ export interface GrassEnvironmentConfig {
|
|
|
259
270
|
windStrength?: number;
|
|
260
271
|
colors?: readonly string[];
|
|
261
272
|
seed?: string;
|
|
273
|
+
/** Blade curl range (arc amount, min..max). Shell default [0.12, 0.42] — raise for wind-combed scrub, lower for stiff turf. */
|
|
274
|
+
bladeBend?: readonly [number, number];
|
|
275
|
+
/** Tuft spread radius in meters — how far a clump's blades splay from its root. Shell default 0.16; wider reads as bushy clumps rather than lone blades. */
|
|
276
|
+
tuftRadius?: number;
|
|
277
|
+
/** Per-tuft brightness jitter 0..1 (shell default 0.28) — higher breaks up the field so it never reads as one repeated blade. */
|
|
278
|
+
colorVariation?: number;
|
|
279
|
+
/** 0..1 blend of blade normals toward straight-up (shell default 0.6). Low values light each blade plane individually and go dark when side-lit; high values light the field like a soft carpet. */
|
|
280
|
+
normalLift?: number;
|
|
262
281
|
}
|
|
263
282
|
export interface OceanEnvironmentConfig {
|
|
264
283
|
bounds?: WorldBounds;
|
|
@@ -393,7 +412,7 @@ export type SnowEnvironmentDescriptor = {
|
|
|
393
412
|
} & Required<Pick<SnowEnvironmentConfig, "area" | "density" | "speed" | "flakeSize" | "drift" | "wind" | "color" | "opacity">>;
|
|
394
413
|
export type GrassEnvironmentDescriptor = {
|
|
395
414
|
kind: "grass";
|
|
396
|
-
} & Required<Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors">> & Pick<GrassEnvironmentConfig, "seed">;
|
|
415
|
+
} & Required<Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors">> & Pick<GrassEnvironmentConfig, "seed" | "bladeBend" | "tuftRadius" | "colorVariation" | "normalLift">;
|
|
397
416
|
export type OceanEnvironmentDescriptor = {
|
|
398
417
|
kind: "ocean";
|
|
399
418
|
} & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position" | "levelAt">;
|
|
@@ -519,6 +538,13 @@ export interface PlotsWorldConfig extends WorldGridConfig {
|
|
|
519
538
|
}
|
|
520
539
|
export interface TilemapWorldConfig extends WorldGridConfig {
|
|
521
540
|
map: string;
|
|
541
|
+
/** Atlas-backed tile rendering; cell glyphs map to atlas frame ids. */
|
|
542
|
+
tileSet?: {
|
|
543
|
+
atlas: SpriteAtlas;
|
|
544
|
+
tiles: Record<string, string>;
|
|
545
|
+
};
|
|
546
|
+
/** Render ordering key consumed by the shell tile renderer. */
|
|
547
|
+
sortingLayer?: string;
|
|
522
548
|
}
|
|
523
549
|
/**
|
|
524
550
|
* A declared world shape passed to `defineGame`. The preferred model is the place feature from
|
package/dist/world/features.js
CHANGED
|
@@ -183,7 +183,7 @@ export function snow(config = {}) {
|
|
|
183
183
|
}
|
|
184
184
|
/** Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors. */
|
|
185
185
|
export function grass(config = {}) {
|
|
186
|
-
return
|
|
186
|
+
return {
|
|
187
187
|
kind: "grass",
|
|
188
188
|
area: withAreaPosition(config.area ?? { w: 128, d: 128 }, config.position),
|
|
189
189
|
density: config.density ?? 4,
|
|
@@ -191,7 +191,12 @@ export function grass(config = {}) {
|
|
|
191
191
|
bladeWidth: config.bladeWidth ?? 0.035,
|
|
192
192
|
windStrength: config.windStrength ?? 0.35,
|
|
193
193
|
colors: config.colors ?? ["#3f7d2d", "#66a83f"],
|
|
194
|
-
|
|
194
|
+
...(config.seed === undefined ? {} : { seed: config.seed }),
|
|
195
|
+
...(config.bladeBend === undefined ? {} : { bladeBend: config.bladeBend }),
|
|
196
|
+
...(config.tuftRadius === undefined ? {} : { tuftRadius: config.tuftRadius }),
|
|
197
|
+
...(config.colorVariation === undefined ? {} : { colorVariation: config.colorVariation }),
|
|
198
|
+
...(config.normalLift === undefined ? {} : { normalLift: config.normalLift }),
|
|
199
|
+
};
|
|
195
200
|
}
|
|
196
201
|
/** Declares an ocean water body for `environment()` — bounds, level, and wave tuning. */
|
|
197
202
|
export function ocean(config = {}) {
|