@jgengine/core 0.8.0 → 0.10.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 +36 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.d.ts +13 -0
- package/dist/ai/spawnDirector.js +41 -17
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +214 -129
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +52 -0
- package/dist/combat/shotOrigin.js +86 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +44 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +25 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +14 -1
- package/dist/game/lootTable.js +28 -7
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +116 -4
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +27 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +33 -3
- package/dist/input/axisInput.js +60 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +44 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/movementModel.d.ts +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +72 -8
- package/dist/runtime/gameContext.js +374 -61
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +18 -0
- package/dist/runtime/inputSnapshot.js +7 -0
- package/dist/runtime/motionIntents.d.ts +16 -6
- package/dist/runtime/motionIntents.js +31 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +50 -0
- package/dist/scene/colliders.js +101 -0
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +51 -5
- package/dist/scene/entityStore.js +119 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +17 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +212 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +21 -4
- package/dist/world/environmentSummary.js +22 -3
- package/dist/world/features.d.ts +185 -8
- package/dist/world/features.js +64 -6
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +69 -3
- package/dist/world/terrain.js +309 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +3013 -1726
- package/package.json +12 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
|
|
2
|
+
export const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
3
|
+
/** Matches the shell's fallback actor capsule (~0.7m wide, feet at y=0 to head at ~1.8m). */
|
|
4
|
+
export const DEFAULT_ENTITY_BODY_HALF_EXTENTS = [0.35, 0.9, 0.35];
|
|
5
|
+
/** Entity-local center of the default body hitbox (half its height above the feet). */
|
|
6
|
+
export const DEFAULT_ENTITY_BODY_OFFSET = [0, 0.9, 0];
|
|
7
|
+
export function defaultEntityColliders() {
|
|
8
|
+
return {
|
|
9
|
+
hitboxes: [
|
|
10
|
+
{
|
|
11
|
+
name: "body",
|
|
12
|
+
purpose: "damage",
|
|
13
|
+
shape: {
|
|
14
|
+
kind: "aabb",
|
|
15
|
+
halfExtents: DEFAULT_ENTITY_BODY_HALF_EXTENTS,
|
|
16
|
+
offset: DEFAULT_ENTITY_BODY_OFFSET,
|
|
17
|
+
},
|
|
18
|
+
damageEligible: true,
|
|
19
|
+
blocks: false,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function defaultObjectColliders(halfExtents = DEFAULT_OBJECT_HALF_EXTENTS) {
|
|
25
|
+
return {
|
|
26
|
+
body: {
|
|
27
|
+
name: "body",
|
|
28
|
+
purpose: "physical",
|
|
29
|
+
shape: { kind: "aabb", halfExtents },
|
|
30
|
+
damageEligible: false,
|
|
31
|
+
blocks: true,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Blocking physical body derived from an object's rendered scale: a grounded box spanning the visual (base at y=0, matching the shell's fallback mesh). */
|
|
36
|
+
export function scaledObjectColliders(scale) {
|
|
37
|
+
return {
|
|
38
|
+
body: {
|
|
39
|
+
name: "body",
|
|
40
|
+
purpose: "physical",
|
|
41
|
+
shape: {
|
|
42
|
+
kind: "aabb",
|
|
43
|
+
halfExtents: [scale[0] / 2, scale[1] / 2, scale[2] / 2],
|
|
44
|
+
offset: [0, scale[1] / 2, 0],
|
|
45
|
+
},
|
|
46
|
+
damageEligible: false,
|
|
47
|
+
blocks: true,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function resolveColliders(set) {
|
|
52
|
+
if (set === null || set === undefined)
|
|
53
|
+
return [];
|
|
54
|
+
const out = [];
|
|
55
|
+
if (set.body !== undefined)
|
|
56
|
+
out.push(resolveOne(set.body));
|
|
57
|
+
if (set.hitboxes !== undefined) {
|
|
58
|
+
for (const hitbox of set.hitboxes)
|
|
59
|
+
out.push(resolveOne(hitbox));
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function resolveOne(def) {
|
|
64
|
+
const damageEligible = def.damageEligible ?? def.purpose === "damage";
|
|
65
|
+
const blocks = def.blocks ?? def.purpose === "physical";
|
|
66
|
+
return {
|
|
67
|
+
name: def.name,
|
|
68
|
+
purpose: def.purpose,
|
|
69
|
+
shape: def.shape,
|
|
70
|
+
damageEligible,
|
|
71
|
+
blocks,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function worldOffset(local, position, rotationY) {
|
|
75
|
+
if (local === undefined)
|
|
76
|
+
return position;
|
|
77
|
+
const cos = Math.cos(rotationY);
|
|
78
|
+
const sin = Math.sin(rotationY);
|
|
79
|
+
const lx = local[0];
|
|
80
|
+
const ly = local[1];
|
|
81
|
+
const lz = local[2];
|
|
82
|
+
return [position[0] + lx * cos + lz * sin, position[1] + ly, position[2] - lx * sin + lz * cos];
|
|
83
|
+
}
|
|
84
|
+
export function colliderWorldCenter(collider, position, rotationY) {
|
|
85
|
+
return worldOffset(collider.shape.offset, position, rotationY);
|
|
86
|
+
}
|
|
87
|
+
export function colliderBounds(collider, position, rotationY) {
|
|
88
|
+
const center = colliderWorldCenter(collider, position, rotationY);
|
|
89
|
+
if (collider.shape.kind === "sphere") {
|
|
90
|
+
const r = collider.shape.radius;
|
|
91
|
+
return {
|
|
92
|
+
min: [center[0] - r, center[1] - r, center[2] - r],
|
|
93
|
+
max: [center[0] + r, center[1] + r, center[2] + r],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const h = collider.shape.halfExtents;
|
|
97
|
+
return {
|
|
98
|
+
min: [center[0] - h[0], center[1] - h[1], center[2] - h[2]],
|
|
99
|
+
max: [center[0] + h[0], center[1] + h[1], center[2] + h[2]],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -28,6 +28,10 @@ export declare function getStatValue(map: StatValueMap, statId: string): StatVal
|
|
|
28
28
|
export declare function setStatValue(map: StatValueMap, statId: string, patch: StatValuePatch): StatValueMap;
|
|
29
29
|
export declare function applyPoolDelta(map: StatValueMap, statId: string, amount: number): PoolDeltaResult;
|
|
30
30
|
export declare function seedStatValues(catalogStats: StatCatalog): StatValueMap;
|
|
31
|
+
/** Deep-copy the per-entity stat maps into a serializable record — the transport counterpart of {@link hydrateEntityStats}. */
|
|
32
|
+
export declare function snapshotEntityStats(store: ReadonlyMap<string, StatValueMap>): Record<string, StatValueMap>;
|
|
33
|
+
/** Replace a live stat store with deep copies of a snapshot, clearing entities absent from it. */
|
|
34
|
+
export declare function hydrateEntityStats(store: Map<string, StatValueMap>, data: Record<string, StatValueMap>): void;
|
|
31
35
|
export interface EntityStatsApi {
|
|
32
36
|
get(instanceId: string, statId: string): StatValue | null;
|
|
33
37
|
set(instanceId: string, statId: string, patch: StatValuePatch): boolean;
|
|
@@ -35,6 +35,27 @@ export function seedStatValues(catalogStats) {
|
|
|
35
35
|
}
|
|
36
36
|
return map;
|
|
37
37
|
}
|
|
38
|
+
/** Deep-copy the per-entity stat maps into a serializable record — the transport counterpart of {@link hydrateEntityStats}. */
|
|
39
|
+
export function snapshotEntityStats(store) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [instanceId, map] of store) {
|
|
42
|
+
const copy = {};
|
|
43
|
+
for (const [statId, value] of Object.entries(map))
|
|
44
|
+
copy[statId] = { ...value };
|
|
45
|
+
out[instanceId] = copy;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Replace a live stat store with deep copies of a snapshot, clearing entities absent from it. */
|
|
50
|
+
export function hydrateEntityStats(store, data) {
|
|
51
|
+
store.clear();
|
|
52
|
+
for (const [instanceId, map] of Object.entries(data)) {
|
|
53
|
+
const copy = {};
|
|
54
|
+
for (const [statId, value] of Object.entries(map))
|
|
55
|
+
copy[statId] = { ...value };
|
|
56
|
+
store.set(instanceId, copy);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
38
59
|
export function createEntityStatsApi(resolve) {
|
|
39
60
|
return {
|
|
40
61
|
get(instanceId, statId) {
|
|
@@ -10,7 +10,7 @@ export type SpawnPositionInput = EntityPosition | {
|
|
|
10
10
|
y: number;
|
|
11
11
|
z: number;
|
|
12
12
|
};
|
|
13
|
-
export interface SceneEntity<TMeta =
|
|
13
|
+
export interface SceneEntity<TMeta = unknown> {
|
|
14
14
|
id: string;
|
|
15
15
|
name: string;
|
|
16
16
|
position: EntityPosition;
|
|
@@ -22,12 +22,13 @@ export interface SceneEntity<TMeta = undefined> {
|
|
|
22
22
|
role: EntityRole;
|
|
23
23
|
movement: EntityMovement;
|
|
24
24
|
behaviors: readonly BehaviorDescriptor[];
|
|
25
|
+
/** Game-defined per-instance data set at `spawn`/`update` and carried to `renderEntity`/queries — narrow with a cast or type guard on read (#286.1); never encode state in the id/name string. */
|
|
25
26
|
meta: TMeta;
|
|
26
27
|
}
|
|
27
28
|
/** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
|
|
28
29
|
export declare function groundSpeed(entity: SceneEntity<unknown>): number;
|
|
29
30
|
export declare function movedWhileFrozen(entity: SceneEntity<unknown>, threshold?: number): boolean;
|
|
30
|
-
export interface SpawnOptions<TMeta =
|
|
31
|
+
export interface SpawnOptions<TMeta = unknown> {
|
|
31
32
|
id?: string;
|
|
32
33
|
position?: SpawnPositionInput;
|
|
33
34
|
rotationY?: number;
|
|
@@ -37,6 +38,8 @@ export interface SpawnOptions<TMeta = undefined> {
|
|
|
37
38
|
movement?: EntityMovement;
|
|
38
39
|
behaviors?: readonly BehaviorDescriptor[];
|
|
39
40
|
meta?: TMeta;
|
|
41
|
+
/** When `id` is already spawned: `"throw"` (default), `"replace"` (fresh spawn over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
|
|
42
|
+
onExisting?: "throw" | "replace" | "keep";
|
|
40
43
|
}
|
|
41
44
|
export interface EntityPose {
|
|
42
45
|
position: SpawnPositionInput;
|
|
@@ -50,17 +53,60 @@ export interface SpawnPose {
|
|
|
50
53
|
position: EntityPosition;
|
|
51
54
|
rotationY: number;
|
|
52
55
|
}
|
|
53
|
-
export interface
|
|
56
|
+
export interface PoseConstraintFrame {
|
|
57
|
+
entityId: string;
|
|
58
|
+
current: EntityPosition;
|
|
59
|
+
next: EntityPosition;
|
|
60
|
+
dt?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Return a replacement position to constrain the step, or nothing to accept it. */
|
|
63
|
+
export type PoseConstraint = (frame: PoseConstraintFrame) => readonly [number, number, number] | undefined | void;
|
|
64
|
+
export type EntityUpdatePatch<TMeta = unknown> = Partial<Pick<SceneEntity<TMeta>, "name" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">> & {
|
|
65
|
+
/** Accepts the same friendly shapes as `spawn`/`setPose` (#286.13). Raw patch semantics — velocity is not derived; use `setPose` with `dt` for that. */
|
|
66
|
+
position?: SpawnPositionInput;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Per-entity scratch and cooldown timers, auto-cleared on despawn (#533.8) — the home for AI state
|
|
70
|
+
* (next-shot-at, alert level) that would otherwise be smuggled through the serializable `meta` or a
|
|
71
|
+
* hand-pruned module-level map. Keys share one namespace per entity: `arm`/`ready`/`remaining` store a
|
|
72
|
+
* deadline under a key, `get`/`set` store arbitrary scratch.
|
|
73
|
+
*/
|
|
74
|
+
export interface EntityBlackboard {
|
|
75
|
+
/** Read a scratch value; `undefined` if unset or the entity has despawned. */
|
|
76
|
+
get<T>(id: string, key: string): T | undefined;
|
|
77
|
+
/** Write a scratch value; does not notify subscribers, and clears when the entity despawns. */
|
|
78
|
+
set(id: string, key: string, value: unknown): void;
|
|
79
|
+
has(id: string, key: string): boolean;
|
|
80
|
+
delete(id: string, key: string): void;
|
|
81
|
+
/** Drop all scratch and timers for one entity. */
|
|
82
|
+
clear(id: string): void;
|
|
83
|
+
/** Arm a cooldown for `key` that stays busy until absolute time `untilMs`. */
|
|
84
|
+
arm(id: string, key: string, untilMs: number): void;
|
|
85
|
+
/** True when no cooldown is armed for `key`, or its deadline has passed. */
|
|
86
|
+
ready(id: string, key: string, nowMs: number): boolean;
|
|
87
|
+
/** Milliseconds until the armed cooldown elapses; 0 when ready. */
|
|
88
|
+
remaining(id: string, key: string, nowMs: number): number;
|
|
89
|
+
}
|
|
90
|
+
export interface EntityStore<TMeta = unknown> {
|
|
54
91
|
spawn(name: string, options?: SpawnOptions<TMeta>): string;
|
|
55
92
|
despawn(id: string): boolean;
|
|
56
|
-
update(id: string, patch:
|
|
93
|
+
update(id: string, patch: EntityUpdatePatch<TMeta>): boolean;
|
|
57
94
|
setPose(id: string, pose: EntityPose): boolean;
|
|
95
|
+
/** Register a constraint applied inside every `setPose` for this entity — the self-driven sibling of the shell's `beforeCommit` (#282.9): nav clamps, corridor walls, arena bounds without wrapping every call site. `null` clears; despawn clears automatically. */
|
|
96
|
+
setPoseConstraint(id: string, constraint: PoseConstraint | null): void;
|
|
58
97
|
get(id: string): SceneEntity<TMeta> | null;
|
|
59
98
|
list(): readonly SceneEntity<TMeta>[];
|
|
60
99
|
clear(): void;
|
|
61
100
|
subscribe(listener: () => void): () => void;
|
|
62
101
|
snapshot(): readonly SceneEntity<TMeta>[];
|
|
102
|
+
/**
|
|
103
|
+
* Apply a `snapshot()` (e.g. from an authoritative host) to this store — the counterpart of `snapshot()`
|
|
104
|
+
* for host→client world mirroring: spawns entities present in the snapshot but not here, overwrites
|
|
105
|
+
* existing ones field-for-field (including velocity), and despawns any local entity absent from it.
|
|
106
|
+
*/
|
|
107
|
+
hydrate(entities: readonly SceneEntity<TMeta>[]): void;
|
|
63
108
|
spawnPoseOf(id: string): SpawnPose | null;
|
|
64
109
|
resetToSpawn(id: string): boolean;
|
|
110
|
+
blackboard: EntityBlackboard;
|
|
65
111
|
}
|
|
66
|
-
export declare function createEntityStore<TMeta =
|
|
112
|
+
export declare function createEntityStore<TMeta = unknown>(): EntityStore<TMeta>;
|
|
@@ -20,7 +20,35 @@ function toEntityPosition(position) {
|
|
|
20
20
|
export function createEntityStore() {
|
|
21
21
|
const store = createObservableKeyedStore();
|
|
22
22
|
const spawnPoses = new Map();
|
|
23
|
+
const constraints = new Map();
|
|
24
|
+
const blackboards = new Map();
|
|
23
25
|
let nextCounter = 1;
|
|
26
|
+
const blackboard = {
|
|
27
|
+
get: (id, key) => blackboards.get(id)?.get(key),
|
|
28
|
+
set(id, key, value) {
|
|
29
|
+
const board = blackboards.get(id) ?? new Map();
|
|
30
|
+
board.set(key, value);
|
|
31
|
+
blackboards.set(id, board);
|
|
32
|
+
},
|
|
33
|
+
has: (id, key) => blackboards.get(id)?.has(key) ?? false,
|
|
34
|
+
delete(id, key) {
|
|
35
|
+
blackboards.get(id)?.delete(key);
|
|
36
|
+
},
|
|
37
|
+
clear(id) {
|
|
38
|
+
blackboards.delete(id);
|
|
39
|
+
},
|
|
40
|
+
arm(id, key, untilMs) {
|
|
41
|
+
this.set(id, key, untilMs);
|
|
42
|
+
},
|
|
43
|
+
ready(id, key, nowMs) {
|
|
44
|
+
const until = blackboards.get(id)?.get(key);
|
|
45
|
+
return typeof until !== "number" || nowMs >= until;
|
|
46
|
+
},
|
|
47
|
+
remaining(id, key, nowMs) {
|
|
48
|
+
const until = blackboards.get(id)?.get(key);
|
|
49
|
+
return typeof until === "number" ? Math.max(0, until - nowMs) : 0;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
24
52
|
function generateId() {
|
|
25
53
|
let id = `entity-${nextCounter}`;
|
|
26
54
|
while (store.has(id)) {
|
|
@@ -33,7 +61,12 @@ export function createEntityStore() {
|
|
|
33
61
|
return {
|
|
34
62
|
spawn(name, options = {}) {
|
|
35
63
|
if (options.id !== undefined && store.has(options.id)) {
|
|
36
|
-
|
|
64
|
+
const onExisting = options.onExisting ?? "throw";
|
|
65
|
+
if (onExisting === "keep")
|
|
66
|
+
return options.id;
|
|
67
|
+
if (onExisting === "throw")
|
|
68
|
+
throw new Error(`Scene entity id "${options.id}" is already spawned.`);
|
|
69
|
+
blackboards.delete(options.id);
|
|
37
70
|
}
|
|
38
71
|
const id = options.id ?? generateId();
|
|
39
72
|
const position = toEntityPosition(options.position);
|
|
@@ -58,20 +91,58 @@ export function createEntityStore() {
|
|
|
58
91
|
const existed = store.has(id);
|
|
59
92
|
store.delete(id);
|
|
60
93
|
spawnPoses.delete(id);
|
|
94
|
+
constraints.delete(id);
|
|
95
|
+
blackboards.delete(id);
|
|
61
96
|
return existed;
|
|
62
97
|
},
|
|
63
98
|
update(id, patch) {
|
|
64
99
|
const current = store.get(id);
|
|
65
100
|
if (!current)
|
|
66
101
|
return false;
|
|
67
|
-
|
|
102
|
+
if (patch.position !== undefined)
|
|
103
|
+
current.position = toEntityPosition(patch.position);
|
|
104
|
+
if (patch.name !== undefined)
|
|
105
|
+
current.name = patch.name;
|
|
106
|
+
if (patch.rotationY !== undefined)
|
|
107
|
+
current.rotationY = patch.rotationY;
|
|
108
|
+
if (patch.rotationX !== undefined)
|
|
109
|
+
current.rotationX = patch.rotationX;
|
|
110
|
+
if (patch.rotationZ !== undefined)
|
|
111
|
+
current.rotationZ = patch.rotationZ;
|
|
112
|
+
if (patch.role !== undefined)
|
|
113
|
+
current.role = patch.role;
|
|
114
|
+
if (patch.movement !== undefined)
|
|
115
|
+
current.movement = patch.movement;
|
|
116
|
+
if (patch.behaviors !== undefined)
|
|
117
|
+
current.behaviors = patch.behaviors;
|
|
118
|
+
if (patch.meta !== undefined)
|
|
119
|
+
current.meta = patch.meta;
|
|
120
|
+
store.set(id, current);
|
|
68
121
|
return true;
|
|
69
122
|
},
|
|
123
|
+
setPoseConstraint(id, constraint) {
|
|
124
|
+
if (constraint === null)
|
|
125
|
+
constraints.delete(id);
|
|
126
|
+
else
|
|
127
|
+
constraints.set(id, constraint);
|
|
128
|
+
},
|
|
70
129
|
setPose(id, pose) {
|
|
71
130
|
const current = store.get(id);
|
|
72
131
|
if (!current)
|
|
73
132
|
return false;
|
|
74
|
-
|
|
133
|
+
let position = toEntityPosition(pose.position);
|
|
134
|
+
const constraint = constraints.get(id);
|
|
135
|
+
if (constraint !== undefined) {
|
|
136
|
+
const frame = {
|
|
137
|
+
entityId: id,
|
|
138
|
+
current: current.position,
|
|
139
|
+
next: position,
|
|
140
|
+
...(pose.dt === undefined ? {} : { dt: pose.dt }),
|
|
141
|
+
};
|
|
142
|
+
const replacement = constraint(frame);
|
|
143
|
+
if (replacement !== undefined)
|
|
144
|
+
position = toEntityPosition(replacement);
|
|
145
|
+
}
|
|
75
146
|
const velocity = pose.dt !== undefined && pose.dt > 0
|
|
76
147
|
? [
|
|
77
148
|
(position[0] - current.position[0]) / pose.dt,
|
|
@@ -79,14 +150,15 @@ export function createEntityStore() {
|
|
|
79
150
|
(position[2] - current.position[2]) / pose.dt,
|
|
80
151
|
]
|
|
81
152
|
: current.velocity;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
153
|
+
current.position = position;
|
|
154
|
+
current.velocity = velocity;
|
|
155
|
+
if (pose.rotationY !== undefined)
|
|
156
|
+
current.rotationY = pose.rotationY;
|
|
157
|
+
if (pose.rotationX !== undefined)
|
|
158
|
+
current.rotationX = pose.rotationX;
|
|
159
|
+
if (pose.rotationZ !== undefined)
|
|
160
|
+
current.rotationZ = pose.rotationZ;
|
|
161
|
+
store.set(id, current);
|
|
90
162
|
return true;
|
|
91
163
|
},
|
|
92
164
|
get(id) {
|
|
@@ -99,6 +171,8 @@ export function createEntityStore() {
|
|
|
99
171
|
for (const entity of store.arraySnapshot()) {
|
|
100
172
|
store.delete(entity.id);
|
|
101
173
|
}
|
|
174
|
+
constraints.clear();
|
|
175
|
+
blackboards.clear();
|
|
102
176
|
},
|
|
103
177
|
subscribe(listener) {
|
|
104
178
|
return store.subscribe(listener);
|
|
@@ -106,6 +180,35 @@ export function createEntityStore() {
|
|
|
106
180
|
snapshot() {
|
|
107
181
|
return store.arraySnapshot();
|
|
108
182
|
},
|
|
183
|
+
hydrate(entities) {
|
|
184
|
+
const incoming = new Set(entities.map((entity) => entity.id));
|
|
185
|
+
for (const current of store.arraySnapshot()) {
|
|
186
|
+
if (!incoming.has(current.id)) {
|
|
187
|
+
store.delete(current.id);
|
|
188
|
+
spawnPoses.delete(current.id);
|
|
189
|
+
constraints.delete(current.id);
|
|
190
|
+
blackboards.delete(current.id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const entity of entities) {
|
|
194
|
+
store.set(entity.id, {
|
|
195
|
+
id: entity.id,
|
|
196
|
+
name: entity.name,
|
|
197
|
+
position: entity.position,
|
|
198
|
+
rotationY: entity.rotationY,
|
|
199
|
+
rotationX: entity.rotationX,
|
|
200
|
+
rotationZ: entity.rotationZ,
|
|
201
|
+
velocity: entity.velocity,
|
|
202
|
+
role: entity.role,
|
|
203
|
+
movement: entity.movement,
|
|
204
|
+
behaviors: entity.behaviors,
|
|
205
|
+
meta: entity.meta,
|
|
206
|
+
});
|
|
207
|
+
if (!spawnPoses.has(entity.id)) {
|
|
208
|
+
spawnPoses.set(entity.id, { position: entity.position, rotationY: entity.rotationY });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
109
212
|
spawnPoseOf(id) {
|
|
110
213
|
return spawnPoses.get(id) ?? null;
|
|
111
214
|
},
|
|
@@ -114,8 +217,12 @@ export function createEntityStore() {
|
|
|
114
217
|
const current = store.get(id);
|
|
115
218
|
if (pose === undefined || current === undefined)
|
|
116
219
|
return false;
|
|
117
|
-
|
|
220
|
+
current.position = pose.position;
|
|
221
|
+
current.rotationY = pose.rotationY;
|
|
222
|
+
current.velocity = [0, 0, 0];
|
|
223
|
+
store.set(id, current);
|
|
118
224
|
return true;
|
|
119
225
|
},
|
|
226
|
+
blackboard,
|
|
120
227
|
};
|
|
121
228
|
}
|
|
@@ -14,5 +14,16 @@ export interface ObjectRaycastHit {
|
|
|
14
14
|
point: EntityPosition;
|
|
15
15
|
normal: EntityPosition;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
export
|
|
17
|
+
/** Optional broadphase for large object sets — typically `ObjectStore.inBox`. */
|
|
18
|
+
export interface ObjectQueryBroadphase {
|
|
19
|
+
inBox(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
|
|
20
|
+
}
|
|
21
|
+
export type ObjectRaycastSource = readonly SceneObject[] | ObjectQueryBroadphase;
|
|
22
|
+
declare function normalize(direction: EntityPosition): EntityPosition;
|
|
23
|
+
declare function intersectAabb(origin: EntityPosition, direction: EntityPosition, min: EntityPosition, max: EntityPosition, maxDistance: number): {
|
|
24
|
+
distance: number;
|
|
25
|
+
normal: EntityPosition;
|
|
26
|
+
} | null;
|
|
27
|
+
export declare function raycastObjects(source: ObjectRaycastSource, input: ObjectRaycastInput): ObjectRaycastHit | null;
|
|
28
|
+
export declare function raycastObjectsAll(source: ObjectRaycastSource, input: ObjectRaycastInput): ObjectRaycastHit[];
|
|
29
|
+
export { intersectAabb, normalize as normalizeDirection };
|
|
@@ -5,6 +5,9 @@ function normalize(direction) {
|
|
|
5
5
|
return [0, 0, 0];
|
|
6
6
|
return [direction[0] / length, direction[1] / length, direction[2] / length];
|
|
7
7
|
}
|
|
8
|
+
function isBroadphase(source) {
|
|
9
|
+
return !Array.isArray(source) && typeof source.inBox === "function";
|
|
10
|
+
}
|
|
8
11
|
function intersectAabb(origin, direction, min, max, maxDistance) {
|
|
9
12
|
let tMin = 0;
|
|
10
13
|
let tMax = maxDistance;
|
|
@@ -73,11 +76,38 @@ function hitFor(object, origin, direction, halfExtents, maxDistance) {
|
|
|
73
76
|
normal: hit.normal,
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
function raySegmentBounds(origin, direction, maxDistance, halfExtents) {
|
|
80
|
+
const endX = origin[0] + direction[0] * maxDistance;
|
|
81
|
+
const endY = origin[1] + direction[1] * maxDistance;
|
|
82
|
+
const endZ = origin[2] + direction[2] * maxDistance;
|
|
83
|
+
const padX = halfExtents[0];
|
|
84
|
+
const padY = halfExtents[1];
|
|
85
|
+
const padZ = halfExtents[2];
|
|
86
|
+
return {
|
|
87
|
+
min: [
|
|
88
|
+
Math.min(origin[0], endX) - padX,
|
|
89
|
+
Math.min(origin[1], endY) - padY,
|
|
90
|
+
Math.min(origin[2], endZ) - padZ,
|
|
91
|
+
],
|
|
92
|
+
max: [
|
|
93
|
+
Math.max(origin[0], endX) + padX,
|
|
94
|
+
Math.max(origin[1], endY) + padY,
|
|
95
|
+
Math.max(origin[2], endZ) + padZ,
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveCandidates(source, origin, direction, maxDistance, halfExtents) {
|
|
100
|
+
if (!isBroadphase(source))
|
|
101
|
+
return source;
|
|
102
|
+
const bounds = raySegmentBounds(origin, direction, maxDistance, halfExtents);
|
|
103
|
+
return source.inBox(bounds.min, bounds.max);
|
|
104
|
+
}
|
|
105
|
+
export function raycastObjects(source, input) {
|
|
77
106
|
const direction = normalize(input.direction);
|
|
78
107
|
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
79
108
|
return null;
|
|
80
109
|
const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
|
|
110
|
+
const objects = resolveCandidates(source, input.origin, direction, input.maxDistance, halfExtents);
|
|
81
111
|
let nearest = null;
|
|
82
112
|
for (const object of objects) {
|
|
83
113
|
if (input.filter !== undefined && !input.filter(object))
|
|
@@ -90,11 +120,12 @@ export function raycastObjects(objects, input) {
|
|
|
90
120
|
}
|
|
91
121
|
return nearest;
|
|
92
122
|
}
|
|
93
|
-
export function raycastObjectsAll(
|
|
123
|
+
export function raycastObjectsAll(source, input) {
|
|
94
124
|
const direction = normalize(input.direction);
|
|
95
125
|
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
96
126
|
return [];
|
|
97
127
|
const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
|
|
128
|
+
const objects = resolveCandidates(source, input.origin, direction, input.maxDistance, halfExtents);
|
|
98
129
|
const hits = [];
|
|
99
130
|
for (const object of objects) {
|
|
100
131
|
if (input.filter !== undefined && !input.filter(object))
|
|
@@ -106,3 +137,4 @@ export function raycastObjectsAll(objects, input) {
|
|
|
106
137
|
hits.sort((a, b) => a.distance - b.distance);
|
|
107
138
|
return hits;
|
|
108
139
|
}
|
|
140
|
+
export { intersectAabb, normalize as normalizeDirection };
|
|
@@ -17,6 +17,8 @@ export interface PlaceOptions {
|
|
|
17
17
|
parentSpace?: string;
|
|
18
18
|
rotation?: number;
|
|
19
19
|
visual?: ObjectVisual;
|
|
20
|
+
/** When `instanceId` is already placed: `"throw"` (default), `"replace"` (fresh placement over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
|
|
21
|
+
onExisting?: "throw" | "replace" | "keep";
|
|
20
22
|
}
|
|
21
23
|
export interface ObjectListFilter {
|
|
22
24
|
parentSpace?: string;
|
|
@@ -42,8 +42,14 @@ export function createObjectStore() {
|
|
|
42
42
|
}
|
|
43
43
|
return {
|
|
44
44
|
place(catalogId, x, y, z, options = {}) {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const existing = options.instanceId === undefined ? null : store.get(options.instanceId);
|
|
46
|
+
if (existing !== null && existing !== undefined) {
|
|
47
|
+
const onExisting = options.onExisting ?? "throw";
|
|
48
|
+
if (onExisting === "keep")
|
|
49
|
+
return options.instanceId;
|
|
50
|
+
if (onExisting === "throw")
|
|
51
|
+
throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
|
|
52
|
+
indexRemove(options.instanceId, existing.position);
|
|
47
53
|
}
|
|
48
54
|
const instanceId = options.instanceId ?? generateId();
|
|
49
55
|
const position = [x, y, z];
|
|
@@ -137,16 +143,29 @@ export function createObjectStore() {
|
|
|
137
143
|
},
|
|
138
144
|
inBox(min, max) {
|
|
139
145
|
const hits = [];
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const minCx = cellIndexOf(min[0]);
|
|
147
|
+
const maxCx = cellIndexOf(max[0]);
|
|
148
|
+
const minCy = cellIndexOf(min[1]);
|
|
149
|
+
const maxCy = cellIndexOf(max[1]);
|
|
150
|
+
const minCz = cellIndexOf(min[2]);
|
|
151
|
+
const maxCz = cellIndexOf(max[2]);
|
|
152
|
+
for (let cx = minCx; cx <= maxCx; cx += 1) {
|
|
153
|
+
for (let cy = minCy; cy <= maxCy; cy += 1) {
|
|
154
|
+
for (let cz = minCz; cz <= maxCz; cz += 1) {
|
|
155
|
+
const bucket = cellIndex.get(cellKey(cx, cy, cz));
|
|
156
|
+
if (bucket === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
for (const instanceId of bucket) {
|
|
159
|
+
const object = store.get(instanceId);
|
|
160
|
+
if (object === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
const [x, y, z] = object.position;
|
|
163
|
+
if (x >= min[0] && x <= max[0] &&
|
|
164
|
+
y >= min[1] && y <= max[1] &&
|
|
165
|
+
z >= min[2] && z <= max[2]) {
|
|
166
|
+
hits.push(object);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
150
169
|
}
|
|
151
170
|
}
|
|
152
171
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
import { type ColliderPurpose, type EntityColliderSet } from "./colliders.js";
|
|
3
|
+
import type { SceneObject } from "./objectStore.js";
|
|
4
|
+
export type SceneRaycastTargetKind = "entity" | "object" | "terrain" | "wall";
|
|
5
|
+
export interface SceneRaycastHit {
|
|
6
|
+
targetKind: SceneRaycastTargetKind;
|
|
7
|
+
instanceId: string;
|
|
8
|
+
catalogId?: string;
|
|
9
|
+
colliderName: string;
|
|
10
|
+
purpose: ColliderPurpose;
|
|
11
|
+
damageEligible: boolean;
|
|
12
|
+
blocks: boolean;
|
|
13
|
+
distance: number;
|
|
14
|
+
point: EntityPosition;
|
|
15
|
+
normal: EntityPosition;
|
|
16
|
+
}
|
|
17
|
+
export interface SceneRaycastFilter {
|
|
18
|
+
entities?: boolean;
|
|
19
|
+
objects?: boolean;
|
|
20
|
+
terrain?: boolean;
|
|
21
|
+
walls?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SceneRaycastInput {
|
|
24
|
+
origin: EntityPosition;
|
|
25
|
+
direction: EntityPosition;
|
|
26
|
+
maxDistance: number;
|
|
27
|
+
excludeInstanceIds?: ReadonlySet<string> | readonly string[];
|
|
28
|
+
filter?: SceneRaycastFilter;
|
|
29
|
+
/** When set, only hits satisfying the predicate are kept (applied after geometry). */
|
|
30
|
+
accept?: (hit: SceneRaycastHit) => boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface SceneEntityQuerySource {
|
|
33
|
+
list(): readonly {
|
|
34
|
+
id: string;
|
|
35
|
+
position: EntityPosition;
|
|
36
|
+
rotationY: number;
|
|
37
|
+
name?: string;
|
|
38
|
+
}[];
|
|
39
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
40
|
+
inRadius?(center: EntityPosition, radius: number): readonly string[];
|
|
41
|
+
get?(instanceId: string): {
|
|
42
|
+
id: string;
|
|
43
|
+
position: EntityPosition;
|
|
44
|
+
rotationY: number;
|
|
45
|
+
name?: string;
|
|
46
|
+
} | null;
|
|
47
|
+
}
|
|
48
|
+
export interface SceneObjectQuerySource {
|
|
49
|
+
list(): readonly SceneObject[];
|
|
50
|
+
inBox?(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
|
|
51
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
52
|
+
halfExtentsOf?(catalogId: string): EntityPosition | null | undefined;
|
|
53
|
+
}
|
|
54
|
+
export interface TerrainRaycastSource {
|
|
55
|
+
sampleHeight(x: number, z: number): number;
|
|
56
|
+
}
|
|
57
|
+
export interface WallSegment {
|
|
58
|
+
id: string;
|
|
59
|
+
/** Segment endpoints on the XZ plane; extruded by halfHeight along Y around yCenter. */
|
|
60
|
+
a: readonly [number, number];
|
|
61
|
+
b: readonly [number, number];
|
|
62
|
+
yCenter?: number;
|
|
63
|
+
halfHeight?: number;
|
|
64
|
+
thickness?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface SceneRaycastDeps {
|
|
67
|
+
entities?: SceneEntityQuerySource;
|
|
68
|
+
objects?: SceneObjectQuerySource;
|
|
69
|
+
terrain?: TerrainRaycastSource;
|
|
70
|
+
walls?: readonly WallSegment[];
|
|
71
|
+
}
|
|
72
|
+
export interface SceneRaycastApi {
|
|
73
|
+
raycast(input: SceneRaycastInput): SceneRaycastHit | null;
|
|
74
|
+
raycastAll(input: SceneRaycastInput): SceneRaycastHit[];
|
|
75
|
+
}
|
|
76
|
+
/** First impact: nearest hit that blocks, or nearest hit if none block. */
|
|
77
|
+
export declare function firstImpact(hits: readonly SceneRaycastHit[]): SceneRaycastHit | null;
|
|
78
|
+
/** Hits up to and including the first blocking collider (damage hitboxes before a wall stay). */
|
|
79
|
+
export declare function hitsUntilBlocked(hits: readonly SceneRaycastHit[]): SceneRaycastHit[];
|
|
80
|
+
export declare function createSceneRaycast(deps: SceneRaycastDeps): SceneRaycastApi;
|