@jgengine/core 0.8.0 → 0.9.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 +16 -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.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -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/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- 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 +40 -0
- package/dist/combat/shotOrigin.js +57 -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/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/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- 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 +29 -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 +1 -0
- package/dist/index.js +1 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -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/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -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/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/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- 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 +21 -0
- package/dist/runtime/gameContext.js +184 -9
- 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/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -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/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -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.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- 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/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/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 +198 -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 +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- 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/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -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 +2585 -1704
- package/package.json +12 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Stable identifier for a visibility-tracked object or camera. */
|
|
2
|
+
export type VisibilityId = string | number;
|
|
3
|
+
/** World-space point; `z` is optional for 2D adapters. */
|
|
4
|
+
export interface VisibilityPoint {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
z?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Sphere bounds used for visibility/streaming distance checks. */
|
|
10
|
+
export interface VisibilityBounds {
|
|
11
|
+
center: VisibilityPoint;
|
|
12
|
+
radius: number;
|
|
13
|
+
}
|
|
14
|
+
/** A camera's position and reach, as seen by the visibility system. */
|
|
15
|
+
export interface CameraVisibilityContext {
|
|
16
|
+
id: VisibilityId;
|
|
17
|
+
position: VisibilityPoint;
|
|
18
|
+
/** Approximate visible radius in world units. Adapters may derive this from a frustum. */
|
|
19
|
+
visibleRadius: number;
|
|
20
|
+
/** Extra distance used to request assets before an object becomes visible. */
|
|
21
|
+
preloadMargin?: number;
|
|
22
|
+
/** Cameras such as editor thumbnails can opt out of influencing streaming. */
|
|
23
|
+
influencesStreaming?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Per-object escape hatches that bypass or tune the default visibility policy. */
|
|
26
|
+
export interface VisibilityOverrides {
|
|
27
|
+
alwaysVisible?: boolean;
|
|
28
|
+
neverUnload?: boolean;
|
|
29
|
+
cullingDisabled?: boolean;
|
|
30
|
+
streamingDisabled?: boolean;
|
|
31
|
+
minRenderDistance?: number;
|
|
32
|
+
maxRenderDistance?: number;
|
|
33
|
+
preloadMargin?: number;
|
|
34
|
+
}
|
|
35
|
+
/** An entity/asset tracked by `VisibilitySystem.evaluate`. */
|
|
36
|
+
export interface VisibilityObject {
|
|
37
|
+
id: VisibilityId;
|
|
38
|
+
bounds: VisibilityBounds;
|
|
39
|
+
overrides?: VisibilityOverrides;
|
|
40
|
+
}
|
|
41
|
+
/** Baseline policy settings a `VisibilitySystem` is constructed with. */
|
|
42
|
+
export interface VisibilityDefaults {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
streamingEnabled: boolean;
|
|
45
|
+
minRenderDistance: number;
|
|
46
|
+
maxRenderDistance: number;
|
|
47
|
+
preloadMargin: number;
|
|
48
|
+
unloadGraceMs: number;
|
|
49
|
+
hysteresis: number;
|
|
50
|
+
}
|
|
51
|
+
/** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
|
|
52
|
+
export declare const DEFAULT_VISIBILITY_SETTINGS: Readonly<VisibilityDefaults>;
|
|
53
|
+
/** Result of evaluating one object against the active cameras this tick. */
|
|
54
|
+
export interface VisibilityDecision {
|
|
55
|
+
render: boolean;
|
|
56
|
+
load: boolean;
|
|
57
|
+
unload: boolean;
|
|
58
|
+
reason: "always-visible" | "culling-disabled" | "visible" | "preload" | "distance-culled" | "outside-view" | "grace-period" | "streaming-disabled";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Engine-level visibility and asset-residency policy.
|
|
62
|
+
*
|
|
63
|
+
* Render culling and asset streaming are intentionally separate. This class never
|
|
64
|
+
* destroys entities or throttles simulation; adapters decide how to apply results.
|
|
65
|
+
*/
|
|
66
|
+
export declare class VisibilitySystem {
|
|
67
|
+
#private;
|
|
68
|
+
readonly settings: VisibilityDefaults;
|
|
69
|
+
constructor(settings?: Partial<VisibilityDefaults>);
|
|
70
|
+
evaluate(object: VisibilityObject, cameras: readonly CameraVisibilityContext[], now?: number): VisibilityDecision;
|
|
71
|
+
setLoaded(id: VisibilityId, loaded: boolean, now?: number): void;
|
|
72
|
+
forget(id: VisibilityId): void;
|
|
73
|
+
clear(): void;
|
|
74
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
|
|
2
|
+
export const DEFAULT_VISIBILITY_SETTINGS = Object.freeze({
|
|
3
|
+
enabled: true,
|
|
4
|
+
streamingEnabled: true,
|
|
5
|
+
minRenderDistance: 0,
|
|
6
|
+
maxRenderDistance: Number.POSITIVE_INFINITY,
|
|
7
|
+
preloadMargin: 24,
|
|
8
|
+
unloadGraceMs: 5_000,
|
|
9
|
+
hysteresis: 8,
|
|
10
|
+
});
|
|
11
|
+
function squaredDistance(a, b) {
|
|
12
|
+
const dx = a.x - b.x;
|
|
13
|
+
const dy = a.y - b.y;
|
|
14
|
+
const dz = (a.z ?? 0) - (b.z ?? 0);
|
|
15
|
+
return dx * dx + dy * dy + dz * dz;
|
|
16
|
+
}
|
|
17
|
+
function intersectsRadius(camera, bounds, extraMargin) {
|
|
18
|
+
const radius = Math.max(0, camera.visibleRadius + extraMargin + bounds.radius);
|
|
19
|
+
return squaredDistance(camera.position, bounds.center) <= radius * radius;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Engine-level visibility and asset-residency policy.
|
|
23
|
+
*
|
|
24
|
+
* Render culling and asset streaming are intentionally separate. This class never
|
|
25
|
+
* destroys entities or throttles simulation; adapters decide how to apply results.
|
|
26
|
+
*/
|
|
27
|
+
export class VisibilitySystem {
|
|
28
|
+
settings;
|
|
29
|
+
#states = new Map();
|
|
30
|
+
constructor(settings = {}) {
|
|
31
|
+
this.settings = { ...DEFAULT_VISIBILITY_SETTINGS, ...settings };
|
|
32
|
+
}
|
|
33
|
+
evaluate(object, cameras, now = Date.now()) {
|
|
34
|
+
const overrides = object.overrides ?? {};
|
|
35
|
+
const state = this.#states.get(object.id) ?? { loaded: false, lastNeededAt: now };
|
|
36
|
+
if (overrides.alwaysVisible) {
|
|
37
|
+
this.#markNeeded(object.id, state, now);
|
|
38
|
+
return { render: true, load: true, unload: false, reason: "always-visible" };
|
|
39
|
+
}
|
|
40
|
+
if (!this.settings.enabled || overrides.cullingDisabled) {
|
|
41
|
+
this.#markNeeded(object.id, state, now);
|
|
42
|
+
return { render: true, load: true, unload: false, reason: "culling-disabled" };
|
|
43
|
+
}
|
|
44
|
+
const minDistance = Math.max(0, overrides.minRenderDistance ?? this.settings.minRenderDistance);
|
|
45
|
+
const maxDistance = Math.max(minDistance, overrides.maxRenderDistance ?? this.settings.maxRenderDistance);
|
|
46
|
+
const minDistanceSq = minDistance * minDistance;
|
|
47
|
+
const maxDistanceWithRadius = maxDistance + object.bounds.radius;
|
|
48
|
+
const maxDistanceSq = maxDistanceWithRadius * maxDistanceWithRadius;
|
|
49
|
+
let insideDistance = false;
|
|
50
|
+
let visible = false;
|
|
51
|
+
let preload = false;
|
|
52
|
+
for (const camera of cameras) {
|
|
53
|
+
const distanceSq = squaredDistance(camera.position, object.bounds.center);
|
|
54
|
+
if (distanceSq < minDistanceSq || distanceSq > maxDistanceSq)
|
|
55
|
+
continue;
|
|
56
|
+
insideDistance = true;
|
|
57
|
+
if (intersectsRadius(camera, object.bounds, this.settings.hysteresis)) {
|
|
58
|
+
visible = true;
|
|
59
|
+
}
|
|
60
|
+
if (camera.influencesStreaming !== false) {
|
|
61
|
+
const margin = overrides.preloadMargin ?? camera.preloadMargin ?? this.settings.preloadMargin;
|
|
62
|
+
if (intersectsRadius(camera, object.bounds, margin + this.settings.hysteresis)) {
|
|
63
|
+
preload = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (visible) {
|
|
68
|
+
this.#markNeeded(object.id, state, now);
|
|
69
|
+
return { render: true, load: true, unload: false, reason: "visible" };
|
|
70
|
+
}
|
|
71
|
+
if (overrides.streamingDisabled || !this.settings.streamingEnabled) {
|
|
72
|
+
state.loaded = true;
|
|
73
|
+
this.#states.set(object.id, state);
|
|
74
|
+
return {
|
|
75
|
+
render: false,
|
|
76
|
+
load: true,
|
|
77
|
+
unload: false,
|
|
78
|
+
reason: "streaming-disabled",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (preload) {
|
|
82
|
+
this.#markNeeded(object.id, state, now);
|
|
83
|
+
return { render: false, load: true, unload: false, reason: "preload" };
|
|
84
|
+
}
|
|
85
|
+
if (overrides.neverUnload) {
|
|
86
|
+
state.loaded = true;
|
|
87
|
+
this.#states.set(object.id, state);
|
|
88
|
+
return {
|
|
89
|
+
render: false,
|
|
90
|
+
load: true,
|
|
91
|
+
unload: false,
|
|
92
|
+
reason: insideDistance ? "outside-view" : "distance-culled",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const graceActive = state.loaded && now - state.lastNeededAt < this.settings.unloadGraceMs;
|
|
96
|
+
if (graceActive) {
|
|
97
|
+
this.#states.set(object.id, state);
|
|
98
|
+
return { render: false, load: true, unload: false, reason: "grace-period" };
|
|
99
|
+
}
|
|
100
|
+
state.loaded = false;
|
|
101
|
+
this.#states.set(object.id, state);
|
|
102
|
+
return {
|
|
103
|
+
render: false,
|
|
104
|
+
load: false,
|
|
105
|
+
unload: true,
|
|
106
|
+
reason: insideDistance ? "outside-view" : "distance-culled",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
setLoaded(id, loaded, now = Date.now()) {
|
|
110
|
+
const previous = this.#states.get(id);
|
|
111
|
+
this.#states.set(id, {
|
|
112
|
+
loaded,
|
|
113
|
+
lastNeededAt: loaded ? now : previous?.lastNeededAt ?? now,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
forget(id) {
|
|
117
|
+
this.#states.delete(id);
|
|
118
|
+
}
|
|
119
|
+
clear() {
|
|
120
|
+
this.#states.clear();
|
|
121
|
+
}
|
|
122
|
+
#markNeeded(id, state, now) {
|
|
123
|
+
state.loaded = true;
|
|
124
|
+
state.lastNeededAt = now;
|
|
125
|
+
this.#states.set(id, state);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
export type ColliderPurpose = "physical" | "damage";
|
|
3
|
+
export type ColliderShape = {
|
|
4
|
+
kind: "sphere";
|
|
5
|
+
radius: number;
|
|
6
|
+
offset?: EntityPosition;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "aabb";
|
|
9
|
+
halfExtents: EntityPosition;
|
|
10
|
+
offset?: EntityPosition;
|
|
11
|
+
};
|
|
12
|
+
export interface ColliderDef {
|
|
13
|
+
name: string;
|
|
14
|
+
purpose: ColliderPurpose;
|
|
15
|
+
shape: ColliderShape;
|
|
16
|
+
/** Defaults true for `purpose: "damage"`, false for `purpose: "physical"`. */
|
|
17
|
+
damageEligible?: boolean;
|
|
18
|
+
/** Defaults true — a blocking collider stops a ray at first impact. */
|
|
19
|
+
blocks?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface EntityColliderSet {
|
|
22
|
+
/** Single physical collision body (push / obstruction). */
|
|
23
|
+
body?: ColliderDef;
|
|
24
|
+
/** Named damage hitboxes (zero or more). */
|
|
25
|
+
hitboxes?: readonly ColliderDef[];
|
|
26
|
+
}
|
|
27
|
+
export interface ResolvedCollider {
|
|
28
|
+
name: string;
|
|
29
|
+
purpose: ColliderPurpose;
|
|
30
|
+
shape: ColliderShape;
|
|
31
|
+
damageEligible: boolean;
|
|
32
|
+
blocks: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
|
|
35
|
+
export declare const DEFAULT_OBJECT_HALF_EXTENTS: EntityPosition;
|
|
36
|
+
export declare function defaultEntityColliders(): EntityColliderSet;
|
|
37
|
+
export declare function defaultObjectColliders(halfExtents?: EntityPosition): EntityColliderSet;
|
|
38
|
+
export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
|
|
39
|
+
export declare function worldOffset(local: EntityPosition | undefined, position: EntityPosition, rotationY: number): EntityPosition;
|
|
40
|
+
export declare function colliderWorldCenter(collider: ResolvedCollider, position: EntityPosition, rotationY: number): EntityPosition;
|
|
41
|
+
export declare function colliderBounds(collider: ResolvedCollider, position: EntityPosition, rotationY: number): {
|
|
42
|
+
min: EntityPosition;
|
|
43
|
+
max: EntityPosition;
|
|
44
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
|
|
2
|
+
export const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
3
|
+
export function defaultEntityColliders() {
|
|
4
|
+
return {
|
|
5
|
+
hitboxes: [
|
|
6
|
+
{
|
|
7
|
+
name: "body",
|
|
8
|
+
purpose: "damage",
|
|
9
|
+
shape: { kind: "sphere", radius: DEFAULT_ENTITY_HIT_RADIUS },
|
|
10
|
+
damageEligible: true,
|
|
11
|
+
blocks: false,
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function defaultObjectColliders(halfExtents = DEFAULT_OBJECT_HALF_EXTENTS) {
|
|
17
|
+
return {
|
|
18
|
+
body: {
|
|
19
|
+
name: "body",
|
|
20
|
+
purpose: "physical",
|
|
21
|
+
shape: { kind: "aabb", halfExtents },
|
|
22
|
+
damageEligible: false,
|
|
23
|
+
blocks: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function resolveColliders(set) {
|
|
28
|
+
if (set === null || set === undefined)
|
|
29
|
+
return [];
|
|
30
|
+
const out = [];
|
|
31
|
+
if (set.body !== undefined)
|
|
32
|
+
out.push(resolveOne(set.body));
|
|
33
|
+
if (set.hitboxes !== undefined) {
|
|
34
|
+
for (const hitbox of set.hitboxes)
|
|
35
|
+
out.push(resolveOne(hitbox));
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function resolveOne(def) {
|
|
40
|
+
const damageEligible = def.damageEligible ?? def.purpose === "damage";
|
|
41
|
+
const blocks = def.blocks ?? def.purpose === "physical";
|
|
42
|
+
return {
|
|
43
|
+
name: def.name,
|
|
44
|
+
purpose: def.purpose,
|
|
45
|
+
shape: def.shape,
|
|
46
|
+
damageEligible,
|
|
47
|
+
blocks,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function worldOffset(local, position, rotationY) {
|
|
51
|
+
if (local === undefined)
|
|
52
|
+
return position;
|
|
53
|
+
const cos = Math.cos(rotationY);
|
|
54
|
+
const sin = Math.sin(rotationY);
|
|
55
|
+
const lx = local[0];
|
|
56
|
+
const ly = local[1];
|
|
57
|
+
const lz = local[2];
|
|
58
|
+
return [position[0] + lx * cos + lz * sin, position[1] + ly, position[2] - lx * sin + lz * cos];
|
|
59
|
+
}
|
|
60
|
+
export function colliderWorldCenter(collider, position, rotationY) {
|
|
61
|
+
return worldOffset(collider.shape.offset, position, rotationY);
|
|
62
|
+
}
|
|
63
|
+
export function colliderBounds(collider, position, rotationY) {
|
|
64
|
+
const center = colliderWorldCenter(collider, position, rotationY);
|
|
65
|
+
if (collider.shape.kind === "sphere") {
|
|
66
|
+
const r = collider.shape.radius;
|
|
67
|
+
return {
|
|
68
|
+
min: [center[0] - r, center[1] - r, center[2] - r],
|
|
69
|
+
max: [center[0] + r, center[1] + r, center[2] + r],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const h = collider.shape.halfExtents;
|
|
73
|
+
return {
|
|
74
|
+
min: [center[0] - h[0], center[1] - h[1], center[2] - h[2]],
|
|
75
|
+
max: [center[0] + h[0], center[1] + h[1], center[2] + h[2]],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -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,11 +53,25 @@ 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
|
+
export interface EntityStore<TMeta = unknown> {
|
|
54
69
|
spawn(name: string, options?: SpawnOptions<TMeta>): string;
|
|
55
70
|
despawn(id: string): boolean;
|
|
56
|
-
update(id: string, patch:
|
|
71
|
+
update(id: string, patch: EntityUpdatePatch<TMeta>): boolean;
|
|
57
72
|
setPose(id: string, pose: EntityPose): boolean;
|
|
73
|
+
/** 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. */
|
|
74
|
+
setPoseConstraint(id: string, constraint: PoseConstraint | null): void;
|
|
58
75
|
get(id: string): SceneEntity<TMeta> | null;
|
|
59
76
|
list(): readonly SceneEntity<TMeta>[];
|
|
60
77
|
clear(): void;
|
|
@@ -63,4 +80,4 @@ export interface EntityStore<TMeta = undefined> {
|
|
|
63
80
|
spawnPoseOf(id: string): SpawnPose | null;
|
|
64
81
|
resetToSpawn(id: string): boolean;
|
|
65
82
|
}
|
|
66
|
-
export declare function createEntityStore<TMeta =
|
|
83
|
+
export declare function createEntityStore<TMeta = unknown>(): EntityStore<TMeta>;
|
|
@@ -20,6 +20,7 @@ function toEntityPosition(position) {
|
|
|
20
20
|
export function createEntityStore() {
|
|
21
21
|
const store = createObservableKeyedStore();
|
|
22
22
|
const spawnPoses = new Map();
|
|
23
|
+
const constraints = new Map();
|
|
23
24
|
let nextCounter = 1;
|
|
24
25
|
function generateId() {
|
|
25
26
|
let id = `entity-${nextCounter}`;
|
|
@@ -33,7 +34,11 @@ export function createEntityStore() {
|
|
|
33
34
|
return {
|
|
34
35
|
spawn(name, options = {}) {
|
|
35
36
|
if (options.id !== undefined && store.has(options.id)) {
|
|
36
|
-
|
|
37
|
+
const onExisting = options.onExisting ?? "throw";
|
|
38
|
+
if (onExisting === "keep")
|
|
39
|
+
return options.id;
|
|
40
|
+
if (onExisting === "throw")
|
|
41
|
+
throw new Error(`Scene entity id "${options.id}" is already spawned.`);
|
|
37
42
|
}
|
|
38
43
|
const id = options.id ?? generateId();
|
|
39
44
|
const position = toEntityPosition(options.position);
|
|
@@ -58,20 +63,57 @@ export function createEntityStore() {
|
|
|
58
63
|
const existed = store.has(id);
|
|
59
64
|
store.delete(id);
|
|
60
65
|
spawnPoses.delete(id);
|
|
66
|
+
constraints.delete(id);
|
|
61
67
|
return existed;
|
|
62
68
|
},
|
|
63
69
|
update(id, patch) {
|
|
64
70
|
const current = store.get(id);
|
|
65
71
|
if (!current)
|
|
66
72
|
return false;
|
|
67
|
-
|
|
73
|
+
if (patch.position !== undefined)
|
|
74
|
+
current.position = toEntityPosition(patch.position);
|
|
75
|
+
if (patch.name !== undefined)
|
|
76
|
+
current.name = patch.name;
|
|
77
|
+
if (patch.rotationY !== undefined)
|
|
78
|
+
current.rotationY = patch.rotationY;
|
|
79
|
+
if (patch.rotationX !== undefined)
|
|
80
|
+
current.rotationX = patch.rotationX;
|
|
81
|
+
if (patch.rotationZ !== undefined)
|
|
82
|
+
current.rotationZ = patch.rotationZ;
|
|
83
|
+
if (patch.role !== undefined)
|
|
84
|
+
current.role = patch.role;
|
|
85
|
+
if (patch.movement !== undefined)
|
|
86
|
+
current.movement = patch.movement;
|
|
87
|
+
if (patch.behaviors !== undefined)
|
|
88
|
+
current.behaviors = patch.behaviors;
|
|
89
|
+
if (patch.meta !== undefined)
|
|
90
|
+
current.meta = patch.meta;
|
|
91
|
+
store.set(id, current);
|
|
68
92
|
return true;
|
|
69
93
|
},
|
|
94
|
+
setPoseConstraint(id, constraint) {
|
|
95
|
+
if (constraint === null)
|
|
96
|
+
constraints.delete(id);
|
|
97
|
+
else
|
|
98
|
+
constraints.set(id, constraint);
|
|
99
|
+
},
|
|
70
100
|
setPose(id, pose) {
|
|
71
101
|
const current = store.get(id);
|
|
72
102
|
if (!current)
|
|
73
103
|
return false;
|
|
74
|
-
|
|
104
|
+
let position = toEntityPosition(pose.position);
|
|
105
|
+
const constraint = constraints.get(id);
|
|
106
|
+
if (constraint !== undefined) {
|
|
107
|
+
const frame = {
|
|
108
|
+
entityId: id,
|
|
109
|
+
current: current.position,
|
|
110
|
+
next: position,
|
|
111
|
+
...(pose.dt === undefined ? {} : { dt: pose.dt }),
|
|
112
|
+
};
|
|
113
|
+
const replacement = constraint(frame);
|
|
114
|
+
if (replacement !== undefined)
|
|
115
|
+
position = toEntityPosition(replacement);
|
|
116
|
+
}
|
|
75
117
|
const velocity = pose.dt !== undefined && pose.dt > 0
|
|
76
118
|
? [
|
|
77
119
|
(position[0] - current.position[0]) / pose.dt,
|
|
@@ -79,14 +121,15 @@ export function createEntityStore() {
|
|
|
79
121
|
(position[2] - current.position[2]) / pose.dt,
|
|
80
122
|
]
|
|
81
123
|
: current.velocity;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
current.position = position;
|
|
125
|
+
current.velocity = velocity;
|
|
126
|
+
if (pose.rotationY !== undefined)
|
|
127
|
+
current.rotationY = pose.rotationY;
|
|
128
|
+
if (pose.rotationX !== undefined)
|
|
129
|
+
current.rotationX = pose.rotationX;
|
|
130
|
+
if (pose.rotationZ !== undefined)
|
|
131
|
+
current.rotationZ = pose.rotationZ;
|
|
132
|
+
store.set(id, current);
|
|
90
133
|
return true;
|
|
91
134
|
},
|
|
92
135
|
get(id) {
|
|
@@ -99,6 +142,7 @@ export function createEntityStore() {
|
|
|
99
142
|
for (const entity of store.arraySnapshot()) {
|
|
100
143
|
store.delete(entity.id);
|
|
101
144
|
}
|
|
145
|
+
constraints.clear();
|
|
102
146
|
},
|
|
103
147
|
subscribe(listener) {
|
|
104
148
|
return store.subscribe(listener);
|
|
@@ -114,7 +158,10 @@ export function createEntityStore() {
|
|
|
114
158
|
const current = store.get(id);
|
|
115
159
|
if (pose === undefined || current === undefined)
|
|
116
160
|
return false;
|
|
117
|
-
|
|
161
|
+
current.position = pose.position;
|
|
162
|
+
current.rotationY = pose.rotationY;
|
|
163
|
+
current.velocity = [0, 0, 0];
|
|
164
|
+
store.set(id, current);
|
|
118
165
|
return true;
|
|
119
166
|
},
|
|
120
167
|
};
|
|
@@ -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
|
}
|