@jgengine/core 0.19.0 → 0.22.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 +364 -0
- package/dist/ai/decisionGraph.d.ts +2 -0
- package/dist/ai/decisionGraph.js +6 -0
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/anim/animGraph.d.ts +33 -2
- package/dist/anim/animGraph.js +200 -12
- package/dist/anim/locomotionGraph.d.ts +11 -3
- package/dist/anim/locomotionGraph.js +25 -15
- package/dist/audio/audioFalloff.d.ts +4 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +19 -0
- package/dist/combat/abilityKit.js +40 -4
- package/dist/combat/death.d.ts +4 -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/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -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 +116 -17
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.js +1 -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/terrainMaterials.d.ts +10 -0
- package/dist/editor/terrainMaterials.js +13 -0
- package/dist/editor/types.d.ts +11 -0
- package/dist/game/cameraConfig.d.ts +3 -1
- 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 +5 -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/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +52 -20
- 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/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -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/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 +348 -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/movementModel.d.ts +24 -1
- package/dist/movement/movementModel.js +74 -7
- package/dist/movement/playerMovement.d.ts +28 -1
- package/dist/movement/playerMovement.js +425 -102
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +3 -1
- package/dist/movement/solidObstacles.js +8 -5
- 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 +3 -3
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/kinematicVehicle.d.ts +8 -3
- package/dist/physics/kinematicVehicle.js +2 -2
- package/dist/physics/physicsWorldBackend.js +19 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleDynamics.d.ts +3 -2
- package/dist/physics/vehicleDynamics.js +2 -2
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/runtime/cameraDirector.d.ts +39 -2
- package/dist/runtime/cameraDirector.js +162 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/context/combat.d.ts +11 -4
- package/dist/runtime/context/combat.js +178 -11
- package/dist/runtime/context/deathLoot.d.ts +9 -8
- package/dist/runtime/context/deathLoot.js +16 -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 +79 -13
- package/dist/runtime/gameContextTypes.d.ts +14 -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 +16 -4
- package/dist/runtime/inputSnapshot.js +31 -3
- 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/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 +26 -5
- package/dist/scene/behaviorRuntime.js +263 -16
- package/dist/scene/behaviors.d.ts +30 -1
- 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/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 +36 -2
- package/dist/scene/vehicleSeat.js +89 -2
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +9 -2
- package/dist/stats/eventMeter.js +5 -2
- 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/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/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/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingKit.d.ts +8 -3
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +3 -3
- package/dist/world/footprintGrid.d.ts +55 -0
- package/dist/world/footprintGrid.js +201 -0
- package/dist/world/grassKind.d.ts +10 -0
- package/dist/world/grassKind.js +22 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +2 -2
- package/dist/world/terraform.js +46 -18
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +11 -0
- package/dist/world/worldSolids.js +112 -1
- package/dist/world.d.ts +5 -5
- package/dist/world.js +4 -4
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-combat/SKILL.md +8 -2
- package/skills/jgengine-combat/api.md +22 -10
- 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/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +131 -37
- package/skills/jgengine-gameplay/capabilities.md +68 -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 +119 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
|
+
import { behaviorDataEqual } from "./behaviors.js";
|
|
2
3
|
/**
|
|
3
4
|
* Narrow `entity.meta` with a type guard — prefer this over `entity.meta as T` so failed shapes return `null`
|
|
4
5
|
* instead of lying to the type checker.
|
|
@@ -26,12 +27,29 @@ function toEntityPosition(position) {
|
|
|
26
27
|
return [position.x, position.y, position.z];
|
|
27
28
|
return position;
|
|
28
29
|
}
|
|
29
|
-
export function createEntityStore() {
|
|
30
|
-
const
|
|
30
|
+
export function createEntityStore(options = {}) {
|
|
31
|
+
const behaviorListeners = new Set();
|
|
32
|
+
const behaviorChanges = new Set();
|
|
33
|
+
const store = createObservableKeyedStore(undefined, (id, membershipChanged) => {
|
|
34
|
+
options.onChange?.(id, membershipChanged);
|
|
35
|
+
if (id !== undefined && behaviorChanges.delete(id))
|
|
36
|
+
for (const listener of behaviorListeners)
|
|
37
|
+
listener(id);
|
|
38
|
+
});
|
|
31
39
|
const spawnPoses = new Map();
|
|
32
40
|
const constraints = new Map();
|
|
33
41
|
const blackboards = new Map();
|
|
34
42
|
let nextCounter = 1;
|
|
43
|
+
function writeEntity(entity, previous = store.get(entity.id)?.behaviors) {
|
|
44
|
+
if (!behaviorDataEqual(previous ?? [], entity.behaviors))
|
|
45
|
+
behaviorChanges.add(entity.id);
|
|
46
|
+
store.set(entity.id, entity);
|
|
47
|
+
}
|
|
48
|
+
function deleteEntity(id) {
|
|
49
|
+
if ((store.get(id)?.behaviors.length ?? 0) > 0)
|
|
50
|
+
behaviorChanges.add(id);
|
|
51
|
+
store.delete(id);
|
|
52
|
+
}
|
|
35
53
|
const blackboard = {
|
|
36
54
|
get: (id, key) => blackboards.get(id)?.get(key),
|
|
37
55
|
set(id, key, value) {
|
|
@@ -80,7 +98,8 @@ export function createEntityStore() {
|
|
|
80
98
|
const id = options.id ?? generateId();
|
|
81
99
|
const position = toEntityPosition(options.position);
|
|
82
100
|
const rotationY = options.rotationY ?? 0;
|
|
83
|
-
|
|
101
|
+
spawnPoses.set(id, { position, rotationY });
|
|
102
|
+
writeEntity({
|
|
84
103
|
id,
|
|
85
104
|
name,
|
|
86
105
|
position,
|
|
@@ -94,12 +113,11 @@ export function createEntityStore() {
|
|
|
94
113
|
...(options.hidden === undefined ? {} : { hidden: options.hidden }),
|
|
95
114
|
meta: options.meta,
|
|
96
115
|
});
|
|
97
|
-
spawnPoses.set(id, { position, rotationY });
|
|
98
116
|
return id;
|
|
99
117
|
},
|
|
100
118
|
despawn(id) {
|
|
101
119
|
const existed = store.has(id);
|
|
102
|
-
|
|
120
|
+
deleteEntity(id);
|
|
103
121
|
spawnPoses.delete(id);
|
|
104
122
|
constraints.delete(id);
|
|
105
123
|
blackboards.delete(id);
|
|
@@ -109,6 +127,7 @@ export function createEntityStore() {
|
|
|
109
127
|
const current = store.get(id);
|
|
110
128
|
if (!current)
|
|
111
129
|
return false;
|
|
130
|
+
const previousBehaviors = current.behaviors;
|
|
112
131
|
if (patch.position !== undefined)
|
|
113
132
|
current.position = toEntityPosition(patch.position);
|
|
114
133
|
if (patch.name !== undefined)
|
|
@@ -129,7 +148,10 @@ export function createEntityStore() {
|
|
|
129
148
|
current.hidden = patch.hidden;
|
|
130
149
|
if (patch.meta !== undefined)
|
|
131
150
|
current.meta = patch.meta;
|
|
132
|
-
|
|
151
|
+
if (patch.behaviors === undefined)
|
|
152
|
+
store.set(id, current);
|
|
153
|
+
else
|
|
154
|
+
writeEntity(current, previousBehaviors);
|
|
133
155
|
return true;
|
|
134
156
|
},
|
|
135
157
|
setPoseConstraint(id, constraint) {
|
|
@@ -173,6 +195,15 @@ export function createEntityStore() {
|
|
|
173
195
|
store.set(id, current);
|
|
174
196
|
return true;
|
|
175
197
|
},
|
|
198
|
+
setVelocity(id, velocity) {
|
|
199
|
+
const current = store.get(id);
|
|
200
|
+
if (current === undefined || !Array.isArray(velocity) || velocity.length !== 3 ||
|
|
201
|
+
!velocity.every((component) => typeof component === "number" && Number.isFinite(component)))
|
|
202
|
+
return false;
|
|
203
|
+
current.velocity = [velocity[0], velocity[1], velocity[2]];
|
|
204
|
+
store.set(id, current);
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
176
207
|
get(id) {
|
|
177
208
|
return store.get(id) ?? null;
|
|
178
209
|
},
|
|
@@ -184,7 +215,7 @@ export function createEntityStore() {
|
|
|
184
215
|
},
|
|
185
216
|
clear() {
|
|
186
217
|
for (const entity of store.arraySnapshot()) {
|
|
187
|
-
|
|
218
|
+
deleteEntity(entity.id);
|
|
188
219
|
}
|
|
189
220
|
constraints.clear();
|
|
190
221
|
blackboards.clear();
|
|
@@ -195,6 +226,10 @@ export function createEntityStore() {
|
|
|
195
226
|
subscribeMembership(listener) {
|
|
196
227
|
return store.subscribeMembership(listener);
|
|
197
228
|
},
|
|
229
|
+
subscribeBehaviors(listener) {
|
|
230
|
+
behaviorListeners.add(listener);
|
|
231
|
+
return () => { behaviorListeners.delete(listener); };
|
|
232
|
+
},
|
|
198
233
|
snapshot() {
|
|
199
234
|
return store.arraySnapshot();
|
|
200
235
|
},
|
|
@@ -202,14 +237,14 @@ export function createEntityStore() {
|
|
|
202
237
|
const incoming = new Set(entities.map((entity) => entity.id));
|
|
203
238
|
for (const current of store.arraySnapshot()) {
|
|
204
239
|
if (!incoming.has(current.id)) {
|
|
205
|
-
|
|
240
|
+
deleteEntity(current.id);
|
|
206
241
|
spawnPoses.delete(current.id);
|
|
207
242
|
constraints.delete(current.id);
|
|
208
243
|
blackboards.delete(current.id);
|
|
209
244
|
}
|
|
210
245
|
}
|
|
211
246
|
for (const entity of entities) {
|
|
212
|
-
|
|
247
|
+
writeEntity({
|
|
213
248
|
id: entity.id,
|
|
214
249
|
name: entity.name,
|
|
215
250
|
position: entity.position,
|
|
@@ -220,6 +255,7 @@ export function createEntityStore() {
|
|
|
220
255
|
role: entity.role,
|
|
221
256
|
movement: entity.movement,
|
|
222
257
|
behaviors: entity.behaviors,
|
|
258
|
+
...(entity.hidden === undefined ? {} : { hidden: entity.hidden }),
|
|
223
259
|
meta: entity.meta,
|
|
224
260
|
});
|
|
225
261
|
if (!spawnPoses.has(entity.id)) {
|
package/dist/scene/mount.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface Occupant {
|
|
|
22
22
|
riderId: string;
|
|
23
23
|
seatId: string;
|
|
24
24
|
}
|
|
25
|
+
/** Occupancy only; register the world's rideables before restoring. */
|
|
26
|
+
export interface MountedRider extends SeatRef {
|
|
27
|
+
riderId: string;
|
|
28
|
+
}
|
|
25
29
|
export type MountResult = {
|
|
26
30
|
ok: true;
|
|
27
31
|
seat: MountSeat;
|
|
@@ -58,5 +62,10 @@ export declare class MountController {
|
|
|
58
62
|
* (they ride but do not steer), and the rider themselves when not mounted at all.
|
|
59
63
|
*/
|
|
60
64
|
driveTarget(riderId: string): string | null;
|
|
65
|
+
snapshot(): MountedRider[];
|
|
66
|
+
/** Replace occupancy atomically. Invalid, unknown, or duplicate claims leave it unchanged. */
|
|
67
|
+
restore(next: readonly MountedRider[]): void;
|
|
68
|
+
/** Release every rider, preserving authored registrations. */
|
|
69
|
+
reset(): void;
|
|
61
70
|
}
|
|
62
71
|
export declare function createMountController(): MountController;
|
package/dist/scene/mount.js
CHANGED
|
@@ -101,6 +101,41 @@ export class MountController {
|
|
|
101
101
|
const seat = this.mounts.get(ref.mountId)?.seats.find((s) => s.id === ref.seatId);
|
|
102
102
|
return seat?.control === true ? ref.mountId : null;
|
|
103
103
|
}
|
|
104
|
+
snapshot() {
|
|
105
|
+
return [...this.seatOfRider].map(([riderId, ref]) => ({ riderId, ...ref }));
|
|
106
|
+
}
|
|
107
|
+
/** Replace occupancy atomically. Invalid, unknown, or duplicate claims leave it unchanged. */
|
|
108
|
+
restore(next) {
|
|
109
|
+
const riders = new Set();
|
|
110
|
+
const claims = new Map();
|
|
111
|
+
if (!Array.isArray(next))
|
|
112
|
+
throw new Error("mount occupancy must be an array");
|
|
113
|
+
for (const row of next) {
|
|
114
|
+
if (row === null || typeof row !== "object" || typeof row.riderId !== "string" || row.riderId.length === 0 ||
|
|
115
|
+
typeof row.mountId !== "string" || typeof row.seatId !== "string" || riders.has(row.riderId)) {
|
|
116
|
+
throw new Error("invalid mount occupant");
|
|
117
|
+
}
|
|
118
|
+
const reg = this.mounts.get(row.mountId);
|
|
119
|
+
const occupied = claims.get(row.mountId) ?? new Set();
|
|
120
|
+
if (reg === undefined || !reg.seats.some((seat) => seat.id === row.seatId) || occupied.has(row.seatId)) {
|
|
121
|
+
throw new Error("invalid mount seat claim");
|
|
122
|
+
}
|
|
123
|
+
riders.add(row.riderId);
|
|
124
|
+
occupied.add(row.seatId);
|
|
125
|
+
claims.set(row.mountId, occupied);
|
|
126
|
+
}
|
|
127
|
+
this.reset();
|
|
128
|
+
for (const { riderId, mountId, seatId } of next) {
|
|
129
|
+
this.mounts.get(mountId).bySeat.set(seatId, riderId);
|
|
130
|
+
this.seatOfRider.set(riderId, { mountId, seatId });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Release every rider, preserving authored registrations. */
|
|
134
|
+
reset() {
|
|
135
|
+
this.seatOfRider.clear();
|
|
136
|
+
for (const reg of this.mounts.values())
|
|
137
|
+
reg.bySeat.clear();
|
|
138
|
+
}
|
|
104
139
|
}
|
|
105
140
|
export function createMountController() {
|
|
106
141
|
return new MountController();
|
|
@@ -25,6 +25,8 @@ export interface Possession {
|
|
|
25
25
|
own(userId: string, entityId: string): void;
|
|
26
26
|
disown(userId: string, entityId: string): void;
|
|
27
27
|
owns(userId: string, entityId: string): boolean;
|
|
28
|
+
/** Unique owner of an explicitly owned pawn, or undefined when absent or shared. Does not create ownership. */
|
|
29
|
+
ownerOf(entityId: string): string | undefined;
|
|
28
30
|
listOwned(userId: string): readonly string[];
|
|
29
31
|
active(userId: string): string;
|
|
30
32
|
possess(userId: string, entityId: string): {
|
package/dist/scene/possession.js
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
export function createPossession(deps) {
|
|
2
2
|
const owned = new Map();
|
|
3
3
|
const activeByUser = new Map();
|
|
4
|
+
const ownersByEntity = new Map();
|
|
5
|
+
function addOwner(userId, entityId) {
|
|
6
|
+
const owners = ownersByEntity.get(entityId) ?? new Set();
|
|
7
|
+
owners.add(userId);
|
|
8
|
+
ownersByEntity.set(entityId, owners);
|
|
9
|
+
}
|
|
4
10
|
function ownedSet(userId) {
|
|
5
11
|
let set = owned.get(userId);
|
|
6
12
|
if (set === undefined) {
|
|
7
13
|
set = new Set([userId]);
|
|
8
14
|
owned.set(userId, set);
|
|
15
|
+
addOwner(userId, userId);
|
|
9
16
|
}
|
|
10
17
|
return set;
|
|
11
18
|
}
|
|
12
19
|
return {
|
|
13
20
|
own(userId, entityId) {
|
|
14
21
|
ownedSet(userId).add(entityId);
|
|
22
|
+
addOwner(userId, entityId);
|
|
15
23
|
},
|
|
16
24
|
disown(userId, entityId) {
|
|
17
25
|
if (entityId === userId)
|
|
18
26
|
return;
|
|
19
27
|
ownedSet(userId).delete(entityId);
|
|
28
|
+
const owners = ownersByEntity.get(entityId);
|
|
29
|
+
owners?.delete(userId);
|
|
30
|
+
if (owners?.size === 0)
|
|
31
|
+
ownersByEntity.delete(entityId);
|
|
20
32
|
if (activeByUser.get(userId) === entityId)
|
|
21
33
|
activeByUser.delete(userId);
|
|
22
34
|
},
|
|
23
35
|
owns(userId, entityId) {
|
|
24
36
|
return ownedSet(userId).has(entityId);
|
|
25
37
|
},
|
|
38
|
+
ownerOf(entityId) {
|
|
39
|
+
const owners = ownersByEntity.get(entityId);
|
|
40
|
+
return owners?.size === 1 ? owners.values().next().value : undefined;
|
|
41
|
+
},
|
|
26
42
|
listOwned(userId) {
|
|
27
43
|
return Array.from(ownedSet(userId));
|
|
28
44
|
},
|
|
@@ -55,8 +71,12 @@ export function createPossession(deps) {
|
|
|
55
71
|
},
|
|
56
72
|
hydrateAll(state) {
|
|
57
73
|
owned.clear();
|
|
58
|
-
|
|
74
|
+
ownersByEntity.clear();
|
|
75
|
+
for (const [userId, ids] of Object.entries(state.owned)) {
|
|
59
76
|
owned.set(userId, new Set(ids));
|
|
77
|
+
for (const entityId of ids)
|
|
78
|
+
addOwner(userId, entityId);
|
|
79
|
+
}
|
|
60
80
|
activeByUser.clear();
|
|
61
81
|
for (const [userId, entityId] of Object.entries(state.active))
|
|
62
82
|
activeByUser.set(userId, entityId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityPosition } from "./entityStore.js";
|
|
2
2
|
import { type ColliderPurpose, type EntityColliderSet } from "./colliders.js";
|
|
3
3
|
import type { SceneObject } from "./objectStore.js";
|
|
4
|
+
import type { WorldSolids } from "../world/worldSolids.js";
|
|
4
5
|
export type SceneRaycastTargetKind = "entity" | "object" | "terrain" | "wall";
|
|
5
6
|
export interface SceneRaycastHit {
|
|
6
7
|
targetKind: SceneRaycastTargetKind;
|
|
@@ -18,11 +19,15 @@ export interface SceneRaycastFilter {
|
|
|
18
19
|
entities?: boolean;
|
|
19
20
|
objects?: boolean;
|
|
20
21
|
terrain?: boolean;
|
|
22
|
+
/** Wall segments and indexed world solids. */
|
|
21
23
|
walls?: boolean;
|
|
22
24
|
}
|
|
23
25
|
export interface SceneRaycastInput {
|
|
26
|
+
/** Finite world coordinates. */
|
|
24
27
|
origin: EntityPosition;
|
|
28
|
+
/** Finite direction and magnitude, normalized by the query. Zero direction returns no hits. */
|
|
25
29
|
direction: EntityPosition;
|
|
30
|
+
/** Finite, nonnegative segment length in world units. */
|
|
26
31
|
maxDistance: number;
|
|
27
32
|
excludeInstanceIds?: ReadonlySet<string> | readonly string[];
|
|
28
33
|
filter?: SceneRaycastFilter;
|
|
@@ -68,6 +73,8 @@ export interface SceneRaycastDeps {
|
|
|
68
73
|
objects?: SceneObjectQuerySource;
|
|
69
74
|
terrain?: TerrainRaycastSource;
|
|
70
75
|
walls?: readonly WallSegment[];
|
|
76
|
+
/** Indexed geometry returns physical, non-damage wall hits with instance id `world-solid`. */
|
|
77
|
+
solids?: Pick<WorldSolids, "inBox" | "inRay">;
|
|
71
78
|
}
|
|
72
79
|
export interface SceneRaycastApi {
|
|
73
80
|
raycast(input: SceneRaycastInput): SceneRaycastHit | null;
|
|
@@ -127,6 +127,20 @@ function wallAabb(wall) {
|
|
|
127
127
|
center: [(ax + bx) / 2, yCenter, (az + bz) / 2],
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
function rayHitsWorldSolid(solid, origin, direction, maxDistance) {
|
|
131
|
+
const cos = Math.cos(solid.rotationY ?? 0);
|
|
132
|
+
const sin = Math.sin(solid.rotationY ?? 0);
|
|
133
|
+
const x = origin[0] - solid.center[0];
|
|
134
|
+
const z = origin[2] - solid.center[2];
|
|
135
|
+
const [hx, hy, hz] = solid.halfExtents;
|
|
136
|
+
const hit = intersectAabb([x * cos - z * sin, origin[1] - solid.center[1], x * sin + z * cos], [direction[0] * cos - direction[2] * sin, direction[1], direction[0] * sin + direction[2] * cos], [-hx, -hy, -hz], [hx, hy, hz], maxDistance);
|
|
137
|
+
if (hit === null)
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
distance: hit.distance,
|
|
141
|
+
normal: [hit.normal[0] * cos + hit.normal[2] * sin, hit.normal[1], -hit.normal[0] * sin + hit.normal[2] * cos],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
130
144
|
function rayHitsTerrain(origin, direction, maxDistance, terrain) {
|
|
131
145
|
const steps = Math.max(8, Math.ceil(maxDistance * 2));
|
|
132
146
|
const step = maxDistance / steps;
|
|
@@ -208,6 +222,11 @@ function candidateObjects(objects, origin, direction, maxDistance) {
|
|
|
208
222
|
return objects.list();
|
|
209
223
|
}
|
|
210
224
|
function gatherHits(deps, input) {
|
|
225
|
+
if (!input.origin.every(Number.isFinite) || !input.direction.every(Number.isFinite) || !Number.isFinite(input.maxDistance) || input.maxDistance < 0) {
|
|
226
|
+
throw new RangeError("Scene rays require finite coordinates, direction and nonnegative distance");
|
|
227
|
+
}
|
|
228
|
+
if (!Number.isFinite(Math.hypot(...input.direction)))
|
|
229
|
+
throw new RangeError("Scene ray direction length must be finite");
|
|
211
230
|
const direction = normalizeDirection(input.direction);
|
|
212
231
|
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
213
232
|
return [];
|
|
@@ -258,6 +277,42 @@ function gatherHits(deps, input) {
|
|
|
258
277
|
});
|
|
259
278
|
}
|
|
260
279
|
}
|
|
280
|
+
if (enabled(input.filter, "walls") && deps.solids !== undefined && !excluded(input.excludeInstanceIds, "world-solid")) {
|
|
281
|
+
let candidates;
|
|
282
|
+
if (deps.solids.inRay !== undefined) {
|
|
283
|
+
candidates = deps.solids.inRay(origin, direction, maxDistance);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const end = [
|
|
287
|
+
origin[0] + direction[0] * maxDistance,
|
|
288
|
+
origin[1] + direction[1] * maxDistance,
|
|
289
|
+
origin[2] + direction[2] * maxDistance,
|
|
290
|
+
];
|
|
291
|
+
const min = [Math.min(origin[0], end[0]), Math.min(origin[1], end[1]), Math.min(origin[2], end[2])];
|
|
292
|
+
const max = [Math.max(origin[0], end[0]), Math.max(origin[1], end[1]), Math.max(origin[2], end[2])];
|
|
293
|
+
candidates = deps.solids.inBox(min, max);
|
|
294
|
+
}
|
|
295
|
+
for (const solid of candidates) {
|
|
296
|
+
const hit = rayHitsWorldSolid(solid, origin, direction, maxDistance);
|
|
297
|
+
if (hit === null)
|
|
298
|
+
continue;
|
|
299
|
+
hits.push({
|
|
300
|
+
targetKind: "wall",
|
|
301
|
+
instanceId: "world-solid",
|
|
302
|
+
colliderName: "solid",
|
|
303
|
+
purpose: "physical",
|
|
304
|
+
damageEligible: false,
|
|
305
|
+
blocks: true,
|
|
306
|
+
distance: hit.distance,
|
|
307
|
+
normal: hit.normal,
|
|
308
|
+
point: [
|
|
309
|
+
origin[0] + direction[0] * hit.distance,
|
|
310
|
+
origin[1] + direction[1] * hit.distance,
|
|
311
|
+
origin[2] + direction[2] * hit.distance,
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
261
316
|
if (enabled(input.filter, "terrain") && deps.terrain !== undefined) {
|
|
262
317
|
const terrainHit = rayHitsTerrain(origin, direction, maxDistance, deps.terrain);
|
|
263
318
|
if (terrainHit !== null)
|
package/dist/scene/spatial.d.ts
CHANGED
|
@@ -38,11 +38,20 @@ export interface SpatialApiOptions {
|
|
|
38
38
|
/**
|
|
39
39
|
* Broadphase for `inRadius`/`queryArc`. Defaults to `{ cellSize: 8 }`. Pass `false` to force a
|
|
40
40
|
* linear scan. When enabled, the index rebuilds lazily on first use after `invalidate()` or when
|
|
41
|
-
* `getVersion()` advances — call `
|
|
41
|
+
* `getVersion()` advances — call `updateEntity(id)` after each committed move, or `invalidate()`
|
|
42
|
+
* after an unreported batch, unless a version source is wired.
|
|
42
43
|
*/
|
|
43
44
|
grid?: SpatialGridOptions | false;
|
|
44
45
|
/** When this number changes between queries, the grid rebuilds without an explicit `invalidate()`. */
|
|
45
46
|
getVersion?: () => number;
|
|
47
|
+
/** Opt in only when every candidate add/remove/position change calls `updateEntity(id)` or
|
|
48
|
+
* `invalidate()`. Avoids scanning all candidate IDs on warm queries. Initially unresolved IDs
|
|
49
|
+
* still cost one position lookup per query until updated or invalidated. Default false retains
|
|
50
|
+
* discovery of new candidates without notification. If positions also resolve outside the
|
|
51
|
+
* candidate set, pass its explicit presence flag to `updateEntity`. Rebuilds are deferred to
|
|
52
|
+
* the first query.
|
|
53
|
+
*/
|
|
54
|
+
incremental?: boolean;
|
|
46
55
|
/**
|
|
47
56
|
* Slide a horizontal step against the scene's solid geometry, returning the step actually allowed.
|
|
48
57
|
* Wired by the runtime to the same blocking-collider query the player resolver reads, so a chaser
|
|
@@ -61,6 +70,16 @@ export interface SpatialApi {
|
|
|
61
70
|
queryArc(options: QueryArcOptions): string[];
|
|
62
71
|
moveToward(instanceId: string, target: EntityPosition | string, options: MoveTowardOptions): EntityPosition | null;
|
|
63
72
|
invalidate(): void;
|
|
73
|
+
/** Notify after committing a candidate's position or membership. Pass `false` to remove an ID
|
|
74
|
+
* even if its position still resolves, or `true` for a present candidate with a late position.
|
|
75
|
+
* Omitted presence asserts membership when the position resolves, otherwise removes the entry;
|
|
76
|
+
* subset candidate sets with a broader position resolver must pass explicit presence.
|
|
77
|
+
* Preserves candidate order; does not build an invalidated/cold index or enumerate candidates.
|
|
78
|
+
*/
|
|
79
|
+
updateEntity(instanceId: string, candidatePresent?: boolean): void;
|
|
64
80
|
}
|
|
65
81
|
export declare function distanceBetween(a: EntityPosition, b: EntityPosition): number;
|
|
82
|
+
/** Spatial queries with an optional notified incremental broadphase.
|
|
83
|
+
* @capability spatial-incremental Update one entity's spatial cell after a committed write without scanning the population
|
|
84
|
+
*/
|
|
66
85
|
export declare function createSpatialApi(options: SpatialApiOptions): SpatialApi;
|
package/dist/scene/spatial.js
CHANGED
|
@@ -22,6 +22,10 @@ function packCell(cx, cz) {
|
|
|
22
22
|
return ((cx + CELL_KEY_OFFSET) << 16) | ((cz + CELL_KEY_OFFSET) & 0xffff);
|
|
23
23
|
}
|
|
24
24
|
const DEFAULT_CELL_SIZE = 8;
|
|
25
|
+
const MEMBERSHIP_REBUILD_THRESHOLD = 256;
|
|
26
|
+
/** Spatial queries with an optional notified incremental broadphase.
|
|
27
|
+
* @capability spatial-incremental Update one entity's spatial cell after a committed write without scanning the population
|
|
28
|
+
*/
|
|
25
29
|
export function createSpatialApi(options) {
|
|
26
30
|
const { resolvePosition, candidates, occluder, getVersion, resolveStep } = options;
|
|
27
31
|
const gridConfig = options.grid === false ? undefined : (options.grid ?? { cellSize: DEFAULT_CELL_SIZE });
|
|
@@ -29,10 +33,14 @@ export function createSpatialApi(options) {
|
|
|
29
33
|
let gridIndex = null;
|
|
30
34
|
let gridDirty = true;
|
|
31
35
|
let lastVersion;
|
|
36
|
+
let membershipChanges = 0;
|
|
32
37
|
function resolveTarget(target) {
|
|
33
38
|
return typeof target === "string" ? resolvePosition(target) : target;
|
|
34
39
|
}
|
|
35
40
|
function takeBucket(index) {
|
|
41
|
+
const recycled = index.freeBuckets.pop();
|
|
42
|
+
if (recycled !== undefined)
|
|
43
|
+
return recycled;
|
|
36
44
|
if (index.poolCursor < index.bucketPool.length) {
|
|
37
45
|
const bucket = index.bucketPool[index.poolCursor];
|
|
38
46
|
index.poolCursor += 1;
|
|
@@ -52,8 +60,10 @@ export function createSpatialApi(options) {
|
|
|
52
60
|
gridDirty = true;
|
|
53
61
|
lastVersion = version;
|
|
54
62
|
}
|
|
55
|
-
if (!gridDirty && gridIndex !== null)
|
|
63
|
+
if (!gridDirty && gridIndex !== null) {
|
|
64
|
+
membershipChanges = 0;
|
|
56
65
|
return gridIndex;
|
|
66
|
+
}
|
|
57
67
|
const previous = gridIndex;
|
|
58
68
|
const cells = previous?.cells ?? new Map();
|
|
59
69
|
cells.clear();
|
|
@@ -64,13 +74,27 @@ export function createSpatialApi(options) {
|
|
|
64
74
|
indexed,
|
|
65
75
|
bucketPool: previous?.bucketPool ?? [],
|
|
66
76
|
poolCursor: 0,
|
|
77
|
+
locations: previous?.locations ?? new Map(),
|
|
78
|
+
order: previous?.order ?? new Map(),
|
|
79
|
+
nextOrder: 0,
|
|
80
|
+
unresolved: previous?.unresolved ?? new Set(),
|
|
81
|
+
freeBuckets: [],
|
|
67
82
|
};
|
|
83
|
+
index.locations.clear();
|
|
84
|
+
index.order.clear();
|
|
85
|
+
index.unresolved.clear();
|
|
68
86
|
for (const instanceId of candidates()) {
|
|
87
|
+
if (index.order.has(instanceId))
|
|
88
|
+
continue;
|
|
89
|
+
index.order.set(instanceId, index.nextOrder++);
|
|
69
90
|
const position = resolvePosition(instanceId);
|
|
70
|
-
if (position === undefined)
|
|
91
|
+
if (position === undefined) {
|
|
92
|
+
index.unresolved.add(instanceId);
|
|
71
93
|
continue;
|
|
94
|
+
}
|
|
72
95
|
indexed.add(instanceId);
|
|
73
96
|
const key = packCell(cellCoord(position[0], cellSize), cellCoord(position[2], cellSize));
|
|
97
|
+
index.locations.set(instanceId, key);
|
|
74
98
|
let bucket = cells.get(key);
|
|
75
99
|
if (bucket === undefined) {
|
|
76
100
|
bucket = takeBucket(index);
|
|
@@ -80,8 +104,70 @@ export function createSpatialApi(options) {
|
|
|
80
104
|
}
|
|
81
105
|
gridIndex = index;
|
|
82
106
|
gridDirty = false;
|
|
107
|
+
membershipChanges = 0;
|
|
83
108
|
return index;
|
|
84
109
|
}
|
|
110
|
+
function updateEntity(instanceId, candidatePresent) {
|
|
111
|
+
const index = gridIndex;
|
|
112
|
+
if (index === null || gridDirty || cellSize === undefined)
|
|
113
|
+
return;
|
|
114
|
+
const position = candidatePresent === false ? undefined : resolvePosition(instanceId);
|
|
115
|
+
const present = candidatePresent ?? (position !== undefined);
|
|
116
|
+
const oldKey = index.locations.get(instanceId);
|
|
117
|
+
const newKey = position === undefined ? undefined
|
|
118
|
+
: packCell(cellCoord(position[0], cellSize), cellCoord(position[2], cellSize));
|
|
119
|
+
const had = index.order.has(instanceId);
|
|
120
|
+
if (had !== present) {
|
|
121
|
+
membershipChanges += 1;
|
|
122
|
+
if (membershipChanges > MEMBERSHIP_REBUILD_THRESHOLD) {
|
|
123
|
+
gridDirty = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (oldKey !== undefined && oldKey !== newKey) {
|
|
128
|
+
const bucket = index.cells.get(oldKey);
|
|
129
|
+
bucket.splice(bucket.indexOf(instanceId), 1);
|
|
130
|
+
if (bucket.length === 0) {
|
|
131
|
+
index.cells.delete(oldKey);
|
|
132
|
+
index.freeBuckets.push(bucket);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
index.unresolved.delete(instanceId);
|
|
136
|
+
if (newKey === undefined) {
|
|
137
|
+
index.indexed.delete(instanceId);
|
|
138
|
+
index.locations.delete(instanceId);
|
|
139
|
+
if (present) {
|
|
140
|
+
if (!had)
|
|
141
|
+
index.order.set(instanceId, index.nextOrder++);
|
|
142
|
+
index.unresolved.add(instanceId);
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
index.order.delete(instanceId);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!had)
|
|
149
|
+
index.order.set(instanceId, index.nextOrder++);
|
|
150
|
+
index.indexed.add(instanceId);
|
|
151
|
+
index.locations.set(instanceId, newKey);
|
|
152
|
+
if (oldKey === newKey)
|
|
153
|
+
return;
|
|
154
|
+
let bucket = index.cells.get(newKey);
|
|
155
|
+
if (bucket === undefined) {
|
|
156
|
+
bucket = takeBucket(index);
|
|
157
|
+
index.cells.set(newKey, bucket);
|
|
158
|
+
}
|
|
159
|
+
const ordinal = index.order.get(instanceId);
|
|
160
|
+
let low = 0;
|
|
161
|
+
let high = bucket.length;
|
|
162
|
+
while (low < high) {
|
|
163
|
+
const mid = (low + high) >>> 1;
|
|
164
|
+
if (index.order.get(bucket[mid]) < ordinal)
|
|
165
|
+
low = mid + 1;
|
|
166
|
+
else
|
|
167
|
+
high = mid;
|
|
168
|
+
}
|
|
169
|
+
bucket.splice(low, 0, instanceId);
|
|
170
|
+
}
|
|
85
171
|
function collectNear(index, centerX, centerZ, radius, out) {
|
|
86
172
|
const size = cellSize;
|
|
87
173
|
const cxMin = cellCoord(centerX - radius, size);
|
|
@@ -109,6 +195,11 @@ export function createSpatialApi(options) {
|
|
|
109
195
|
const nearScratch = [];
|
|
110
196
|
const resultScratch = [];
|
|
111
197
|
function appendUnindexed(index, out) {
|
|
198
|
+
if (options.incremental === true) {
|
|
199
|
+
for (const instanceId of index.unresolved)
|
|
200
|
+
out.push(instanceId);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
112
203
|
for (const instanceId of candidates()) {
|
|
113
204
|
if (index.indexed.has(instanceId))
|
|
114
205
|
continue;
|
|
@@ -130,6 +221,7 @@ export function createSpatialApi(options) {
|
|
|
130
221
|
return resultScratch.slice();
|
|
131
222
|
}
|
|
132
223
|
return {
|
|
224
|
+
updateEntity,
|
|
133
225
|
distance(aInstanceId, bInstanceId) {
|
|
134
226
|
const a = resolvePosition(aInstanceId);
|
|
135
227
|
const b = resolvePosition(bInstanceId);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { MountController, type MountSeat, type RideableConfig } from "./mount.js";
|
|
1
|
+
import { MountController, type MountedRider, type MountSeat, type RideableConfig } from "./mount.js";
|
|
2
|
+
import type { GameContext } from "../runtime/gameContext.js";
|
|
3
|
+
import type { CameraRigKind } from "../game/cameraConfig.js";
|
|
4
|
+
import { type CameraRigConfig, type ChaseCameraTuning } from "../runtime/cameraDirector.js";
|
|
2
5
|
/** World-space `[x, y, z]` for a vehicle's current position. */
|
|
3
6
|
export type VehiclePosition = readonly [number, number, number];
|
|
4
7
|
/** A vehicle's world-space position and heading, used for {@link VehicleSeats.exit}'s dismount placement math. */
|
|
@@ -38,7 +41,7 @@ export type ExitVehicleResult = {
|
|
|
38
41
|
riderMovementPatch: RiderMovementPatch;
|
|
39
42
|
} | {
|
|
40
43
|
ok: false;
|
|
41
|
-
reason: "not_seated";
|
|
44
|
+
reason: "not_seated" | "invalid_pose";
|
|
42
45
|
};
|
|
43
46
|
/** Where {@link VehicleSeats.exit} steps the rider out, relative to the vehicle's heading. */
|
|
44
47
|
export interface DismountOffset {
|
|
@@ -62,6 +65,11 @@ export declare class VehicleSeats {
|
|
|
62
65
|
exit(riderId: string, vehiclePose: VehiclePose, offset?: DismountOffset): ExitVehicleResult;
|
|
63
66
|
isSeated(riderId: string): boolean;
|
|
64
67
|
driverOf(vehicleId: string): string | null;
|
|
68
|
+
/** The seated rider's controlled mount; `null` for passengers and unseated riders. */
|
|
69
|
+
drivenBy(riderId: string): string | null;
|
|
70
|
+
snapshot(): MountedRider[];
|
|
71
|
+
restore(next: readonly MountedRider[]): void;
|
|
72
|
+
reset(): void;
|
|
65
73
|
get mounts(): MountController;
|
|
66
74
|
}
|
|
67
75
|
/**
|
|
@@ -70,3 +78,29 @@ export declare class VehicleSeats {
|
|
|
70
78
|
* @capability vehicle-seats enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
|
|
71
79
|
*/
|
|
72
80
|
export declare function createVehicleSeats(controller?: MountController): VehicleSeats;
|
|
81
|
+
/** Seat selection and optional local camera policy for an explicit rider transition. */
|
|
82
|
+
export interface BoardVehicleOptions extends EnterVehicleOptions {
|
|
83
|
+
/** Presentation policy; `false` applies only rider/occupancy patches for authoritative remote riders. */
|
|
84
|
+
camera?: boolean;
|
|
85
|
+
/** Omit to keep the current rig. `null` restores the configured rig. */
|
|
86
|
+
rig?: CameraRigKind | null;
|
|
87
|
+
config?: CameraRigConfig;
|
|
88
|
+
chase?: ChaseCameraTuning | null;
|
|
89
|
+
}
|
|
90
|
+
/** Exit placement and optional local camera restoration for an explicit rider. */
|
|
91
|
+
export interface LeaveVehicleOptions extends DismountOffset {
|
|
92
|
+
camera?: boolean;
|
|
93
|
+
/** Supply a last known vehicle pose when its entity has already been removed. */
|
|
94
|
+
pose?: VehiclePose;
|
|
95
|
+
}
|
|
96
|
+
/** Apply an existing seat transition to the explicitly named rider. Motor/input policy stays caller-owned.
|
|
97
|
+
* @capability board-vehicle board a rideable, freeze/hide its rider and apply caller camera policy
|
|
98
|
+
*/
|
|
99
|
+
export declare function boardVehicle(ctx: GameContext, seats: VehicleSeats, riderId: string, vehicleId: string, options?: BoardVehicleOptions): EnterVehicleResult;
|
|
100
|
+
/** Apply dismount placement, restore rider movement/visibility and the configured camera rig.
|
|
101
|
+
* @capability leave-vehicle leave a rideable beside its current pose and restore on-foot camera/movement
|
|
102
|
+
*/
|
|
103
|
+
export declare function leaveVehicle(ctx: GameContext, seats: VehicleSeats, riderId: string, options?: LeaveVehicleOptions): ExitVehicleResult | {
|
|
104
|
+
ok: false;
|
|
105
|
+
reason: "unknown_rider" | "unknown_vehicle";
|
|
106
|
+
};
|