@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,74 @@
|
|
|
1
|
+
const clamp01 = (value) => (Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0);
|
|
2
|
+
/**
|
|
3
|
+
* Per-player haptic channels mixed by priority.
|
|
4
|
+
* @capability haptics Continuous gamepad rumble from named channels (engine, road, impact) mixed by priority, plus fading pulses.
|
|
5
|
+
*/
|
|
6
|
+
export function createHapticChannels(options = {}) {
|
|
7
|
+
let duck = clamp01(options.duck ?? 0.4);
|
|
8
|
+
const channels = new Map();
|
|
9
|
+
const last = { strong: 0, weak: 0 };
|
|
10
|
+
const write = (name, channel) => {
|
|
11
|
+
if (channel.strong === 0 && channel.weak === 0)
|
|
12
|
+
channels.delete(name);
|
|
13
|
+
else
|
|
14
|
+
channels.set(name, channel);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
set(name, level, priority = 0) {
|
|
18
|
+
write(name, { strong: clamp01(level.strong), weak: clamp01(level.weak), priority, remainingMs: null, totalMs: null });
|
|
19
|
+
},
|
|
20
|
+
pulse(name, pulse, priority = 0) {
|
|
21
|
+
const ms = Math.max(0, pulse.ms);
|
|
22
|
+
if (ms === 0) {
|
|
23
|
+
channels.delete(name);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
write(name, { strong: clamp01(pulse.strong), weak: clamp01(pulse.weak), priority, remainingMs: ms, totalMs: ms });
|
|
27
|
+
},
|
|
28
|
+
clear(name) {
|
|
29
|
+
if (name === undefined)
|
|
30
|
+
channels.clear();
|
|
31
|
+
else
|
|
32
|
+
channels.delete(name);
|
|
33
|
+
},
|
|
34
|
+
mix(dt) {
|
|
35
|
+
const stepMs = Math.max(0, dt) * 1000;
|
|
36
|
+
let top = -Infinity;
|
|
37
|
+
for (const [name, channel] of channels) {
|
|
38
|
+
if (channel.remainingMs !== null) {
|
|
39
|
+
channel.remainingMs -= stepMs;
|
|
40
|
+
if (channel.remainingMs <= 0) {
|
|
41
|
+
channels.delete(name);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (channel.priority > top)
|
|
46
|
+
top = channel.priority;
|
|
47
|
+
}
|
|
48
|
+
let strong = 0;
|
|
49
|
+
let weak = 0;
|
|
50
|
+
for (const channel of channels.values()) {
|
|
51
|
+
const fade = channel.remainingMs === null || channel.totalMs === null ? 1 : channel.remainingMs / channel.totalMs;
|
|
52
|
+
const scale = (channel.priority >= top ? 1 : duck) * fade;
|
|
53
|
+
strong = Math.max(strong, channel.strong * scale);
|
|
54
|
+
weak = Math.max(weak, channel.weak * scale);
|
|
55
|
+
}
|
|
56
|
+
last.strong = strong;
|
|
57
|
+
last.weak = weak;
|
|
58
|
+
return last;
|
|
59
|
+
},
|
|
60
|
+
level: () => last,
|
|
61
|
+
retune(next) {
|
|
62
|
+
duck = clamp01(next.duck ?? duck);
|
|
63
|
+
},
|
|
64
|
+
snapshot() {
|
|
65
|
+
return { duck, channels: [...channels].map(([name, channel]) => ({ name, ...channel })) };
|
|
66
|
+
},
|
|
67
|
+
restore(snapshot) {
|
|
68
|
+
duck = clamp01(snapshot.duck);
|
|
69
|
+
channels.clear();
|
|
70
|
+
for (const { name, ...channel } of snapshot.channels)
|
|
71
|
+
channels.set(name, { ...channel });
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One action's press history inside an {@link InputBuffer}. */
|
|
2
|
+
export interface BufferedAction {
|
|
3
|
+
pressedAt: number;
|
|
4
|
+
previousPressAt: number | null;
|
|
5
|
+
releasedAt: number | null;
|
|
6
|
+
consumed: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Serializable state for an {@link InputBuffer}. */
|
|
9
|
+
export interface InputBufferSnapshot {
|
|
10
|
+
windowMs: number;
|
|
11
|
+
actions: Record<string, BufferedAction>;
|
|
12
|
+
}
|
|
13
|
+
/** Remembers recent action presses so a press slightly early (a jump before landing) still counts. Times are caller-supplied ms, so it runs the same on a client, a host or in a replay. */
|
|
14
|
+
export interface InputBuffer {
|
|
15
|
+
/** Record a press edge. */
|
|
16
|
+
press(action: string, nowMs: number): void;
|
|
17
|
+
/** Record a release edge; {@link InputBuffer.hold} stops growing. */
|
|
18
|
+
release(action: string, nowMs: number): void;
|
|
19
|
+
/** `true` once for a press no older than `windowMs` (default: the buffer's window), then the press is spent. */
|
|
20
|
+
consume(action: string, nowMs: number, windowMs?: number): boolean;
|
|
21
|
+
/** `true` while `nowMs` is within `windowMs` of `groundedAtMs` — late-jump grace after leaving a ledge. `null` never qualifies. */
|
|
22
|
+
coyote(groundedAtMs: number | null, nowMs: number, windowMs: number): boolean;
|
|
23
|
+
/** How long the action has been held, ms; `0` when released or never pressed. */
|
|
24
|
+
hold(action: string, nowMs: number): number;
|
|
25
|
+
/** `true` when the last two presses were at most `gapMs` apart and the latest is no older than `gapMs`. */
|
|
26
|
+
doubleTap(action: string, nowMs: number, gapMs: number): boolean;
|
|
27
|
+
/** Change the default buffer window. */
|
|
28
|
+
retune(next: {
|
|
29
|
+
windowMs: number;
|
|
30
|
+
}): void;
|
|
31
|
+
snapshot(): InputBufferSnapshot;
|
|
32
|
+
restore(snapshot: InputBufferSnapshot): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates an input buffer for jump buffering, coyote time, hold duration and double taps.
|
|
36
|
+
* @capability input-buffer buffer early presses, coyote time, hold duration and double tap for responsive controls
|
|
37
|
+
*/
|
|
38
|
+
export declare function createInputBuffer(options: {
|
|
39
|
+
windowMs: number;
|
|
40
|
+
}): InputBuffer;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an input buffer for jump buffering, coyote time, hold duration and double taps.
|
|
3
|
+
* @capability input-buffer buffer early presses, coyote time, hold duration and double tap for responsive controls
|
|
4
|
+
*/
|
|
5
|
+
export function createInputBuffer(options) {
|
|
6
|
+
let windowMs = options.windowMs;
|
|
7
|
+
let actions = new Map();
|
|
8
|
+
return {
|
|
9
|
+
press(action, nowMs) {
|
|
10
|
+
const previous = actions.get(action);
|
|
11
|
+
actions.set(action, {
|
|
12
|
+
pressedAt: nowMs,
|
|
13
|
+
previousPressAt: previous === undefined ? null : previous.pressedAt,
|
|
14
|
+
releasedAt: null,
|
|
15
|
+
consumed: false,
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
release(action, nowMs) {
|
|
19
|
+
const entry = actions.get(action);
|
|
20
|
+
if (entry !== undefined && entry.releasedAt === null)
|
|
21
|
+
entry.releasedAt = nowMs;
|
|
22
|
+
},
|
|
23
|
+
consume(action, nowMs, window = windowMs) {
|
|
24
|
+
const entry = actions.get(action);
|
|
25
|
+
if (entry === undefined || entry.consumed || nowMs - entry.pressedAt > window)
|
|
26
|
+
return false;
|
|
27
|
+
entry.consumed = true;
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
coyote(groundedAtMs, nowMs, window) {
|
|
31
|
+
return groundedAtMs !== null && nowMs - groundedAtMs <= window;
|
|
32
|
+
},
|
|
33
|
+
hold(action, nowMs) {
|
|
34
|
+
const entry = actions.get(action);
|
|
35
|
+
if (entry === undefined || entry.releasedAt !== null)
|
|
36
|
+
return 0;
|
|
37
|
+
return Math.max(0, nowMs - entry.pressedAt);
|
|
38
|
+
},
|
|
39
|
+
doubleTap(action, nowMs, gapMs) {
|
|
40
|
+
const entry = actions.get(action);
|
|
41
|
+
if (entry === undefined || entry.previousPressAt === null)
|
|
42
|
+
return false;
|
|
43
|
+
return entry.pressedAt - entry.previousPressAt <= gapMs && nowMs - entry.pressedAt <= gapMs;
|
|
44
|
+
},
|
|
45
|
+
retune(next) {
|
|
46
|
+
windowMs = next.windowMs;
|
|
47
|
+
},
|
|
48
|
+
snapshot() {
|
|
49
|
+
const copy = {};
|
|
50
|
+
for (const [action, entry] of actions)
|
|
51
|
+
copy[action] = { ...entry };
|
|
52
|
+
return { windowMs, actions: copy };
|
|
53
|
+
},
|
|
54
|
+
restore(snapshot) {
|
|
55
|
+
windowMs = snapshot.windowMs;
|
|
56
|
+
actions = new Map(Object.entries(snapshot.actions).map(([action, entry]) => [action, { ...entry }]));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -9,6 +9,14 @@ export interface LookDeltas {
|
|
|
9
9
|
yaw: number;
|
|
10
10
|
pitch: number;
|
|
11
11
|
}
|
|
12
|
+
/** Plain JSON state of a {@link LookChannel}: the pending pixel deltas and the committed pose. */
|
|
13
|
+
export interface LookChannelState {
|
|
14
|
+
deltaX: number;
|
|
15
|
+
deltaY: number;
|
|
16
|
+
yaw: number;
|
|
17
|
+
pitch: number;
|
|
18
|
+
verticalOffset: number;
|
|
19
|
+
}
|
|
12
20
|
export interface LookChannel {
|
|
13
21
|
accumulate(dx: number, dy: number): void;
|
|
14
22
|
/** Yaw/pitch deltas (radians) for this frame; clears the accumulator. */
|
|
@@ -19,6 +27,8 @@ export interface LookChannel {
|
|
|
19
27
|
readPitch(): number;
|
|
20
28
|
setVerticalOffset(offset: number): void;
|
|
21
29
|
readVerticalOffset(): number;
|
|
30
|
+
snapshot(): LookChannelState;
|
|
31
|
+
restore(next: LookChannelState): void;
|
|
22
32
|
}
|
|
23
33
|
export interface LookChannelOptions {
|
|
24
34
|
/** Radians of rotation per pixel of pointer travel. */
|
|
@@ -40,5 +40,11 @@ export function createLookChannel({ sensitivity, maxVerticalOffset = Infinity })
|
|
|
40
40
|
readVerticalOffset() {
|
|
41
41
|
return verticalOffset;
|
|
42
42
|
},
|
|
43
|
+
snapshot() {
|
|
44
|
+
return { deltaX, deltaY, yaw, pitch, verticalOffset };
|
|
45
|
+
},
|
|
46
|
+
restore(next) {
|
|
47
|
+
({ deltaX, deltaY, yaw, pitch, verticalOffset } = next);
|
|
48
|
+
},
|
|
43
49
|
};
|
|
44
50
|
}
|
package/dist/input/pointer.d.ts
CHANGED
|
@@ -65,5 +65,7 @@ export interface DragCapture {
|
|
|
65
65
|
cancel(): void;
|
|
66
66
|
/** The current drag state, or `null` when idle. */
|
|
67
67
|
state(): DragState | null;
|
|
68
|
+
/** Resumes the drag a {@link DragCapture.state} described (its `origin` and `current`), or goes idle on `null`. */
|
|
69
|
+
restore(next: DragState | null): void;
|
|
68
70
|
}
|
|
69
71
|
export declare function createDragCapture(config?: DragCaptureConfig): DragCapture;
|
package/dist/input/pointer.js
CHANGED
|
@@ -61,5 +61,9 @@ export function createDragCapture(config = {}) {
|
|
|
61
61
|
current = null;
|
|
62
62
|
},
|
|
63
63
|
state: snapshot,
|
|
64
|
+
restore(next) {
|
|
65
|
+
origin = next === null ? null : [next.origin[0], next.origin[1], next.origin[2]];
|
|
66
|
+
current = next === null ? null : [next.current[0], next.current[1], next.current[2]];
|
|
67
|
+
},
|
|
64
68
|
};
|
|
65
69
|
}
|
|
@@ -29,13 +29,14 @@ export interface PromptRegistrySnapshot {
|
|
|
29
29
|
export interface PromptRegistry {
|
|
30
30
|
/**
|
|
31
31
|
* Add (or replace, by id) a positioned prompt. Replacing an existing id keeps
|
|
32
|
-
* its registration slot so tie-break order is stable.
|
|
33
|
-
*
|
|
32
|
+
* its registration slot so tie-break order is stable. Changed active content
|
|
33
|
+
* notifies immediately; selection is recalculated by the next {@link resolve}.
|
|
34
34
|
*/
|
|
35
35
|
register(prompt: PositionedPrompt): void;
|
|
36
36
|
/**
|
|
37
37
|
* Patch a registered prompt in place. No-op if `id` is unknown. Only the fields
|
|
38
|
-
* present in `patch` change; the rest are preserved.
|
|
38
|
+
* present in `patch` change; the rest are preserved. Changed active content
|
|
39
|
+
* notifies immediately without recalculating selection.
|
|
39
40
|
*/
|
|
40
41
|
update(id: string, patch: PositionedPromptPatch): void;
|
|
41
42
|
/** Remove the prompt with this id, if present. Returns whether one was removed. */
|
|
@@ -54,7 +55,7 @@ export interface PromptRegistry {
|
|
|
54
55
|
resolve(playerPosition: PromptPoint): PositionedPrompt | null;
|
|
55
56
|
/** The prompt selected by the most recent {@link resolve}, or `null`. */
|
|
56
57
|
active(): PositionedPrompt | null;
|
|
57
|
-
/** Observe active-prompt changes
|
|
58
|
+
/** Observe active-prompt content changes, selection transitions, clear and restore. Returns an unsubscribe fn. */
|
|
58
59
|
subscribe(listener: () => void): () => void;
|
|
59
60
|
/** Serializable state for a save. */
|
|
60
61
|
snapshot(): PromptRegistrySnapshot;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveActivePrompt, } from "./proximityPrompt.js";
|
|
1
|
+
import { positionedPromptsEqual, resolveActivePrompt, } from "./proximityPrompt.js";
|
|
2
2
|
/** Structural copy of a positioned prompt so external mutation can't corrupt registry state. */
|
|
3
3
|
function clonePrompt(prompt) {
|
|
4
4
|
const copy = {
|
|
@@ -50,8 +50,12 @@ export function createPromptRegistry() {
|
|
|
50
50
|
}
|
|
51
51
|
return {
|
|
52
52
|
register(prompt) {
|
|
53
|
-
prompts.
|
|
53
|
+
const existing = prompts.get(prompt.id);
|
|
54
|
+
const next = clonePrompt(prompt);
|
|
55
|
+
prompts.set(prompt.id, next);
|
|
54
56
|
invalidate();
|
|
57
|
+
if (activeId === prompt.id && existing !== undefined && !positionedPromptsEqual(existing, next))
|
|
58
|
+
notify();
|
|
55
59
|
},
|
|
56
60
|
update(id, patch) {
|
|
57
61
|
const existing = prompts.get(id);
|
|
@@ -71,6 +75,8 @@ export function createPromptRegistry() {
|
|
|
71
75
|
}
|
|
72
76
|
prompts.set(id, merged);
|
|
73
77
|
invalidate();
|
|
78
|
+
if (activeId === id && !positionedPromptsEqual(existing, merged))
|
|
79
|
+
notify();
|
|
74
80
|
},
|
|
75
81
|
unregister(id) {
|
|
76
82
|
const removed = prompts.delete(id);
|
|
@@ -14,6 +14,9 @@ function violatesKind(layout, traits, itemId) {
|
|
|
14
14
|
return kind === null || !layout.accepts.includes(kind);
|
|
15
15
|
return kind !== layout.accepts;
|
|
16
16
|
}
|
|
17
|
+
function validSlot(state, slot) {
|
|
18
|
+
return Number.isInteger(slot) && slot >= 0 && slot < state.slots.length;
|
|
19
|
+
}
|
|
17
20
|
function computeCapacity(state, traits, itemId) {
|
|
18
21
|
const limit = traits.stackLimit(itemId);
|
|
19
22
|
let existingRoom = 0;
|
|
@@ -58,7 +61,7 @@ function autoPut(state, layout, traits, itemId, count) {
|
|
|
58
61
|
return { status: "ok", state: { slots } };
|
|
59
62
|
}
|
|
60
63
|
function explicitPut(state, layout, traits, itemId, count, slot) {
|
|
61
|
-
if (
|
|
64
|
+
if (!validSlot(state, slot))
|
|
62
65
|
return { status: "rejected", reason: "invalid-slot" };
|
|
63
66
|
if (violatesKind(layout, traits, itemId))
|
|
64
67
|
return { status: "rejected", reason: "wrong-kind" };
|
|
@@ -120,7 +123,7 @@ export function hasItem(state, itemId, count) {
|
|
|
120
123
|
}
|
|
121
124
|
/** Move the stack at `fromSlot` to `toSlot` (or auto-stack when omitted) — swapping, merging with remainder, or relocating across two inventories. Rejects on empty source, wrong kind, or no space. */
|
|
122
125
|
export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
|
|
123
|
-
if (
|
|
126
|
+
if (!validSlot(from, fromSlot))
|
|
124
127
|
return { status: "rejected", reason: "invalid-slot" };
|
|
125
128
|
const source = from.slots[fromSlot];
|
|
126
129
|
if (source === null)
|
|
@@ -138,7 +141,7 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
|
|
|
138
141
|
const toSlots = sameInventory ? fromSlots : to.slots.slice();
|
|
139
142
|
const limit = traits.stackLimit(source.itemId);
|
|
140
143
|
if (toSlot !== undefined) {
|
|
141
|
-
if (
|
|
144
|
+
if (!validSlot(to, toSlot))
|
|
142
145
|
return { status: "rejected", reason: "invalid-slot" };
|
|
143
146
|
const dest = toSlots[toSlot];
|
|
144
147
|
if (dest === null) {
|
|
@@ -179,12 +182,12 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
|
|
|
179
182
|
* @capability inventory-split split a stack into a target or first-empty slot
|
|
180
183
|
*/
|
|
181
184
|
export function splitStack(state, slot, amount, toSlot) {
|
|
182
|
-
if (
|
|
185
|
+
if (!validSlot(state, slot))
|
|
183
186
|
return { status: "rejected", reason: "invalid-slot" };
|
|
184
187
|
const source = state.slots[slot];
|
|
185
188
|
if (source === null)
|
|
186
189
|
return { status: "rejected", reason: "empty-slot" };
|
|
187
|
-
if (amount <= 0 || amount >= source.count)
|
|
190
|
+
if (!Number.isFinite(amount) || amount <= 0 || amount >= source.count)
|
|
188
191
|
return { status: "rejected", reason: "invalid-amount" };
|
|
189
192
|
let target = toSlot;
|
|
190
193
|
if (target === undefined) {
|
|
@@ -193,7 +196,7 @@ export function splitStack(state, slot, amount, toSlot) {
|
|
|
193
196
|
return { status: "rejected", reason: "no-space" };
|
|
194
197
|
}
|
|
195
198
|
else {
|
|
196
|
-
if (
|
|
199
|
+
if (!validSlot(state, target))
|
|
197
200
|
return { status: "rejected", reason: "invalid-slot" };
|
|
198
201
|
if (target === slot)
|
|
199
202
|
return { status: "rejected", reason: "slot-occupied" };
|
|
@@ -18,7 +18,7 @@ export interface ShapedGrid<T> {
|
|
|
18
18
|
readonly height: number;
|
|
19
19
|
readonly placements: readonly Placement<T>[];
|
|
20
20
|
}
|
|
21
|
-
export type ShapedRejection = "out-of-bounds" | "overlap" | "duplicate-id" | "unknown-id";
|
|
21
|
+
export type ShapedRejection = "out-of-bounds" | "overlap" | "duplicate-id" | "unknown-id" | "invalid-footprint" | "invalid-origin";
|
|
22
22
|
export interface ShapedPlaceResult<T> {
|
|
23
23
|
status: "ok";
|
|
24
24
|
grid: ShapedGrid<T>;
|
|
@@ -36,19 +36,58 @@ export declare function rotateFootprint(footprint: Footprint, rotation: Rotation
|
|
|
36
36
|
/** @internal */
|
|
37
37
|
export declare function occupiedCells(footprint: Footprint, origin: Cell, rotation: Rotation): Cell[];
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* An immutable spatial inventory board holding caller-defined multi-cell items.
|
|
40
40
|
*
|
|
41
|
-
* @capability
|
|
42
|
-
|
|
43
|
-
*/
|
|
41
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
42
|
+
*/
|
|
44
43
|
export declare function createShapedGrid<T>(width: number, height: number): ShapedGrid<T>;
|
|
45
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Check bounds and overlap for a discrete footprint, optionally ignoring the item being moved.
|
|
46
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
47
|
+
*/
|
|
46
48
|
export declare function canPlace<T>(grid: ShapedGrid<T>, footprint: Footprint, origin: Cell, rotation: Rotation, ignoreId?: string): ShapedRejection | null;
|
|
47
|
-
/**
|
|
49
|
+
/** Search policy: rotation order first, then rows and columns; the work budget counts candidate origins. */
|
|
50
|
+
export interface ShapedPlacementSearchOptions {
|
|
51
|
+
/** Defaults to `[0, 1, 2, 3]`; a game can prefer or forbid rotations. */
|
|
52
|
+
rotations?: readonly Rotation[];
|
|
53
|
+
/** Ignore an existing placement while searching for its next position. */
|
|
54
|
+
ignoreId?: string;
|
|
55
|
+
/** Nonnegative integer candidate limit; defaults to 4096. */
|
|
56
|
+
maxChecks?: number;
|
|
57
|
+
}
|
|
58
|
+
/** A detached placement, a completed no-fit search, or an incomplete search that reached its budget. */
|
|
59
|
+
export type ShapedPlacementSearchResult = {
|
|
60
|
+
status: "found";
|
|
61
|
+
origin: Cell;
|
|
62
|
+
rotation: Rotation;
|
|
63
|
+
checks: number;
|
|
64
|
+
} | {
|
|
65
|
+
status: "no-space";
|
|
66
|
+
checks: number;
|
|
67
|
+
} | {
|
|
68
|
+
status: "budget-exceeded";
|
|
69
|
+
checks: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Find the first legal placement without mutating the board; occupancy is indexed once per search.
|
|
73
|
+
*
|
|
74
|
+
* @capability shaped-placement find a shaped inventory item's first fit with ordered rotations, row-major placement, and an explicit search budget
|
|
75
|
+
*/
|
|
76
|
+
export declare function findShapedPlacement<T>(grid: ShapedGrid<T>, footprint: Footprint, options?: ShapedPlacementSearchOptions): ShapedPlacementSearchResult;
|
|
77
|
+
/**
|
|
78
|
+
* Place a caller-defined item without mutating the source board; duplicate ids reject.
|
|
79
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
80
|
+
*/
|
|
48
81
|
export declare function placeShaped<T>(grid: ShapedGrid<T>, item: ShapedItem<T>, origin: Cell, rotation?: Rotation): ShapedResult<T>;
|
|
49
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Remove one placement without mutating the source board.
|
|
84
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
85
|
+
*/
|
|
50
86
|
export declare function removeShaped<T>(grid: ShapedGrid<T>, id: string): ShapedResult<T>;
|
|
51
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Move or rotate one placement, ignoring its current cells during overlap checks.
|
|
89
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
90
|
+
*/
|
|
52
91
|
export declare function moveShaped<T>(grid: ShapedGrid<T>, id: string, origin: Cell, rotation?: Rotation): ShapedResult<T>;
|
|
53
92
|
/** @internal */
|
|
54
93
|
export declare function cellOccupant<T>(grid: ShapedGrid<T>, cell: Cell): string | null;
|
|
@@ -41,16 +41,28 @@ export function occupiedCells(footprint, origin, rotation) {
|
|
|
41
41
|
return rotated.map(([c, r]) => [c + origin[0], r + origin[1]]);
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* An immutable spatial inventory board holding caller-defined multi-cell items.
|
|
45
45
|
*
|
|
46
|
-
* @capability
|
|
47
|
-
|
|
48
|
-
*/
|
|
46
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
47
|
+
*/
|
|
49
48
|
export function createShapedGrid(width, height) {
|
|
50
|
-
|
|
51
|
-
throw new Error("shaped grid needs positive dimensions");
|
|
49
|
+
requireDimensions(width, height);
|
|
52
50
|
return { width, height, placements: [] };
|
|
53
51
|
}
|
|
52
|
+
function requireDimensions(width, height) {
|
|
53
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) {
|
|
54
|
+
throw new RangeError("shaped grid needs positive integer dimensions");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function validCell(cell) {
|
|
58
|
+
return Number.isSafeInteger(cell[0]) && Number.isSafeInteger(cell[1]);
|
|
59
|
+
}
|
|
60
|
+
function validFootprint(footprint) {
|
|
61
|
+
return footprint.length > 0 && footprint.every(validCell);
|
|
62
|
+
}
|
|
63
|
+
function validRotation(rotation) {
|
|
64
|
+
return Number.isInteger(rotation) && rotation >= 0 && rotation <= 3;
|
|
65
|
+
}
|
|
54
66
|
function inBounds(grid, cells) {
|
|
55
67
|
return cells.every(([c, r]) => c >= 0 && r >= 0 && c < grid.width && r < grid.height);
|
|
56
68
|
}
|
|
@@ -65,8 +77,16 @@ function occupancyMap(grid, ignoreId) {
|
|
|
65
77
|
}
|
|
66
78
|
return map;
|
|
67
79
|
}
|
|
68
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Check bounds and overlap for a discrete footprint, optionally ignoring the item being moved.
|
|
82
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
83
|
+
*/
|
|
69
84
|
export function canPlace(grid, footprint, origin, rotation, ignoreId) {
|
|
85
|
+
requireDimensions(grid.width, grid.height);
|
|
86
|
+
if (!validFootprint(footprint) || !validRotation(rotation))
|
|
87
|
+
return "invalid-footprint";
|
|
88
|
+
if (!validCell(origin))
|
|
89
|
+
return "invalid-origin";
|
|
70
90
|
const cells = occupiedCells(footprint, origin, rotation);
|
|
71
91
|
if (!inBounds(grid, cells))
|
|
72
92
|
return "out-of-bounds";
|
|
@@ -77,7 +97,40 @@ export function canPlace(grid, footprint, origin, rotation, ignoreId) {
|
|
|
77
97
|
}
|
|
78
98
|
return null;
|
|
79
99
|
}
|
|
80
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Find the first legal placement without mutating the board; occupancy is indexed once per search.
|
|
102
|
+
*
|
|
103
|
+
* @capability shaped-placement find a shaped inventory item's first fit with ordered rotations, row-major placement, and an explicit search budget
|
|
104
|
+
*/
|
|
105
|
+
export function findShapedPlacement(grid, footprint, options = {}) {
|
|
106
|
+
requireDimensions(grid.width, grid.height);
|
|
107
|
+
const rotations = options.rotations ?? [0, 1, 2, 3];
|
|
108
|
+
const maxChecks = options.maxChecks ?? 4096;
|
|
109
|
+
if (!validFootprint(footprint) || !rotations.every(validRotation))
|
|
110
|
+
throw new RangeError("invalid footprint or rotation");
|
|
111
|
+
if (!Number.isSafeInteger(maxChecks) || maxChecks < 0)
|
|
112
|
+
throw new RangeError("invalid placement search budget");
|
|
113
|
+
const occupied = occupancyMap(grid, options.ignoreId);
|
|
114
|
+
let checks = 0;
|
|
115
|
+
for (const rotation of rotations) {
|
|
116
|
+
const cells = rotateFootprint(footprint, rotation);
|
|
117
|
+
for (let row = 0; row < grid.height; row += 1) {
|
|
118
|
+
for (let column = 0; column < grid.width; column += 1) {
|
|
119
|
+
if (checks >= maxChecks)
|
|
120
|
+
return { status: "budget-exceeded", checks };
|
|
121
|
+
checks += 1;
|
|
122
|
+
if (cells.every(([c, r]) => column + c < grid.width && row + r < grid.height && !occupied.has(cellKey([column + c, row + r])))) {
|
|
123
|
+
return { status: "found", origin: [column, row], rotation, checks };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { status: "no-space", checks };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Place a caller-defined item without mutating the source board; duplicate ids reject.
|
|
132
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
133
|
+
*/
|
|
81
134
|
export function placeShaped(grid, item, origin, rotation = 0) {
|
|
82
135
|
if (grid.placements.some((p) => p.id === item.id)) {
|
|
83
136
|
return { status: "rejected", reason: "duplicate-id", detail: item.id };
|
|
@@ -94,14 +147,20 @@ export function placeShaped(grid, item, origin, rotation = 0) {
|
|
|
94
147
|
};
|
|
95
148
|
return { status: "ok", grid: { ...grid, placements: [...grid.placements, placement] } };
|
|
96
149
|
}
|
|
97
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Remove one placement without mutating the source board.
|
|
152
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
153
|
+
*/
|
|
98
154
|
export function removeShaped(grid, id) {
|
|
99
155
|
if (!grid.placements.some((p) => p.id === id)) {
|
|
100
156
|
return { status: "rejected", reason: "unknown-id", detail: id };
|
|
101
157
|
}
|
|
102
158
|
return { status: "ok", grid: { ...grid, placements: grid.placements.filter((p) => p.id !== id) } };
|
|
103
159
|
}
|
|
104
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Move or rotate one placement, ignoring its current cells during overlap checks.
|
|
162
|
+
* @capability shaped-grid a serializable inventory board with arbitrary cell footprints, rotation, overlap checks, and immutable placement
|
|
163
|
+
*/
|
|
105
164
|
export function moveShaped(grid, id, origin, rotation) {
|
|
106
165
|
const existing = grid.placements.find((p) => p.id === id);
|
|
107
166
|
if (existing === undefined)
|
package/dist/item/affix.d.ts
CHANGED
|
@@ -51,4 +51,10 @@ export interface AffixRoller {
|
|
|
51
51
|
roll(base: ItemBaseDef, rarityId: string, rng: () => number): RolledItem;
|
|
52
52
|
rollRandom(base: ItemBaseDef, rng: () => number): RolledItem;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Rolls randomized gear from a base item: a weighted rarity tier scales base stats and picks
|
|
56
|
+
* prefix/suffix affixes that name and modify the item. Deterministic for a given `rng`.
|
|
57
|
+
*
|
|
58
|
+
* @capability affix-roller roll randomized gear (guns, armor, loot) with rarity tiers, stat scaling and prefix/suffix affixes from a seed
|
|
59
|
+
*/
|
|
54
60
|
export declare function createAffixRoller(config: RollerConfig): AffixRoller;
|
package/dist/item/affix.js
CHANGED
|
@@ -20,6 +20,12 @@ function assertConfig(config) {
|
|
|
20
20
|
throw new Error(`rarity "${tier.id}" weight must be positive`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Rolls randomized gear from a base item: a weighted rarity tier scales base stats and picks
|
|
25
|
+
* prefix/suffix affixes that name and modify the item. Deterministic for a given `rng`.
|
|
26
|
+
*
|
|
27
|
+
* @capability affix-roller roll randomized gear (guns, armor, loot) with rarity tiers, stat scaling and prefix/suffix affixes from a seed
|
|
28
|
+
*/
|
|
23
29
|
export function createAffixRoller(config) {
|
|
24
30
|
assertConfig(config);
|
|
25
31
|
const poolById = new Map(config.pools.map((p) => [p.id, p]));
|
package/dist/item/durability.js
CHANGED
|
@@ -21,6 +21,8 @@ export function wearAmount(spec, kind) {
|
|
|
21
21
|
* @capability durability track item wear, breakage, and repair
|
|
22
22
|
*/
|
|
23
23
|
export function applyWear(state, amount) {
|
|
24
|
+
if (Number.isNaN(amount))
|
|
25
|
+
throw new RangeError("Wear amount must not be NaN.");
|
|
24
26
|
if (amount <= 0)
|
|
25
27
|
return state;
|
|
26
28
|
return { current: clamp(state.current - amount, 0, state.max), max: state.max };
|
|
@@ -40,6 +42,8 @@ export function repairQuote(spec, state, options) {
|
|
|
40
42
|
return null;
|
|
41
43
|
if (repair.station !== undefined && repair.station !== options?.station)
|
|
42
44
|
return null;
|
|
45
|
+
if (options?.to !== undefined && !Number.isFinite(options.to))
|
|
46
|
+
return null;
|
|
43
47
|
const loss = repair.qualityLossPerRepair ?? 0;
|
|
44
48
|
const newMax = Math.max(1, state.max - loss);
|
|
45
49
|
const capped = Math.min(state.current, newMax);
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/** Save data for a registry; definitions must be structured-cloneable and use the caller's serialization format. */
|
|
2
|
+
export interface ItemInstanceRegistryState<TDef> {
|
|
3
|
+
prefix: string;
|
|
4
|
+
sequence: number;
|
|
5
|
+
entries: [string, TDef][];
|
|
6
|
+
}
|
|
7
|
+
/** Caller-owned synchronous storage; `entries()` must stream without materializing the collection. */
|
|
8
|
+
export interface ItemInstanceStorage<TDef> {
|
|
9
|
+
get(id: string): TDef | undefined;
|
|
10
|
+
has(id: string): boolean;
|
|
11
|
+
set(id: string, def: TDef): void;
|
|
12
|
+
delete(id: string): void;
|
|
13
|
+
count(): number;
|
|
14
|
+
entries(): Iterable<[string, TDef]>;
|
|
15
|
+
/** Consume entries completely and atomically replace storage; any failure must preserve previous data. */
|
|
16
|
+
replace(entries: Iterable<[string, TDef]>): void;
|
|
17
|
+
}
|
|
18
|
+
/** Inject storage and its saved allocator sequence when attaching an existing collection. */
|
|
19
|
+
export interface ItemInstanceRegistryOptions<TDef> {
|
|
20
|
+
storage?: ItemInstanceStorage<TDef>;
|
|
21
|
+
/** Nonnegative safe integer; defaults to zero. Retain it across releases and storage reattachment. */
|
|
22
|
+
sequence?: number;
|
|
23
|
+
}
|
|
1
24
|
/**
|
|
2
25
|
* A runtime store for procedurally generated item instances — a rolled unique gun, a rolled
|
|
3
26
|
* affixed relic — keyed by a generated id distinct from any static catalog id. The counterpart a
|
|
@@ -5,6 +28,14 @@
|
|
|
5
28
|
* rolls never need a hand-rolled parallel registry (#536.1).
|
|
6
29
|
*/
|
|
7
30
|
export interface ItemInstanceRegistry<TDef> {
|
|
31
|
+
/** Detached definitions and allocator state, including ids of released instances in the sequence. */
|
|
32
|
+
state(): ItemInstanceRegistryState<TDef>;
|
|
33
|
+
/** Detached batches of at most `pageSize` entries; throws if the registry changes during iteration. Keep external storage and definitions unchanged until finished. */
|
|
34
|
+
statePages(pageSize: number): IterableIterator<ItemInstanceRegistryState<TDef>>;
|
|
35
|
+
/** Replace the registry; returns false without mutation when the prefix or allocator is invalid. */
|
|
36
|
+
restore(next: ItemInstanceRegistryState<TDef>): boolean;
|
|
37
|
+
/** Stream detached pages through atomic storage replacement. All pages must share prefix and sequence; malformed pages or storage failures throw without changing state. Empty saves require one empty page. */
|
|
38
|
+
restorePages(pages: Iterable<ItemInstanceRegistryState<TDef>>): boolean;
|
|
8
39
|
/** Stores `def` under a fresh generated id derived from `baseId`; returns that id. */
|
|
9
40
|
register(baseId: string, def: TDef): string;
|
|
10
41
|
get(id: string): TDef | undefined;
|
|
@@ -19,7 +50,7 @@ export interface ItemInstanceRegistry<TDef> {
|
|
|
19
50
|
*
|
|
20
51
|
* @capability item-instance-registry a runtime store for procedurally generated item instances
|
|
21
52
|
*/
|
|
22
|
-
export declare function createItemInstanceRegistry<TDef>(prefix?: string): ItemInstanceRegistry<TDef>;
|
|
53
|
+
export declare function createItemInstanceRegistry<TDef>(prefix?: string, options?: ItemInstanceRegistryOptions<TDef>): ItemInstanceRegistry<TDef>;
|
|
23
54
|
/**
|
|
24
55
|
* Bridges any procedural roller into a `LootEntry.generate` callback: rolls a `{ baseId, def }` pair
|
|
25
56
|
* and registers it, returning the runtime id the loot roll hands back as the drop's `item`.
|