@jgengine/core 0.6.0 → 0.8.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 +214 -2
- package/README.md +2 -0
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +61 -0
- package/dist/ai/spawnDirector.js +162 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +64 -0
- package/dist/cards/cardPile.js +175 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +47 -0
- package/dist/combat/abilityKit.js +139 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +12 -1
- package/dist/combat/effects.js +31 -19
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +31 -0
- package/dist/combat/telegraph.js +51 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +87 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +516 -8
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +100 -0
- package/dist/game/race.js +236 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +69 -0
- package/dist/game/social.js +138 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +95 -0
- package/dist/game/worldItem.js +138 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +69 -0
- package/dist/input/pointer.js +65 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +115 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +71 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +56 -0
- package/dist/nav/navGrid.js +265 -0
- package/dist/nav/pathFollow.d.ts +31 -0
- package/dist/nav/pathFollow.js +69 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +124 -2
- package/dist/physics/physicsWorld.js +493 -41
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +143 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +99 -0
- package/dist/physics/vehicleBody.js +233 -0
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +107 -3
- package/dist/runtime/gameContext.js +253 -7
- package/dist/runtime/hostPersistence.d.ts +39 -0
- package/dist/runtime/hostPersistence.js +54 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +9 -1
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +79 -0
- package/dist/session/roundState.js +158 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +68 -0
- package/dist/tactics/tacticalGrid.js +261 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +60 -0
- package/dist/turn/turnLoop.js +223 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +69 -0
- package/dist/world/placementController.js +153 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
import type { SceneObject } from "./objectStore.js";
|
|
3
|
+
export interface ObjectRaycastInput {
|
|
4
|
+
origin: EntityPosition;
|
|
5
|
+
direction: EntityPosition;
|
|
6
|
+
maxDistance: number;
|
|
7
|
+
halfExtents?: EntityPosition;
|
|
8
|
+
filter?: (object: SceneObject) => boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ObjectRaycastHit {
|
|
11
|
+
instanceId: string;
|
|
12
|
+
catalogId: string;
|
|
13
|
+
distance: number;
|
|
14
|
+
point: EntityPosition;
|
|
15
|
+
normal: EntityPosition;
|
|
16
|
+
}
|
|
17
|
+
export declare function raycastObjects(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit | null;
|
|
18
|
+
export declare function raycastObjectsAll(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const DEFAULT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
2
|
+
function normalize(direction) {
|
|
3
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]);
|
|
4
|
+
if (length === 0)
|
|
5
|
+
return [0, 0, 0];
|
|
6
|
+
return [direction[0] / length, direction[1] / length, direction[2] / length];
|
|
7
|
+
}
|
|
8
|
+
function intersectAabb(origin, direction, min, max, maxDistance) {
|
|
9
|
+
let tMin = 0;
|
|
10
|
+
let tMax = maxDistance;
|
|
11
|
+
let normalAxis = -1;
|
|
12
|
+
let normalSign = 0;
|
|
13
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
14
|
+
const originAxis = origin[axis];
|
|
15
|
+
const dirAxis = direction[axis];
|
|
16
|
+
const minAxis = min[axis];
|
|
17
|
+
const maxAxis = max[axis];
|
|
18
|
+
if (Math.abs(dirAxis) < 1e-12) {
|
|
19
|
+
if (originAxis < minAxis || originAxis > maxAxis)
|
|
20
|
+
return null;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const invD = 1 / dirAxis;
|
|
24
|
+
const t1 = (minAxis - originAxis) * invD;
|
|
25
|
+
const t2 = (maxAxis - originAxis) * invD;
|
|
26
|
+
const minIsNear = t1 <= t2;
|
|
27
|
+
const tNearAxis = minIsNear ? t1 : t2;
|
|
28
|
+
const tFarAxis = minIsNear ? t2 : t1;
|
|
29
|
+
const nearFaceSign = minIsNear ? -1 : 1;
|
|
30
|
+
if (tNearAxis > tMin) {
|
|
31
|
+
tMin = tNearAxis;
|
|
32
|
+
normalAxis = axis;
|
|
33
|
+
normalSign = nearFaceSign;
|
|
34
|
+
}
|
|
35
|
+
tMax = Math.min(tMax, tFarAxis);
|
|
36
|
+
if (tMin > tMax)
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (tMin > maxDistance || tMin < 0)
|
|
40
|
+
return null;
|
|
41
|
+
const normal = normalAxis === 0
|
|
42
|
+
? [normalSign, 0, 0]
|
|
43
|
+
: normalAxis === 1
|
|
44
|
+
? [0, normalSign, 0]
|
|
45
|
+
: normalAxis === 2
|
|
46
|
+
? [0, 0, normalSign]
|
|
47
|
+
: [0, 1, 0];
|
|
48
|
+
return { distance: tMin, normal };
|
|
49
|
+
}
|
|
50
|
+
function hitFor(object, origin, direction, halfExtents, maxDistance) {
|
|
51
|
+
const min = [
|
|
52
|
+
object.position[0] - halfExtents[0],
|
|
53
|
+
object.position[1] - halfExtents[1],
|
|
54
|
+
object.position[2] - halfExtents[2],
|
|
55
|
+
];
|
|
56
|
+
const max = [
|
|
57
|
+
object.position[0] + halfExtents[0],
|
|
58
|
+
object.position[1] + halfExtents[1],
|
|
59
|
+
object.position[2] + halfExtents[2],
|
|
60
|
+
];
|
|
61
|
+
const hit = intersectAabb(origin, direction, min, max, maxDistance);
|
|
62
|
+
if (hit === null)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
instanceId: object.instanceId,
|
|
66
|
+
catalogId: object.catalogId,
|
|
67
|
+
distance: hit.distance,
|
|
68
|
+
point: [
|
|
69
|
+
origin[0] + direction[0] * hit.distance,
|
|
70
|
+
origin[1] + direction[1] * hit.distance,
|
|
71
|
+
origin[2] + direction[2] * hit.distance,
|
|
72
|
+
],
|
|
73
|
+
normal: hit.normal,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function raycastObjects(objects, input) {
|
|
77
|
+
const direction = normalize(input.direction);
|
|
78
|
+
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
79
|
+
return null;
|
|
80
|
+
const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
|
|
81
|
+
let nearest = null;
|
|
82
|
+
for (const object of objects) {
|
|
83
|
+
if (input.filter !== undefined && !input.filter(object))
|
|
84
|
+
continue;
|
|
85
|
+
const hit = hitFor(object, input.origin, direction, halfExtents, input.maxDistance);
|
|
86
|
+
if (hit === null)
|
|
87
|
+
continue;
|
|
88
|
+
if (nearest === null || hit.distance < nearest.distance)
|
|
89
|
+
nearest = hit;
|
|
90
|
+
}
|
|
91
|
+
return nearest;
|
|
92
|
+
}
|
|
93
|
+
export function raycastObjectsAll(objects, input) {
|
|
94
|
+
const direction = normalize(input.direction);
|
|
95
|
+
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
96
|
+
return [];
|
|
97
|
+
const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
|
|
98
|
+
const hits = [];
|
|
99
|
+
for (const object of objects) {
|
|
100
|
+
if (input.filter !== undefined && !input.filter(object))
|
|
101
|
+
continue;
|
|
102
|
+
const hit = hitFor(object, input.origin, direction, halfExtents, input.maxDistance);
|
|
103
|
+
if (hit !== null)
|
|
104
|
+
hits.push(hit);
|
|
105
|
+
}
|
|
106
|
+
hits.sort((a, b) => a.distance - b.distance);
|
|
107
|
+
return hits;
|
|
108
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
export interface ObjectVisual {
|
|
3
|
+
scale?: number | readonly [number, number, number];
|
|
4
|
+
color?: string;
|
|
5
|
+
opacity?: number;
|
|
6
|
+
}
|
|
2
7
|
export interface SceneObject {
|
|
3
8
|
instanceId: string;
|
|
4
9
|
catalogId: string;
|
|
5
10
|
position: EntityPosition;
|
|
6
11
|
rotationY: number;
|
|
7
12
|
parentSpace?: string;
|
|
13
|
+
visual?: ObjectVisual;
|
|
8
14
|
}
|
|
9
15
|
export interface PlaceOptions {
|
|
10
16
|
instanceId?: string;
|
|
11
17
|
parentSpace?: string;
|
|
12
18
|
rotation?: number;
|
|
19
|
+
visual?: ObjectVisual;
|
|
13
20
|
}
|
|
14
21
|
export interface ObjectListFilter {
|
|
15
22
|
parentSpace?: string;
|
|
@@ -19,10 +26,14 @@ export interface ObjectStore {
|
|
|
19
26
|
remove(instanceId: string): boolean;
|
|
20
27
|
move(instanceId: string, x: number, y: number, z: number): boolean;
|
|
21
28
|
rotate(instanceId: string, rotationY: number): boolean;
|
|
29
|
+
setVisual(instanceId: string, visual: ObjectVisual | undefined): boolean;
|
|
22
30
|
get(instanceId: string): SceneObject | null;
|
|
23
31
|
list(filter?: ObjectListFilter): readonly SceneObject[];
|
|
32
|
+
at(x: number, y: number, z: number, tolerance?: number): SceneObject[];
|
|
33
|
+
inBox(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
|
|
24
34
|
clear(): void;
|
|
25
35
|
subscribe(listener: () => void): () => void;
|
|
26
36
|
snapshot(): readonly SceneObject[];
|
|
27
37
|
}
|
|
28
38
|
export declare function createObjectStore(): ObjectStore;
|
|
39
|
+
export declare function objectVisualScale(visual: ObjectVisual | undefined): readonly [number, number, number];
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
|
+
const CELL_SIZE = 1;
|
|
3
|
+
function cellIndexOf(value) {
|
|
4
|
+
return Math.floor(value / CELL_SIZE);
|
|
5
|
+
}
|
|
6
|
+
function cellKey(cx, cy, cz) {
|
|
7
|
+
return `${cx}:${cy}:${cz}`;
|
|
8
|
+
}
|
|
9
|
+
function cellKeyOf(position) {
|
|
10
|
+
return cellKey(cellIndexOf(position[0]), cellIndexOf(position[1]), cellIndexOf(position[2]));
|
|
11
|
+
}
|
|
2
12
|
export function createObjectStore() {
|
|
3
13
|
const store = createObservableKeyedStore();
|
|
14
|
+
const cellIndex = new Map();
|
|
4
15
|
let nextCounter = 1;
|
|
5
16
|
function generateId() {
|
|
6
17
|
let id = `object-${nextCounter}`;
|
|
@@ -11,31 +22,58 @@ export function createObjectStore() {
|
|
|
11
22
|
nextCounter += 1;
|
|
12
23
|
return id;
|
|
13
24
|
}
|
|
25
|
+
function indexAdd(instanceId, position) {
|
|
26
|
+
const key = cellKeyOf(position);
|
|
27
|
+
let bucket = cellIndex.get(key);
|
|
28
|
+
if (bucket === undefined) {
|
|
29
|
+
bucket = new Set();
|
|
30
|
+
cellIndex.set(key, bucket);
|
|
31
|
+
}
|
|
32
|
+
bucket.add(instanceId);
|
|
33
|
+
}
|
|
34
|
+
function indexRemove(instanceId, position) {
|
|
35
|
+
const key = cellKeyOf(position);
|
|
36
|
+
const bucket = cellIndex.get(key);
|
|
37
|
+
if (bucket === undefined)
|
|
38
|
+
return;
|
|
39
|
+
bucket.delete(instanceId);
|
|
40
|
+
if (bucket.size === 0)
|
|
41
|
+
cellIndex.delete(key);
|
|
42
|
+
}
|
|
14
43
|
return {
|
|
15
44
|
place(catalogId, x, y, z, options = {}) {
|
|
16
45
|
if (options.instanceId !== undefined && store.has(options.instanceId)) {
|
|
17
46
|
throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
|
|
18
47
|
}
|
|
19
48
|
const instanceId = options.instanceId ?? generateId();
|
|
49
|
+
const position = [x, y, z];
|
|
20
50
|
store.set(instanceId, {
|
|
21
51
|
instanceId,
|
|
22
52
|
catalogId,
|
|
23
|
-
position
|
|
53
|
+
position,
|
|
24
54
|
rotationY: options.rotation ?? 0,
|
|
25
55
|
...(options.parentSpace !== undefined ? { parentSpace: options.parentSpace } : {}),
|
|
56
|
+
...(options.visual !== undefined ? { visual: options.visual } : {}),
|
|
26
57
|
});
|
|
58
|
+
indexAdd(instanceId, position);
|
|
27
59
|
return instanceId;
|
|
28
60
|
},
|
|
29
61
|
remove(instanceId) {
|
|
30
|
-
const
|
|
62
|
+
const current = store.get(instanceId);
|
|
63
|
+
if (!current)
|
|
64
|
+
return false;
|
|
31
65
|
store.delete(instanceId);
|
|
32
|
-
|
|
66
|
+
indexRemove(instanceId, current.position);
|
|
67
|
+
return true;
|
|
33
68
|
},
|
|
34
69
|
move(instanceId, x, y, z) {
|
|
35
70
|
const current = store.get(instanceId);
|
|
36
71
|
if (!current)
|
|
37
72
|
return false;
|
|
38
|
-
|
|
73
|
+
const position = [x, y, z];
|
|
74
|
+
store.set(instanceId, { ...current, position });
|
|
75
|
+
indexRemove(instanceId, current.position);
|
|
76
|
+
indexAdd(instanceId, position);
|
|
39
77
|
return true;
|
|
40
78
|
},
|
|
41
79
|
rotate(instanceId, rotationY) {
|
|
@@ -45,6 +83,20 @@ export function createObjectStore() {
|
|
|
45
83
|
store.set(instanceId, { ...current, rotationY });
|
|
46
84
|
return true;
|
|
47
85
|
},
|
|
86
|
+
setVisual(instanceId, visual) {
|
|
87
|
+
const current = store.get(instanceId);
|
|
88
|
+
if (!current)
|
|
89
|
+
return false;
|
|
90
|
+
store.set(instanceId, {
|
|
91
|
+
instanceId: current.instanceId,
|
|
92
|
+
catalogId: current.catalogId,
|
|
93
|
+
position: current.position,
|
|
94
|
+
rotationY: current.rotationY,
|
|
95
|
+
...(current.parentSpace !== undefined ? { parentSpace: current.parentSpace } : {}),
|
|
96
|
+
...(visual !== undefined ? { visual } : {}),
|
|
97
|
+
});
|
|
98
|
+
return true;
|
|
99
|
+
},
|
|
48
100
|
get(instanceId) {
|
|
49
101
|
return store.get(instanceId) ?? null;
|
|
50
102
|
},
|
|
@@ -54,10 +106,57 @@ export function createObjectStore() {
|
|
|
54
106
|
return all;
|
|
55
107
|
return all.filter((object) => object.parentSpace === filter.parentSpace);
|
|
56
108
|
},
|
|
109
|
+
at(x, y, z, tolerance = 0) {
|
|
110
|
+
const minCx = cellIndexOf(x - tolerance);
|
|
111
|
+
const maxCx = cellIndexOf(x + tolerance);
|
|
112
|
+
const minCy = cellIndexOf(y - tolerance);
|
|
113
|
+
const maxCy = cellIndexOf(y + tolerance);
|
|
114
|
+
const minCz = cellIndexOf(z - tolerance);
|
|
115
|
+
const maxCz = cellIndexOf(z + tolerance);
|
|
116
|
+
const results = [];
|
|
117
|
+
for (let cx = minCx; cx <= maxCx; cx += 1) {
|
|
118
|
+
for (let cy = minCy; cy <= maxCy; cy += 1) {
|
|
119
|
+
for (let cz = minCz; cz <= maxCz; cz += 1) {
|
|
120
|
+
const bucket = cellIndex.get(cellKey(cx, cy, cz));
|
|
121
|
+
if (bucket === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
for (const instanceId of bucket) {
|
|
124
|
+
const object = store.get(instanceId);
|
|
125
|
+
if (object === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
const dx = object.position[0] - x;
|
|
128
|
+
const dy = object.position[1] - y;
|
|
129
|
+
const dz = object.position[2] - z;
|
|
130
|
+
if (Math.hypot(dx, dy, dz) <= tolerance)
|
|
131
|
+
results.push(object);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
},
|
|
138
|
+
inBox(min, max) {
|
|
139
|
+
const hits = [];
|
|
140
|
+
for (const bucket of cellIndex.values()) {
|
|
141
|
+
for (const instanceId of bucket) {
|
|
142
|
+
const object = store.get(instanceId);
|
|
143
|
+
if (object === undefined)
|
|
144
|
+
continue;
|
|
145
|
+
const [x, y, z] = object.position;
|
|
146
|
+
if (x >= min[0] && x <= max[0] &&
|
|
147
|
+
y >= min[1] && y <= max[1] &&
|
|
148
|
+
z >= min[2] && z <= max[2]) {
|
|
149
|
+
hits.push(object);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return hits;
|
|
154
|
+
},
|
|
57
155
|
clear() {
|
|
58
156
|
for (const object of store.arraySnapshot()) {
|
|
59
157
|
store.delete(object.instanceId);
|
|
60
158
|
}
|
|
159
|
+
cellIndex.clear();
|
|
61
160
|
},
|
|
62
161
|
subscribe(listener) {
|
|
63
162
|
return store.subscribe(listener);
|
|
@@ -67,3 +166,9 @@ export function createObjectStore() {
|
|
|
67
166
|
},
|
|
68
167
|
};
|
|
69
168
|
}
|
|
169
|
+
export function objectVisualScale(visual) {
|
|
170
|
+
const scale = visual?.scale;
|
|
171
|
+
if (scale === undefined)
|
|
172
|
+
return [1, 1, 1];
|
|
173
|
+
return typeof scale === "number" ? [scale, scale, scale] : scale;
|
|
174
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PaintStroke {
|
|
2
|
+
u: number;
|
|
3
|
+
v: number;
|
|
4
|
+
radius: number;
|
|
5
|
+
color: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PaintLayer {
|
|
8
|
+
paint(instanceId: string, stroke: PaintStroke): void;
|
|
9
|
+
strokes(instanceId: string): readonly PaintStroke[];
|
|
10
|
+
paintedIds(): readonly string[];
|
|
11
|
+
clear(instanceId?: string): void;
|
|
12
|
+
version(instanceId: string): number;
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createPaintLayer(): PaintLayer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function createPaintLayer() {
|
|
2
|
+
const strokesByInstance = new Map();
|
|
3
|
+
const versionByInstance = new Map();
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
function bumpVersion(instanceId) {
|
|
6
|
+
versionByInstance.set(instanceId, (versionByInstance.get(instanceId) ?? 0) + 1);
|
|
7
|
+
}
|
|
8
|
+
function notify() {
|
|
9
|
+
for (const listener of listeners)
|
|
10
|
+
listener();
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
paint(instanceId, stroke) {
|
|
14
|
+
const list = strokesByInstance.get(instanceId);
|
|
15
|
+
if (list === undefined)
|
|
16
|
+
strokesByInstance.set(instanceId, [stroke]);
|
|
17
|
+
else
|
|
18
|
+
list.push(stroke);
|
|
19
|
+
bumpVersion(instanceId);
|
|
20
|
+
notify();
|
|
21
|
+
},
|
|
22
|
+
strokes(instanceId) {
|
|
23
|
+
return strokesByInstance.get(instanceId) ?? [];
|
|
24
|
+
},
|
|
25
|
+
paintedIds() {
|
|
26
|
+
return Array.from(strokesByInstance.keys());
|
|
27
|
+
},
|
|
28
|
+
clear(instanceId) {
|
|
29
|
+
if (instanceId === undefined) {
|
|
30
|
+
if (strokesByInstance.size === 0)
|
|
31
|
+
return;
|
|
32
|
+
for (const id of strokesByInstance.keys())
|
|
33
|
+
bumpVersion(id);
|
|
34
|
+
strokesByInstance.clear();
|
|
35
|
+
notify();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!strokesByInstance.delete(instanceId))
|
|
39
|
+
return;
|
|
40
|
+
bumpVersion(instanceId);
|
|
41
|
+
notify();
|
|
42
|
+
},
|
|
43
|
+
version(instanceId) {
|
|
44
|
+
return versionByInstance.get(instanceId) ?? 0;
|
|
45
|
+
},
|
|
46
|
+
subscribe(listener) {
|
|
47
|
+
listeners.add(listener);
|
|
48
|
+
return () => {
|
|
49
|
+
listeners.delete(listener);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SceneEntity } from "./entityStore.js";
|
|
2
|
+
export interface PossessionSwappedEvent {
|
|
3
|
+
userId: string;
|
|
4
|
+
entityId: string;
|
|
5
|
+
previousEntityId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PossessionEntities {
|
|
8
|
+
get(id: string): SceneEntity | null;
|
|
9
|
+
update(id: string, patch: {
|
|
10
|
+
role?: SceneEntity["role"];
|
|
11
|
+
}): boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface PossessionEvents {
|
|
14
|
+
emit(name: "possession.swapped", payload: PossessionSwappedEvent): void;
|
|
15
|
+
}
|
|
16
|
+
export interface PossessionDeps {
|
|
17
|
+
entities: PossessionEntities;
|
|
18
|
+
events?: PossessionEvents;
|
|
19
|
+
}
|
|
20
|
+
export interface Possession {
|
|
21
|
+
own(userId: string, entityId: string): void;
|
|
22
|
+
disown(userId: string, entityId: string): void;
|
|
23
|
+
owns(userId: string, entityId: string): boolean;
|
|
24
|
+
listOwned(userId: string): readonly string[];
|
|
25
|
+
active(userId: string): string;
|
|
26
|
+
possess(userId: string, entityId: string): {
|
|
27
|
+
reason: string;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
export declare function createPossession(deps: PossessionDeps): Possession;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function createPossession(deps) {
|
|
2
|
+
const owned = new Map();
|
|
3
|
+
const activeByUser = new Map();
|
|
4
|
+
function ownedSet(userId) {
|
|
5
|
+
let set = owned.get(userId);
|
|
6
|
+
if (set === undefined) {
|
|
7
|
+
set = new Set([userId]);
|
|
8
|
+
owned.set(userId, set);
|
|
9
|
+
}
|
|
10
|
+
return set;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
own(userId, entityId) {
|
|
14
|
+
ownedSet(userId).add(entityId);
|
|
15
|
+
},
|
|
16
|
+
disown(userId, entityId) {
|
|
17
|
+
if (entityId === userId)
|
|
18
|
+
return;
|
|
19
|
+
ownedSet(userId).delete(entityId);
|
|
20
|
+
if (activeByUser.get(userId) === entityId)
|
|
21
|
+
activeByUser.delete(userId);
|
|
22
|
+
},
|
|
23
|
+
owns(userId, entityId) {
|
|
24
|
+
return ownedSet(userId).has(entityId);
|
|
25
|
+
},
|
|
26
|
+
listOwned(userId) {
|
|
27
|
+
return Array.from(ownedSet(userId));
|
|
28
|
+
},
|
|
29
|
+
active(userId) {
|
|
30
|
+
return activeByUser.get(userId) ?? userId;
|
|
31
|
+
},
|
|
32
|
+
possess(userId, entityId) {
|
|
33
|
+
if (!ownedSet(userId).has(entityId)) {
|
|
34
|
+
return { reason: `entity "${entityId}" is not owned by "${userId}"` };
|
|
35
|
+
}
|
|
36
|
+
if (deps.entities.get(entityId) === null) {
|
|
37
|
+
return { reason: `entity "${entityId}" is not spawned` };
|
|
38
|
+
}
|
|
39
|
+
const previousEntityId = activeByUser.get(userId) ?? userId;
|
|
40
|
+
if (previousEntityId === entityId)
|
|
41
|
+
return null;
|
|
42
|
+
activeByUser.set(userId, entityId);
|
|
43
|
+
if (deps.entities.get(previousEntityId) !== null) {
|
|
44
|
+
deps.entities.update(previousEntityId, { role: "npc" });
|
|
45
|
+
}
|
|
46
|
+
deps.entities.update(entityId, { role: "player" });
|
|
47
|
+
deps.events?.emit("possession.swapped", { userId, entityId, previousEntityId });
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface RosterEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
catalogId: string;
|
|
4
|
+
capturedAt: number;
|
|
5
|
+
equipped: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RosterCaptureOptions {
|
|
8
|
+
id?: string;
|
|
9
|
+
capturedAt?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Roster {
|
|
12
|
+
capture(userId: string, catalogId: string, options?: RosterCaptureOptions): RosterEntry;
|
|
13
|
+
release(userId: string, entryId: string): boolean;
|
|
14
|
+
has(userId: string, entryId: string): boolean;
|
|
15
|
+
get(userId: string, entryId: string): RosterEntry | null;
|
|
16
|
+
list(userId: string): readonly RosterEntry[];
|
|
17
|
+
equippedList(userId: string): readonly RosterEntry[];
|
|
18
|
+
setEquipped(userId: string, entryId: string, equipped: boolean): RosterEntry | null;
|
|
19
|
+
snapshot(userId: string): readonly RosterEntry[];
|
|
20
|
+
hydrate(userId: string, entries: readonly RosterEntry[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface RosterDeps {
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export declare function createRoster(deps?: RosterDeps): Roster;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createRoster(deps = {}) {
|
|
2
|
+
const now = deps.now ?? Date.now;
|
|
3
|
+
const rosters = new Map();
|
|
4
|
+
let counter = 0;
|
|
5
|
+
function requireEntries(userId) {
|
|
6
|
+
let entries = rosters.get(userId);
|
|
7
|
+
if (entries === undefined) {
|
|
8
|
+
entries = new Map();
|
|
9
|
+
rosters.set(userId, entries);
|
|
10
|
+
}
|
|
11
|
+
return entries;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
capture(userId, catalogId, options = {}) {
|
|
15
|
+
counter += 1;
|
|
16
|
+
const id = options.id ?? `roster_${counter}`;
|
|
17
|
+
const entry = {
|
|
18
|
+
id,
|
|
19
|
+
catalogId,
|
|
20
|
+
capturedAt: options.capturedAt ?? now(),
|
|
21
|
+
equipped: false,
|
|
22
|
+
};
|
|
23
|
+
requireEntries(userId).set(id, entry);
|
|
24
|
+
return entry;
|
|
25
|
+
},
|
|
26
|
+
release(userId, entryId) {
|
|
27
|
+
return rosters.get(userId)?.delete(entryId) ?? false;
|
|
28
|
+
},
|
|
29
|
+
has(userId, entryId) {
|
|
30
|
+
return rosters.get(userId)?.has(entryId) ?? false;
|
|
31
|
+
},
|
|
32
|
+
get(userId, entryId) {
|
|
33
|
+
return rosters.get(userId)?.get(entryId) ?? null;
|
|
34
|
+
},
|
|
35
|
+
list(userId) {
|
|
36
|
+
return Array.from(rosters.get(userId)?.values() ?? []);
|
|
37
|
+
},
|
|
38
|
+
equippedList(userId) {
|
|
39
|
+
return Array.from(rosters.get(userId)?.values() ?? []).filter((entry) => entry.equipped);
|
|
40
|
+
},
|
|
41
|
+
setEquipped(userId, entryId, equipped) {
|
|
42
|
+
const entries = rosters.get(userId);
|
|
43
|
+
const entry = entries?.get(entryId);
|
|
44
|
+
if (entries === undefined || entry === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
const next = { ...entry, equipped };
|
|
47
|
+
entries.set(entryId, next);
|
|
48
|
+
return next;
|
|
49
|
+
},
|
|
50
|
+
snapshot(userId) {
|
|
51
|
+
return Array.from(rosters.get(userId)?.values() ?? []);
|
|
52
|
+
},
|
|
53
|
+
hydrate(userId, entries) {
|
|
54
|
+
rosters.set(userId, new Map(entries.map((entry) => [entry.id, entry])));
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ScreenRect {
|
|
2
|
+
minX: number;
|
|
3
|
+
minY: number;
|
|
4
|
+
maxX: number;
|
|
5
|
+
maxY: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ScreenPoint {
|
|
8
|
+
id: string;
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
}
|
|
12
|
+
/** Normalize two drag corners (in any order) into a rectangle. */
|
|
13
|
+
export declare function screenRect(ax: number, ay: number, bx: number, by: number): ScreenRect;
|
|
14
|
+
export declare function rectContainsPoint(rect: ScreenRect, x: number, y: number): boolean;
|
|
15
|
+
/** Ids of the projected candidates whose screen point falls inside the marquee. */
|
|
16
|
+
export declare function selectWithinRect(candidates: readonly ScreenPoint[], rect: ScreenRect): string[];
|
|
17
|
+
/** True when the drag is large enough to be a marquee rather than a click. */
|
|
18
|
+
export declare function isMarquee(rect: ScreenRect, thresholdPx?: number): boolean;
|
|
19
|
+
export interface SelectionSet {
|
|
20
|
+
add(id: string): void;
|
|
21
|
+
remove(id: string): void;
|
|
22
|
+
toggle(id: string): void;
|
|
23
|
+
has(id: string): boolean;
|
|
24
|
+
replace(ids: Iterable<string>): void;
|
|
25
|
+
clear(): void;
|
|
26
|
+
list(): string[];
|
|
27
|
+
size(): number;
|
|
28
|
+
}
|
|
29
|
+
/** An ordered, deduplicated set of selected instance ids for RTS unit-command routing. */
|
|
30
|
+
export declare function createSelectionSet(initial?: Iterable<string>): SelectionSet;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Normalize two drag corners (in any order) into a rectangle. */
|
|
2
|
+
export function screenRect(ax, ay, bx, by) {
|
|
3
|
+
return {
|
|
4
|
+
minX: Math.min(ax, bx),
|
|
5
|
+
minY: Math.min(ay, by),
|
|
6
|
+
maxX: Math.max(ax, bx),
|
|
7
|
+
maxY: Math.max(ay, by),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function rectContainsPoint(rect, x, y) {
|
|
11
|
+
return x >= rect.minX && x <= rect.maxX && y >= rect.minY && y <= rect.maxY;
|
|
12
|
+
}
|
|
13
|
+
/** Ids of the projected candidates whose screen point falls inside the marquee. */
|
|
14
|
+
export function selectWithinRect(candidates, rect) {
|
|
15
|
+
return candidates.filter((candidate) => rectContainsPoint(rect, candidate.x, candidate.y)).map((c) => c.id);
|
|
16
|
+
}
|
|
17
|
+
/** True when the drag is large enough to be a marquee rather than a click. */
|
|
18
|
+
export function isMarquee(rect, thresholdPx = 4) {
|
|
19
|
+
return rect.maxX - rect.minX >= thresholdPx || rect.maxY - rect.minY >= thresholdPx;
|
|
20
|
+
}
|
|
21
|
+
/** An ordered, deduplicated set of selected instance ids for RTS unit-command routing. */
|
|
22
|
+
export function createSelectionSet(initial) {
|
|
23
|
+
const ids = new Set(initial);
|
|
24
|
+
return {
|
|
25
|
+
add: (id) => void ids.add(id),
|
|
26
|
+
remove: (id) => void ids.delete(id),
|
|
27
|
+
toggle: (id) => (ids.has(id) ? ids.delete(id) : ids.add(id)),
|
|
28
|
+
has: (id) => ids.has(id),
|
|
29
|
+
replace(next) {
|
|
30
|
+
ids.clear();
|
|
31
|
+
for (const id of next)
|
|
32
|
+
ids.add(id);
|
|
33
|
+
},
|
|
34
|
+
clear: () => ids.clear(),
|
|
35
|
+
list: () => Array.from(ids),
|
|
36
|
+
size: () => ids.size,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/scene/spatial.d.ts
CHANGED
|
@@ -18,10 +18,22 @@ export interface MoveTowardOptions {
|
|
|
18
18
|
stopDistance?: number;
|
|
19
19
|
dt: number;
|
|
20
20
|
}
|
|
21
|
+
export interface SpatialGridOptions {
|
|
22
|
+
/** Cell edge over the x/z plane; tune to ≈ the typical query radius so a query touches a handful of cells. */
|
|
23
|
+
cellSize: number;
|
|
24
|
+
}
|
|
21
25
|
export interface SpatialApiOptions {
|
|
22
26
|
resolvePosition: (instanceId: string) => EntityPosition | undefined;
|
|
23
27
|
candidates: () => string[];
|
|
24
28
|
occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Opt-in broadphase acceleration for `inRadius`/`queryArc` over large candidate sets. When set,
|
|
31
|
+
* a uniform x/z cell index is built lazily from `candidates()` + `resolvePosition()` on first use
|
|
32
|
+
* and reused across calls until `invalidate()` is called — so many queries against one tick's
|
|
33
|
+
* frozen positions pay for one rebuild, not one linear scan each. Results are exactly the linear
|
|
34
|
+
* scan's results (same filtering; see `SpatialApi.invalidate` for the staleness contract).
|
|
35
|
+
*/
|
|
36
|
+
grid?: SpatialGridOptions;
|
|
25
37
|
}
|
|
26
38
|
export interface SpatialApi {
|
|
27
39
|
distance(aInstanceId: string, bInstanceId: string): number | null;
|
|
@@ -29,6 +41,15 @@ export interface SpatialApi {
|
|
|
29
41
|
hasLineOfSight(fromInstanceId: string, toInstanceId: string): boolean;
|
|
30
42
|
queryArc(options: QueryArcOptions): string[];
|
|
31
43
|
moveToward(instanceId: string, target: EntityPosition | string, options: MoveTowardOptions): EntityPosition | null;
|
|
44
|
+
/**
|
|
45
|
+
* Marks the `grid` broadphase index stale so the next `inRadius`/`queryArc` call rebuilds it from
|
|
46
|
+
* `candidates()` and `resolvePosition()`. Call this after moving, spawning, or despawning a
|
|
47
|
+
* candidate; a candidate absent from the index at query time is never skipped (it falls back to
|
|
48
|
+
* an exact check), but a candidate that *moved* since the last rebuild can be missed until you
|
|
49
|
+
* invalidate — so treat "invalidate after any position change" as the contract, not an
|
|
50
|
+
* optimization. A no-op when no `grid` config was supplied. Safe to call every tick.
|
|
51
|
+
*/
|
|
52
|
+
invalidate(): void;
|
|
32
53
|
}
|
|
33
54
|
export declare function distanceBetween(a: EntityPosition, b: EntityPosition): number;
|
|
34
55
|
export declare function createSpatialApi(options: SpatialApiOptions): SpatialApi;
|