@jgengine/core 0.18.1 → 0.21.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 +504 -7
- package/dist/ai/decisionGraph.d.ts +50 -4
- package/dist/ai/decisionGraph.js +159 -23
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +43 -1
- package/dist/anim/animGraph.js +326 -10
- 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/locomotionGraph.d.ts +18 -1
- package/dist/anim/locomotionGraph.js +42 -3
- package/dist/audio/audioFalloff.d.ts +6 -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/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +20 -0
- package/dist/combat/abilityKit.js +41 -4
- package/dist/combat/death.d.ts +11 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- 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/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +119 -18
- package/dist/editor/environment.js +8 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/types.d.ts +15 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +54 -2
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +11 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +134 -35
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- 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/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- 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 +36 -2
- package/dist/input/gamepadModel.js +69 -30
- 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/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +444 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- 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 +63 -1
- package/dist/movement/movementModel.js +136 -26
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +48 -2
- package/dist/movement/playerMovement.js +493 -87
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +11 -1
- package/dist/movement/solidObstacles.js +48 -3
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +10 -10
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- 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 +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +40 -5
- package/dist/physics/kinematicVehicle.js +17 -2
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +52 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +375 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +54 -3
- package/dist/runtime/cameraDirector.js +187 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +12 -4
- package/dist/runtime/context/combat.js +179 -11
- package/dist/runtime/context/deathLoot.d.ts +11 -8
- package/dist/runtime/context/deathLoot.js +17 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +86 -13
- package/dist/runtime/gameContextTypes.d.ts +33 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +22 -4
- package/dist/runtime/inputSnapshot.js +41 -3
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +38 -7
- package/dist/scene/behaviorRuntime.js +293 -30
- package/dist/scene/behaviors.d.ts +35 -2
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +41 -3
- package/dist/scene/vehicleSeat.js +94 -3
- 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 +11 -1
- package/dist/sensor/perception.js +57 -10
- 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 +2 -0
- package/dist/settings/graphicsProfile.js +12 -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/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +10 -3
- package/dist/stats/eventMeter.js +6 -3
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -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/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +11 -4
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +32 -5
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +62 -0
- package/dist/world/footprintGrid.js +217 -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/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +9 -1
- package/dist/world/terraform.js +53 -18
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +95 -0
- package/dist/world/worldSolids.js +351 -0
- package/dist/world.d.ts +6 -6
- package/dist/world.js +5 -5
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-combat/SKILL.md +13 -2
- package/skills/jgengine-combat/api.md +150 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +583 -463
- package/skills/jgengine-gameplay/capabilities.md +92 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +123 -0
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createEnvironmentField } from "./envField.js";
|
|
2
|
+
import { windField } from "./wind.js";
|
|
3
|
+
const MAX_ENTRIES = 256;
|
|
4
|
+
const MAX_MAGNITUDE = 1_000_000;
|
|
5
|
+
const MAX_DOMAIN = 1_000_000_000_000;
|
|
6
|
+
const MODES = ["clear", "rain", "snow", "mixed", "dust"];
|
|
7
|
+
const CLEAR = { mode: "clear", intensity: 0 };
|
|
8
|
+
function identifier(value, label) {
|
|
9
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 128)
|
|
10
|
+
throw new Error(`${label} must be a nonempty string of at most 128 characters`);
|
|
11
|
+
}
|
|
12
|
+
function finite(value, label, min = -Infinity, max = Infinity) {
|
|
13
|
+
if (!Number.isFinite(value) || value < min || value > max)
|
|
14
|
+
throw new Error(`${label} must be finite and within ${min}..${max}`);
|
|
15
|
+
}
|
|
16
|
+
function objectKeys(value, allowed, label) {
|
|
17
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
18
|
+
throw new Error(`${label} must be an object`);
|
|
19
|
+
for (const key of Object.keys(value))
|
|
20
|
+
if (!allowed.includes(key))
|
|
21
|
+
throw new Error(`${label}.${key}: unknown field`);
|
|
22
|
+
}
|
|
23
|
+
function conditions(value, profile = false) {
|
|
24
|
+
objectKeys(value, ["mode", "intensity", "temperatureOffset", "lightning", ...(profile ? ["id"] : [])], "weather conditions");
|
|
25
|
+
if (!MODES.includes(value.mode))
|
|
26
|
+
throw new Error(`unknown weather mode: ${value.mode}`);
|
|
27
|
+
finite(value.intensity, "weather intensity", 0);
|
|
28
|
+
if (value.intensity > 1)
|
|
29
|
+
throw new Error("weather intensity must be <= 1");
|
|
30
|
+
if (value.temperatureOffset !== undefined)
|
|
31
|
+
finite(value.temperatureOffset, "weather temperatureOffset", -MAX_MAGNITUDE, MAX_MAGNITUDE);
|
|
32
|
+
if (value.lightning !== undefined) {
|
|
33
|
+
finite(value.lightning, "weather lightning", 0);
|
|
34
|
+
if (value.lightning > 1)
|
|
35
|
+
throw new Error("weather lightning must be <= 1");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function validateWind(value) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return;
|
|
41
|
+
objectKeys(value, ["direction", "speed", "gust", "gustFrequency", "turbulence", "seed"], "wind");
|
|
42
|
+
if (value.direction !== undefined) {
|
|
43
|
+
if (!Array.isArray(value.direction) || value.direction.length !== 2)
|
|
44
|
+
throw new Error("wind direction requires world X,Z");
|
|
45
|
+
for (const axis of value.direction)
|
|
46
|
+
finite(axis, "wind direction", -MAX_MAGNITUDE, MAX_MAGNITUDE);
|
|
47
|
+
}
|
|
48
|
+
for (const key of ["speed", "gust", "gustFrequency", "turbulence"]) {
|
|
49
|
+
if (value[key] !== undefined)
|
|
50
|
+
finite(value[key], `wind.${key}`, 0, key === "gustFrequency" ? 1000 : MAX_MAGNITUDE);
|
|
51
|
+
}
|
|
52
|
+
if (value.seed !== undefined && typeof value.seed !== "string")
|
|
53
|
+
finite(value.seed, "wind seed");
|
|
54
|
+
}
|
|
55
|
+
/** Validate authoring references and work bounds before persisting a weather document.
|
|
56
|
+
* @capability authored-weather-validation Reject invalid weather profiles, schedules, zones and wind before authoring mutations.
|
|
57
|
+
*/
|
|
58
|
+
export function validateAuthoredWeather(config) {
|
|
59
|
+
objectKeys(config, ["ambient", "profiles", "initialProfileId", "schedule", "wind", "zones"], "weather");
|
|
60
|
+
for (const list of [config.profiles, config.schedule, config.zones]) {
|
|
61
|
+
if (list !== undefined && !Array.isArray(list))
|
|
62
|
+
throw new Error("weather profiles, schedule and zones must be arrays");
|
|
63
|
+
if (list !== undefined && list.length > MAX_ENTRIES)
|
|
64
|
+
throw new Error(`weather lists are limited to ${MAX_ENTRIES} entries`);
|
|
65
|
+
}
|
|
66
|
+
if (config.ambient !== undefined)
|
|
67
|
+
conditions(config.ambient);
|
|
68
|
+
validateWind(config.wind);
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
for (const profile of config.profiles ?? []) {
|
|
71
|
+
identifier(profile.id, "weather profile id");
|
|
72
|
+
if (!profile.id || ids.has(profile.id))
|
|
73
|
+
throw new Error(`invalid or duplicate weather profile: ${profile.id}`);
|
|
74
|
+
ids.add(profile.id);
|
|
75
|
+
conditions(profile, true);
|
|
76
|
+
}
|
|
77
|
+
if (config.initialProfileId !== undefined)
|
|
78
|
+
identifier(config.initialProfileId, "initial weather profile id");
|
|
79
|
+
if (config.initialProfileId !== undefined && !ids.has(config.initialProfileId))
|
|
80
|
+
throw new Error(`unknown initial weather profile: ${config.initialProfileId}`);
|
|
81
|
+
let previous = -1;
|
|
82
|
+
for (const entry of config.schedule ?? []) {
|
|
83
|
+
objectKeys(entry, ["atSeconds", "profileId", "transitionSeconds"], "weather schedule");
|
|
84
|
+
identifier(entry.profileId, "scheduled weather profile id");
|
|
85
|
+
finite(entry.atSeconds, "weather schedule time", 0, MAX_DOMAIN);
|
|
86
|
+
if (entry.atSeconds <= previous)
|
|
87
|
+
throw new Error("weather schedule times must be strictly increasing");
|
|
88
|
+
previous = entry.atSeconds;
|
|
89
|
+
if (!ids.has(entry.profileId))
|
|
90
|
+
throw new Error(`unknown weather profile: ${entry.profileId}`);
|
|
91
|
+
if (entry.transitionSeconds !== undefined)
|
|
92
|
+
finite(entry.transitionSeconds, "weather transition duration", 0, MAX_DOMAIN);
|
|
93
|
+
}
|
|
94
|
+
ids.clear();
|
|
95
|
+
for (const zone of config.zones ?? []) {
|
|
96
|
+
objectKeys(zone, ["id", "center", "radius", "falloff", "weather", "wind", "radial"], "weather zone");
|
|
97
|
+
identifier(zone.id, "weather zone id");
|
|
98
|
+
if (zone.radial !== undefined && typeof zone.radial !== "boolean")
|
|
99
|
+
throw new Error("weather zone radial must be boolean");
|
|
100
|
+
if (!zone.id || ids.has(zone.id))
|
|
101
|
+
throw new Error(`invalid or duplicate weather zone: ${zone.id}`);
|
|
102
|
+
ids.add(zone.id);
|
|
103
|
+
if (!Array.isArray(zone.center) || zone.center.length !== 2)
|
|
104
|
+
throw new Error("weather zone center requires world X,Z");
|
|
105
|
+
for (const axis of zone.center)
|
|
106
|
+
finite(axis, "weather zone center", -MAX_DOMAIN, MAX_DOMAIN);
|
|
107
|
+
finite(zone.radius, "weather zone radius", Number.MIN_VALUE, MAX_DOMAIN);
|
|
108
|
+
if (zone.falloff !== undefined)
|
|
109
|
+
finite(zone.falloff, "weather zone falloff", 0, MAX_DOMAIN);
|
|
110
|
+
if (zone.weather !== undefined)
|
|
111
|
+
conditions(zone.weather);
|
|
112
|
+
validateWind(zone.wind);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function amounts(value) {
|
|
116
|
+
return [
|
|
117
|
+
value.mode === "rain" ? value.intensity : value.mode === "mixed" ? value.intensity * 0.5 : 0,
|
|
118
|
+
value.mode === "snow" ? value.intensity : value.mode === "mixed" ? value.intensity * 0.5 : 0,
|
|
119
|
+
value.mode === "dust" ? value.intensity : 0,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
function modeOf(rain, snow, dust) {
|
|
123
|
+
if (rain > 0 && snow > 0)
|
|
124
|
+
return "mixed";
|
|
125
|
+
if (rain > 0)
|
|
126
|
+
return "rain";
|
|
127
|
+
if (snow > 0)
|
|
128
|
+
return "snow";
|
|
129
|
+
return dust > 0 ? "dust" : "clear";
|
|
130
|
+
}
|
|
131
|
+
function blendOf(value) {
|
|
132
|
+
const [rain, snow, dust] = amounts(value);
|
|
133
|
+
return { rain, snow, dust, temperatureOffset: value.temperatureOffset ?? 0, lightning: value.lightning ?? 0 };
|
|
134
|
+
}
|
|
135
|
+
function blend(a, b, weight) {
|
|
136
|
+
const out = { ...a };
|
|
137
|
+
for (const key of ["rain", "snow", "dust", "temperatureOffset", "lightning"])
|
|
138
|
+
out[key] += (b[key] - a[key]) * weight;
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function scheduledWeather(initial, transitions, time) {
|
|
142
|
+
let current = initial;
|
|
143
|
+
for (const entry of transitions) {
|
|
144
|
+
if (time < entry.at)
|
|
145
|
+
break;
|
|
146
|
+
current = blend(entry.from, entry.to, entry.duration === 0 ? 1 : Math.min(1, (time - entry.at) / entry.duration));
|
|
147
|
+
}
|
|
148
|
+
return current;
|
|
149
|
+
}
|
|
150
|
+
function compileWeather(source) {
|
|
151
|
+
validateAuthoredWeather(source);
|
|
152
|
+
const config = JSON.parse(JSON.stringify(source));
|
|
153
|
+
const profiles = new Map((config.profiles ?? []).map((profile) => [profile.id, Object.freeze(blendOf(profile))]));
|
|
154
|
+
const initial = config.initialProfileId === undefined ? Object.freeze(blendOf(config.ambient ?? CLEAR)) : profiles.get(config.initialProfileId);
|
|
155
|
+
const ambientWind = config.wind === undefined ? null : windField(config.wind);
|
|
156
|
+
const zones = (config.zones ?? []).map((zone) => Object.freeze({ zone, field: zone.wind === undefined ? null : windField(zone.wind) }));
|
|
157
|
+
const transitions = [];
|
|
158
|
+
for (const entry of config.schedule ?? []) {
|
|
159
|
+
transitions.push(Object.freeze({ at: entry.atSeconds, duration: entry.transitionSeconds ?? 0, from: Object.freeze(scheduledWeather(initial, transitions, entry.atSeconds)), to: profiles.get(entry.profileId) }));
|
|
160
|
+
}
|
|
161
|
+
return Object.freeze({ initial, profiles, ambientWind, zones: Object.freeze(zones), transitions: Object.freeze(transitions) });
|
|
162
|
+
}
|
|
163
|
+
/** Pure authored weather sampling; the caller's simulation clock owns pause, replay and restore.
|
|
164
|
+
* @capability authored-weather Sample scheduled precipitation and local wind from one authored document and simulation clock.
|
|
165
|
+
*/
|
|
166
|
+
export function createAuthoredWeather(source = {}) {
|
|
167
|
+
const compiled = compileWeather(source);
|
|
168
|
+
return {
|
|
169
|
+
sample(x, z, timeSeconds) {
|
|
170
|
+
finite(x, "weather sample x", -MAX_DOMAIN, MAX_DOMAIN);
|
|
171
|
+
finite(z, "weather sample z", -MAX_DOMAIN, MAX_DOMAIN);
|
|
172
|
+
finite(timeSeconds, "weather sample time", 0, MAX_DOMAIN);
|
|
173
|
+
let value = scheduledWeather(compiled.initial, compiled.transitions, timeSeconds);
|
|
174
|
+
const ambient = compiled.ambientWind?.atPoint(x, z, timeSeconds) ?? [0, 0];
|
|
175
|
+
let wx = ambient[0];
|
|
176
|
+
let wz = ambient[1];
|
|
177
|
+
for (const { zone, field } of compiled.zones) {
|
|
178
|
+
const dx = x - zone.center[0];
|
|
179
|
+
const dz = z - zone.center[1];
|
|
180
|
+
const distance = Math.hypot(dx, dz);
|
|
181
|
+
const falloff = zone.falloff ?? 0;
|
|
182
|
+
if (distance > zone.radius + falloff)
|
|
183
|
+
continue;
|
|
184
|
+
const t = distance <= zone.radius ? 1 : 1 - (distance - zone.radius) / falloff;
|
|
185
|
+
const weight = t * t * (3 - 2 * t);
|
|
186
|
+
if (zone.weather !== undefined)
|
|
187
|
+
value = blend(value, blendOf(zone.weather), weight);
|
|
188
|
+
if (field !== null) {
|
|
189
|
+
const vector = zoneWind(field, zone.radial ?? false, dx, dz, distance, timeSeconds);
|
|
190
|
+
wx += vector[0] * weight;
|
|
191
|
+
wz += vector[1] * weight;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const precipitation = value.rain + value.snow;
|
|
195
|
+
return { ...value, mode: modeOf(value.rain, value.snow, value.dust), intensity: Math.min(1, precipitation + value.dust), timeSeconds, wind: [wx, wz], precipitation };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function zoneWind(field, radial, dx, dz, distance, time) {
|
|
200
|
+
if (!radial)
|
|
201
|
+
return field.atPoint(dx, dz, time);
|
|
202
|
+
if (distance === 0)
|
|
203
|
+
return [0, 0];
|
|
204
|
+
const speed = field.strengthAt(dx, dz, time);
|
|
205
|
+
return [dx / distance * speed, dz / distance * speed];
|
|
206
|
+
}
|
|
207
|
+
function exposureConfig(config) {
|
|
208
|
+
const out = { maxSurfaces: config.maxSurfaces ?? 1024, wettingRate: config.wettingRate ?? 0.2, dryingRate: config.dryingRate ?? 0.05, snowRate: config.snowRate ?? 0.1, meltRate: config.meltRate ?? 0.02 };
|
|
209
|
+
if (!Number.isInteger(out.maxSurfaces) || out.maxSurfaces < 1 || out.maxSurfaces > 65_536)
|
|
210
|
+
throw new Error("weather exposure maxSurfaces must be an integer in 1..65536");
|
|
211
|
+
for (const key of ["wettingRate", "dryingRate", "snowRate", "meltRate"])
|
|
212
|
+
finite(out[key], `weather exposure ${key}`, 0);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
216
|
+
/** Bounded surface accumulation shared by gameplay and material adapters, with injected persistence.
|
|
217
|
+
* @capability weather-surface-accumulation Persist and retune bounded surface wetness, snow, temperature and exposure independently of rendering.
|
|
218
|
+
*/
|
|
219
|
+
export function createWeatherExposure(config = {}, storage) {
|
|
220
|
+
let tuning = exposureConfig(config);
|
|
221
|
+
const values = new Map();
|
|
222
|
+
const snapshot = () => ({ surfaces: Array.from(values, ([id, value]) => ({ id, value: { ...value } })) });
|
|
223
|
+
const persist = () => storage?.write(snapshot());
|
|
224
|
+
function restore(state) {
|
|
225
|
+
if (state.surfaces.length > tuning.maxSurfaces)
|
|
226
|
+
throw new Error("weather exposure snapshot exceeds capacity");
|
|
227
|
+
const next = new Map();
|
|
228
|
+
for (const entry of state.surfaces) {
|
|
229
|
+
if (!entry.id || next.has(entry.id))
|
|
230
|
+
throw new Error("weather exposure snapshot has invalid surface ids");
|
|
231
|
+
for (const key of ["wetness", "snow", "heat", "exposure"])
|
|
232
|
+
finite(entry.value[key], `weather exposure ${key}`);
|
|
233
|
+
for (const key of ["wetness", "snow", "exposure"])
|
|
234
|
+
if (entry.value[key] < 0 || entry.value[key] > 1)
|
|
235
|
+
throw new Error(`weather exposure ${key} must be in 0..1`);
|
|
236
|
+
next.set(entry.id, { ...entry.value });
|
|
237
|
+
}
|
|
238
|
+
values.clear();
|
|
239
|
+
for (const [id, value] of next)
|
|
240
|
+
values.set(id, value);
|
|
241
|
+
persist();
|
|
242
|
+
}
|
|
243
|
+
const saved = storage?.read();
|
|
244
|
+
if (saved !== undefined)
|
|
245
|
+
restore(saved);
|
|
246
|
+
return {
|
|
247
|
+
step(dt, timeSeconds, surfaces, weather) {
|
|
248
|
+
finite(dt, "weather exposure dt", 0);
|
|
249
|
+
finite(timeSeconds, "weather exposure time", 0);
|
|
250
|
+
if (dt === 0)
|
|
251
|
+
return;
|
|
252
|
+
if (surfaces.length > tuning.maxSurfaces)
|
|
253
|
+
throw new Error("weather exposure step exceeds capacity");
|
|
254
|
+
const ids = new Set();
|
|
255
|
+
let added = 0;
|
|
256
|
+
for (const surface of surfaces) {
|
|
257
|
+
if (!surface.id || ids.has(surface.id))
|
|
258
|
+
throw new Error("weather exposure step has invalid surface ids");
|
|
259
|
+
ids.add(surface.id);
|
|
260
|
+
finite(surface.x, "weather exposure x");
|
|
261
|
+
finite(surface.z, "weather exposure z");
|
|
262
|
+
if (surface.exposure !== undefined)
|
|
263
|
+
finite(surface.exposure, "weather exposure exposure", 0);
|
|
264
|
+
if ((surface.exposure ?? 1) > 1)
|
|
265
|
+
throw new Error("weather exposure exposure must be <= 1");
|
|
266
|
+
if (surface.temperature !== undefined)
|
|
267
|
+
finite(surface.temperature, "weather exposure temperature");
|
|
268
|
+
if (!values.has(surface.id))
|
|
269
|
+
added += 1;
|
|
270
|
+
}
|
|
271
|
+
if (values.size + added > tuning.maxSurfaces)
|
|
272
|
+
throw new Error("weather exposure capacity exhausted; forget unused surfaces");
|
|
273
|
+
for (const surface of surfaces) {
|
|
274
|
+
const sample = weather.sample(surface.x, surface.z, timeSeconds);
|
|
275
|
+
const current = values.get(surface.id) ?? { wetness: 0, snow: 0, heat: 0, exposure: 1 };
|
|
276
|
+
const exposure = surface.exposure ?? 1;
|
|
277
|
+
const heat = surface.temperature ?? sample.temperatureOffset ?? 0;
|
|
278
|
+
const rain = sample.rain * exposure;
|
|
279
|
+
const snowfall = sample.snow * exposure * tuning.snowRate * dt;
|
|
280
|
+
const melt = Math.min(current.snow + snowfall, Math.max(0, heat) * tuning.meltRate * dt);
|
|
281
|
+
values.set(surface.id, {
|
|
282
|
+
wetness: clamp01(current.wetness + (rain * tuning.wettingRate - (1 - rain) * tuning.dryingRate) * dt + melt),
|
|
283
|
+
snow: clamp01(current.snow + snowfall - melt),
|
|
284
|
+
heat,
|
|
285
|
+
exposure,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
persist();
|
|
289
|
+
},
|
|
290
|
+
sample(id) { const value = values.get(id); return value === undefined ? null : { ...value }; },
|
|
291
|
+
forget(id) { if (values.delete(id))
|
|
292
|
+
persist(); },
|
|
293
|
+
snapshot,
|
|
294
|
+
restore,
|
|
295
|
+
retune(next) {
|
|
296
|
+
const resolved = exposureConfig(next);
|
|
297
|
+
if (values.size > resolved.maxSurfaces)
|
|
298
|
+
throw new Error("weather exposure retune capacity is below retained surface count");
|
|
299
|
+
tuning = resolved;
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** Environment temperature, sunlight and shelter read the same authored precipitation as rendering.
|
|
304
|
+
* @capability weather-environment-field Connect authored precipitation and temperature changes to spatial environment queries.
|
|
305
|
+
*/
|
|
306
|
+
export function createWeatherEnvironmentField(weather, config = {}) {
|
|
307
|
+
const base = createEnvironmentField({ ...config, rain: (x, z, time) => weather.sample(x, z, time).rain });
|
|
308
|
+
const temperature = (x, z, time, y) => base.temperature(x, z, time, y) + (weather.sample(x, z, time).temperatureOffset ?? 0);
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
temperature,
|
|
312
|
+
sample(x, z, time, y) { return { ...base.sample(x, z, time, y), temperature: temperature(x, z, time, y) }; },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @capability building-generator seeded building as a placeable parametric asset
|
|
8
8
|
*/
|
|
9
9
|
import { registerAssetGenerator, partsBounds } from "../scene/assetGenerator.js";
|
|
10
|
-
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, generateBuilding, resolveBuildingPalette } from "./buildings.js";
|
|
10
|
+
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, buildingSurfaceColor, generateBuilding, resolveBuildingPalette } from "./buildings.js";
|
|
11
11
|
/** The generator id a catalog entry / placed marker references. */
|
|
12
12
|
export const BUILDING_GENERATOR_ID = "building";
|
|
13
13
|
/** The building generator's slider schema — drives the inspector via #809. */
|
|
@@ -47,7 +47,7 @@ export function generateBuildingAsset(params, seed) {
|
|
|
47
47
|
position: part.position,
|
|
48
48
|
size: part.scale,
|
|
49
49
|
rotationY: part.rotationY,
|
|
50
|
-
color: palette[part.kind],
|
|
50
|
+
color: buildingSurfaceColor(palette[part.kind]),
|
|
51
51
|
}));
|
|
52
52
|
return { parts, bounds: partsBounds(parts) };
|
|
53
53
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
|
|
4
4
|
* bindings, the renderer loads them.
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import { type MaterialAsset, type MaterialAssignment } from "../material/materialAsset.js";
|
|
7
|
+
import type { BuildingKitSlot, BuildingPartKind, BuildingSurfaceMaterial, BuildingVariantCounts, Vec3 } from "./buildings.js";
|
|
7
8
|
/**
|
|
8
9
|
* How a kit model's native bounds are mapped onto the slot box `BuildingPartPlacement.scale`
|
|
9
10
|
* describes.
|
|
@@ -32,6 +33,10 @@ export interface BuildingKitPart {
|
|
|
32
33
|
/** Multiplier applied after `fit`. */
|
|
33
34
|
scale?: Vec3;
|
|
34
35
|
tint?: string;
|
|
36
|
+
/** Tiled PBR maps applied over the model's own materials; `color` tints, `repeat` sets the tiling. */
|
|
37
|
+
material?: BuildingSurfaceMaterial;
|
|
38
|
+
/** Editor-authored mesh/slot selectors applied after the broad tint and surface. */
|
|
39
|
+
materialAssignments?: readonly MaterialAssignment[];
|
|
35
40
|
/**
|
|
36
41
|
* Quarter-turn the model when its long horizontal axis disagrees with the slot's, so a panel
|
|
37
42
|
* authored running along Z still tiles a bay that runs along X. Default true — modular kits do not
|
|
@@ -47,6 +52,8 @@ export interface BuildingKitPart {
|
|
|
47
52
|
*/
|
|
48
53
|
export interface BuildingKit {
|
|
49
54
|
id: string;
|
|
55
|
+
/** The caller's existing editor-document material assets; only assigned IDs are loaded. */
|
|
56
|
+
materialAssets?: readonly MaterialAsset[];
|
|
50
57
|
/** Variants per part kind; `BuildingKitSlot.variant` indexes this list, wrapping. */
|
|
51
58
|
parts: Readonly<Partial<Record<BuildingPartKind, readonly BuildingKitPart[]>>>;
|
|
52
59
|
/** Overrides keyed by `BuildingKitSlot.key` (`${facade}.${kind}`); beats `parts` for that facade. */
|
|
@@ -61,6 +68,7 @@ export type BuildingKitPartInput = string | BuildingKitPart;
|
|
|
61
68
|
/** The authoring shape {@link defineBuildingKit} normalizes into a {@link BuildingKit}. */
|
|
62
69
|
export interface BuildingKitInput {
|
|
63
70
|
id?: string;
|
|
71
|
+
materialAssets?: readonly MaterialAsset[];
|
|
64
72
|
parts: Readonly<Partial<Record<BuildingPartKind, readonly BuildingKitPartInput[]>>>;
|
|
65
73
|
slots?: Readonly<Record<string, readonly BuildingKitPartInput[]>>;
|
|
66
74
|
omit?: readonly BuildingPartKind[];
|
|
@@ -83,10 +91,9 @@ export type BuildingKitBinding = {
|
|
|
83
91
|
};
|
|
84
92
|
/**
|
|
85
93
|
* Validates and normalizes a kit description, accepting bare model strings as single-field variants.
|
|
86
|
-
* Throws on empty
|
|
87
|
-
* to untextured boxes.
|
|
94
|
+
* Throws on empty variants or invalid editor material references/selectors before loading models.
|
|
88
95
|
*
|
|
89
|
-
* @capability building-kit bind generated facade part slots to
|
|
96
|
+
* @capability building-kit bind generated facade part slots to caller models and editor-authored named materials
|
|
90
97
|
*/
|
|
91
98
|
export declare function defineBuildingKit(input: BuildingKitInput): BuildingKit;
|
|
92
99
|
/**
|
|
@@ -1,36 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binds the part slots `world/buildings` generates to real models, so a procedural building renders
|
|
3
|
+
* as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
|
|
4
|
+
* bindings, the renderer loads them.
|
|
5
|
+
*/
|
|
6
|
+
import { parseMaterialAssignments, validateMaterialAssignments } from "../material/materialAsset.js";
|
|
1
7
|
const BOX = { type: "box" };
|
|
2
8
|
const OMIT = { type: "omit" };
|
|
3
9
|
const DEFAULT_FIT = "stretch";
|
|
4
|
-
function normalizePart(input, where) {
|
|
10
|
+
function normalizePart(input, where, assets) {
|
|
5
11
|
const part = typeof input === "string" ? { model: input } : input;
|
|
6
12
|
if (typeof part.model !== "string" || part.model.length === 0) {
|
|
7
13
|
throw new Error(`Building kit ${where} has a variant with no model reference.`);
|
|
8
14
|
}
|
|
15
|
+
if (part.materialAssignments !== undefined) {
|
|
16
|
+
const assignments = parseMaterialAssignments(part.materialAssignments);
|
|
17
|
+
const errors = validateMaterialAssignments(assignments, assets, { disallowedTextureRoles: ["height"] }).filter(item => item.severity === "error");
|
|
18
|
+
if (errors.length > 0)
|
|
19
|
+
throw new Error(`Building kit ${where}: ${errors.map(item => `${item.path}: ${item.message}`).join("; ")}`);
|
|
20
|
+
return { ...part, materialAssignments: assignments };
|
|
21
|
+
}
|
|
9
22
|
return part;
|
|
10
23
|
}
|
|
11
|
-
function normalizeList(list, where) {
|
|
24
|
+
function normalizeList(list, where, assets) {
|
|
12
25
|
if (list.length === 0) {
|
|
13
26
|
throw new Error(`Building kit ${where} binds an empty variant list. Bind at least one model, or add the kind to \`omit\` to drop it.`);
|
|
14
27
|
}
|
|
15
|
-
return list.map((entry) => normalizePart(entry, where));
|
|
28
|
+
return list.map((entry) => normalizePart(entry, where, assets));
|
|
16
29
|
}
|
|
17
30
|
/**
|
|
18
31
|
* Validates and normalizes a kit description, accepting bare model strings as single-field variants.
|
|
19
|
-
* Throws on empty
|
|
20
|
-
* to untextured boxes.
|
|
32
|
+
* Throws on empty variants or invalid editor material references/selectors before loading models.
|
|
21
33
|
*
|
|
22
|
-
* @capability building-kit bind generated facade part slots to
|
|
34
|
+
* @capability building-kit bind generated facade part slots to caller models and editor-authored named materials
|
|
23
35
|
*/
|
|
24
36
|
export function defineBuildingKit(input) {
|
|
25
37
|
const parts = {};
|
|
26
38
|
for (const [kind, list] of Object.entries(input.parts)) {
|
|
27
39
|
if (list === undefined)
|
|
28
40
|
continue;
|
|
29
|
-
parts[kind] = normalizeList(list, `"${input.id ?? "kit"}" part "${kind}"
|
|
41
|
+
parts[kind] = normalizeList(list, `"${input.id ?? "kit"}" part "${kind}"`, input.materialAssets ?? []);
|
|
30
42
|
}
|
|
31
43
|
const kit = {
|
|
32
44
|
id: input.id ?? "building-kit",
|
|
33
45
|
parts,
|
|
46
|
+
...(input.materialAssets === undefined ? {} : { materialAssets: input.materialAssets }),
|
|
34
47
|
...(input.omit === undefined ? {} : { omit: input.omit }),
|
|
35
48
|
...(input.fit === undefined ? {} : { fit: input.fit }),
|
|
36
49
|
};
|
|
@@ -38,7 +51,7 @@ export function defineBuildingKit(input) {
|
|
|
38
51
|
return kit;
|
|
39
52
|
const slots = {};
|
|
40
53
|
for (const [key, list] of Object.entries(input.slots)) {
|
|
41
|
-
slots[key] = normalizeList(list, `"${kit.id}" slot "${key}"
|
|
54
|
+
slots[key] = normalizeList(list, `"${kit.id}" slot "${key}"`, input.materialAssets ?? []);
|
|
42
55
|
}
|
|
43
56
|
return { ...kit, slots };
|
|
44
57
|
}
|
|
@@ -103,11 +103,43 @@ export interface BuildingGridConfig {
|
|
|
103
103
|
floorRange?: readonly [number, number];
|
|
104
104
|
}
|
|
105
105
|
export declare const DEFAULT_BUILDING_CONFIG: BuildingConfig;
|
|
106
|
-
|
|
106
|
+
/** URLs of the PBR maps a building surface tiles; `buildMaterialCatalog(...).resolve(id)!.maps` fits directly. */
|
|
107
|
+
export interface BuildingSurfaceMaps {
|
|
108
|
+
color?: string;
|
|
109
|
+
normal?: string;
|
|
110
|
+
roughness?: string;
|
|
111
|
+
ao?: string;
|
|
112
|
+
metalness?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A textured building surface: a base colour plus optional tiled PBR maps. With `maps`, `color`
|
|
116
|
+
* multiplies the albedo (leave it light to show the map as authored); without, it is the flat
|
|
117
|
+
* colour the part has always had.
|
|
118
|
+
*/
|
|
119
|
+
export interface BuildingSurfaceMaterial {
|
|
120
|
+
/** Base colour; the style's colour for that part kind when unset. */
|
|
121
|
+
color?: string;
|
|
122
|
+
maps?: BuildingSurfaceMaps;
|
|
123
|
+
/**
|
|
124
|
+
* Texture tiles per part slot along [x, y]; a number tiles both. Slots within one building share
|
|
125
|
+
* a size, so one repeat per kind is uniform there; it drifts across buildings with a different
|
|
126
|
+
* `bayWidth`. Default 1.
|
|
127
|
+
*/
|
|
128
|
+
repeat?: number | readonly [number, number];
|
|
129
|
+
roughness?: number;
|
|
130
|
+
metalness?: number;
|
|
131
|
+
}
|
|
132
|
+
/** What a building part kind is painted with: a hex colour, or a colour plus tiled PBR maps. */
|
|
133
|
+
export type BuildingSurface = string | BuildingSurfaceMaterial;
|
|
134
|
+
export type BuildingPalette = Record<BuildingPartKind, BuildingSurface>;
|
|
107
135
|
export type BuildingPaletteOverrides = Partial<BuildingPalette>;
|
|
136
|
+
/** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
|
|
137
|
+
export declare function buildingSurfaceColor(surface: BuildingSurface | undefined, fallback?: string): string;
|
|
138
|
+
/** Normalises a surface to its material form so a renderer reads one shape. */
|
|
139
|
+
export declare function resolveBuildingSurface(surface: BuildingSurface): BuildingSurfaceMaterial;
|
|
108
140
|
export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
|
|
109
141
|
export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
|
|
110
|
-
export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle,
|
|
142
|
+
export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>>;
|
|
111
143
|
/**
|
|
112
144
|
* Per-style facade-tone family: the wall colours a district spreads across its buildings so
|
|
113
145
|
* neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
|
package/dist/world/buildings.js
CHANGED
|
@@ -38,6 +38,16 @@ export const DEFAULT_BUILDING_CONFIG = {
|
|
|
38
38
|
probabilities: DEFAULT_PROBABILITIES,
|
|
39
39
|
variants: DEFAULT_VARIANTS,
|
|
40
40
|
};
|
|
41
|
+
/** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
|
|
42
|
+
export function buildingSurfaceColor(surface, fallback = "#808080") {
|
|
43
|
+
if (surface === undefined)
|
|
44
|
+
return fallback;
|
|
45
|
+
return typeof surface === "string" ? surface : (surface.color ?? fallback);
|
|
46
|
+
}
|
|
47
|
+
/** Normalises a surface to its material form so a renderer reads one shape. */
|
|
48
|
+
export function resolveBuildingSurface(surface) {
|
|
49
|
+
return typeof surface === "string" ? { color: surface } : surface;
|
|
50
|
+
}
|
|
41
51
|
export const DEFAULT_BUILDING_STYLE = "generic";
|
|
42
52
|
export const BUILDING_STYLE_PALETTES = {
|
|
43
53
|
generic: {
|
|
@@ -207,7 +217,7 @@ export function resolveBuildingWallTones(style = DEFAULT_BUILDING_STYLE) {
|
|
|
207
217
|
const tones = BUILDING_STYLE_WALL_TONES[style];
|
|
208
218
|
if (tones !== undefined && tones.length > 0)
|
|
209
219
|
return tones;
|
|
210
|
-
return [resolveBuildingPalette(style).wall];
|
|
220
|
+
return [buildingSurfaceColor(resolveBuildingPalette(style).wall)];
|
|
211
221
|
}
|
|
212
222
|
/** @internal */
|
|
213
223
|
export function resolveBuildingPalette(style = DEFAULT_BUILDING_STYLE, overrides) {
|
package/dist/world/cityKind.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ParamSchema, type SceneKindObject, type SceneKindResolveContext, t
|
|
|
2
2
|
import { type BuildingStyle } from "./buildings.js";
|
|
3
3
|
import { type CityLotClass, type CityLotPiece, type CityTreeSpecies, type CityZoneBand, type CityZoneProfile } from "./cityContent.js";
|
|
4
4
|
import { type CircuitCorner, type CircuitGate, type CircuitSealKind } from "./raceCircuit.js";
|
|
5
|
+
import type { WorldSolid } from "./worldSolids.js";
|
|
5
6
|
import type { RoadPoint } from "./roads.js";
|
|
6
7
|
import { type StreetFeatureKind } from "./streetGenerator.js";
|
|
7
8
|
import { type CityBlockKind } from "./cityBlocks.js";
|
|
@@ -432,6 +433,12 @@ export interface ResolvedCity {
|
|
|
432
433
|
/** The instanced street race through these streets, when `race` is on and a lap could be found. */
|
|
433
434
|
race?: ResolvedRace;
|
|
434
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* One solid per wall and roof piece of every lot, grounded the way the city renderer grounds it:
|
|
438
|
+
* grade at the highest lot corner, foundations down to half a metre under the lowest.
|
|
439
|
+
* @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
|
|
440
|
+
*/
|
|
441
|
+
export declare function citySolids(city: ResolvedCity, sampleHeight?: (x: number, z: number) => number): WorldSolid[];
|
|
435
442
|
/** Extended resolve context: sibling `cityzone` volumes that override the band/mix locally. */
|
|
436
443
|
export interface CityResolveContext extends SceneKindResolveContext {
|
|
437
444
|
zoneOverrides?: readonly SceneKindObject[];
|
package/dist/world/cityKind.js
CHANGED
|
@@ -164,6 +164,7 @@ export const CITY_SCHEMA = {
|
|
|
164
164
|
options: [{ value: "auto" }, { value: "gravel" }],
|
|
165
165
|
},
|
|
166
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 },
|
|
167
168
|
{ type: "bool", key: "bridges", label: "bridges over water", group: "layout", default: CITY_DEFAULTS.bridges },
|
|
168
169
|
{ type: "bool", key: "tunnels", label: "tunnels through ridges", group: "layout", default: CITY_DEFAULTS.tunnels },
|
|
169
170
|
{
|
|
@@ -638,6 +639,55 @@ export function readCityRules(meta) {
|
|
|
638
639
|
seed: params["seed"],
|
|
639
640
|
};
|
|
640
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
|
+
}
|
|
641
691
|
/** Bounded-work caps so a huge volume can never generate unbounded content. */
|
|
642
692
|
const MAX_STREETS = 320;
|
|
643
693
|
/** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
|
|
@@ -1302,7 +1352,7 @@ export function resolveCityObject(object, context) {
|
|
|
1302
1352
|
return Math.hypot(dx, dz) / (2 * d);
|
|
1303
1353
|
};
|
|
1304
1354
|
// Child `cityzone` volumes override the band (and optionally the class mix) for lots inside them.
|
|
1305
|
-
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);
|
|
1306
1356
|
const overrideAt = overrides.length === 0
|
|
1307
1357
|
? null
|
|
1308
1358
|
: (x, z) => {
|
|
@@ -1789,6 +1839,7 @@ export function registerCityKind() {
|
|
|
1789
1839
|
schema: CITY_SCHEMA,
|
|
1790
1840
|
coverage: { spec: "city", densityKey: "roadsideOccupancy" },
|
|
1791
1841
|
resolve: (object, _params, context) => resolveCityObject(object, context),
|
|
1842
|
+
solids: (resolved, _object, params, context) => resolved === null || params["solid"] === false ? [] : citySolids(resolved, context.sampleHeight),
|
|
1792
1843
|
note: (object) => {
|
|
1793
1844
|
const resolved = resolveCityObject(object);
|
|
1794
1845
|
if (resolved === null)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GameContext } from "../runtime/gameContextTypes.js";
|
|
2
|
+
import type { MotionIntents } from "../runtime/motionIntents.js";
|
|
3
|
+
/** The game selects affected players, coupling, target bits and its acceleration limit. */
|
|
4
|
+
export interface EnvironmentMotionTarget {
|
|
5
|
+
entityId: string;
|
|
6
|
+
motion: Pick<MotionIntents, "impulse" | "pushHorizontal">;
|
|
7
|
+
/** Wind coupling in 1/second; supported range 0..1e12. */
|
|
8
|
+
windResponse: number;
|
|
9
|
+
/** Maximum combined acceleration in metres/second²; supported range 0..1e12. */
|
|
10
|
+
maxAcceleration: number;
|
|
11
|
+
mask?: number;
|
|
12
|
+
/** Opt into authored shelter exposure scaling for wind and localized forces. */
|
|
13
|
+
sheltered?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Inject authoritative spatial forces before existing movement consumes its motion intents.
|
|
16
|
+
* @capability environment-motion apply capped spatial wind and vortex forces to game-selected actors through shared motion intents
|
|
17
|
+
*/
|
|
18
|
+
export declare function installEnvironmentMotion(ctx: GameContext, options: {
|
|
19
|
+
targets: () => readonly EnvironmentMotionTarget[];
|
|
20
|
+
maxTargets?: number;
|
|
21
|
+
}): () => void;
|