@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
package/dist/scene/spatial.js
CHANGED
|
@@ -14,11 +14,62 @@ function forwardXZ(aim) {
|
|
|
14
14
|
}
|
|
15
15
|
return [Math.sin(aim.yaw), Math.cos(aim.yaw)];
|
|
16
16
|
}
|
|
17
|
+
function cellCoord(value, cellSize) {
|
|
18
|
+
return Math.floor(value / cellSize);
|
|
19
|
+
}
|
|
20
|
+
function gridCellKey(cx, cz) {
|
|
21
|
+
return `${cx}:${cz}`;
|
|
22
|
+
}
|
|
17
23
|
export function createSpatialApi(options) {
|
|
18
|
-
const { resolvePosition, candidates, occluder } = options;
|
|
24
|
+
const { resolvePosition, candidates, occluder, grid } = options;
|
|
25
|
+
const cellSize = grid?.cellSize;
|
|
26
|
+
let gridIndex = null;
|
|
27
|
+
let gridDirty = true;
|
|
19
28
|
function resolveTarget(target) {
|
|
20
29
|
return typeof target === "string" ? resolvePosition(target) : target;
|
|
21
30
|
}
|
|
31
|
+
function ensureGrid() {
|
|
32
|
+
if (cellSize === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
if (!gridDirty && gridIndex !== null)
|
|
35
|
+
return gridIndex;
|
|
36
|
+
const cells = new Map();
|
|
37
|
+
const indexed = new Set();
|
|
38
|
+
for (const instanceId of candidates()) {
|
|
39
|
+
const position = resolvePosition(instanceId);
|
|
40
|
+
if (position === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
indexed.add(instanceId);
|
|
43
|
+
const key = gridCellKey(cellCoord(position[0], cellSize), cellCoord(position[2], cellSize));
|
|
44
|
+
let bucket = cells.get(key);
|
|
45
|
+
if (bucket === undefined) {
|
|
46
|
+
bucket = [];
|
|
47
|
+
cells.set(key, bucket);
|
|
48
|
+
}
|
|
49
|
+
bucket.push(instanceId);
|
|
50
|
+
}
|
|
51
|
+
gridIndex = { cells, indexed };
|
|
52
|
+
gridDirty = false;
|
|
53
|
+
return gridIndex;
|
|
54
|
+
}
|
|
55
|
+
function inRangeSet(index, centerX, centerZ, radius) {
|
|
56
|
+
const size = cellSize;
|
|
57
|
+
const cxMin = cellCoord(centerX - radius, size);
|
|
58
|
+
const cxMax = cellCoord(centerX + radius, size);
|
|
59
|
+
const czMin = cellCoord(centerZ - radius, size);
|
|
60
|
+
const czMax = cellCoord(centerZ + radius, size);
|
|
61
|
+
const near = new Set();
|
|
62
|
+
for (let cz = czMin; cz <= czMax; cz += 1) {
|
|
63
|
+
for (let cx = cxMin; cx <= cxMax; cx += 1) {
|
|
64
|
+
const bucket = index.cells.get(gridCellKey(cx, cz));
|
|
65
|
+
if (bucket === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
for (const id of bucket)
|
|
68
|
+
near.add(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return near;
|
|
72
|
+
}
|
|
22
73
|
return {
|
|
23
74
|
distance(aInstanceId, bInstanceId) {
|
|
24
75
|
const a = resolvePosition(aInstanceId);
|
|
@@ -32,11 +83,15 @@ export function createSpatialApi(options) {
|
|
|
32
83
|
const centerPosition = resolveTarget(center);
|
|
33
84
|
if (centerPosition === undefined)
|
|
34
85
|
return [];
|
|
86
|
+
const index = ensureGrid();
|
|
87
|
+
const near = index === null ? null : inRangeSet(index, centerPosition[0], centerPosition[2], radius);
|
|
35
88
|
return candidates().filter((instanceId) => {
|
|
36
89
|
if (instanceId === centerId)
|
|
37
90
|
return false;
|
|
38
91
|
if (filter !== undefined && !filter(instanceId))
|
|
39
92
|
return false;
|
|
93
|
+
if (near !== null && index.indexed.has(instanceId) && !near.has(instanceId))
|
|
94
|
+
return false;
|
|
40
95
|
const position = resolvePosition(instanceId);
|
|
41
96
|
return position !== undefined && distanceBetween(centerPosition, position) <= radius;
|
|
42
97
|
});
|
|
@@ -58,9 +113,13 @@ export function createSpatialApi(options) {
|
|
|
58
113
|
if (forward === null)
|
|
59
114
|
return [];
|
|
60
115
|
const minDot = Math.cos((halfAngleDeg * Math.PI) / 180);
|
|
116
|
+
const index = ensureGrid();
|
|
117
|
+
const near = index === null ? null : inRangeSet(index, origin[0], origin[2], radius);
|
|
61
118
|
return candidates().filter((instanceId) => {
|
|
62
119
|
if (instanceId === from)
|
|
63
120
|
return false;
|
|
121
|
+
if (near !== null && index.indexed.has(instanceId) && !near.has(instanceId))
|
|
122
|
+
return false;
|
|
64
123
|
const position = resolvePosition(instanceId);
|
|
65
124
|
if (position === undefined)
|
|
66
125
|
return false;
|
|
@@ -75,6 +134,9 @@ export function createSpatialApi(options) {
|
|
|
75
134
|
return dot >= minDot;
|
|
76
135
|
});
|
|
77
136
|
},
|
|
137
|
+
invalidate() {
|
|
138
|
+
gridDirty = true;
|
|
139
|
+
},
|
|
78
140
|
moveToward(instanceId, target, { speed, stopDistance = 0, dt }) {
|
|
79
141
|
const current = resolvePosition(instanceId);
|
|
80
142
|
const destination = resolveTarget(target);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MountController, type MountKit } from "./mount.js";
|
|
2
|
+
export interface Station {
|
|
3
|
+
id: string;
|
|
4
|
+
facet: string;
|
|
5
|
+
offset: readonly [number, number, number];
|
|
6
|
+
control?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SharedVehicleConfig {
|
|
9
|
+
id: string;
|
|
10
|
+
kit: MountKit;
|
|
11
|
+
stations: readonly Station[];
|
|
12
|
+
}
|
|
13
|
+
export type ClaimResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
station: Station;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
reason: "unknown_vehicle" | "unknown_facet" | "station_taken" | "vehicle_full";
|
|
19
|
+
};
|
|
20
|
+
export declare class StationClaim {
|
|
21
|
+
private readonly controller;
|
|
22
|
+
private readonly vehicles;
|
|
23
|
+
constructor(controller?: MountController);
|
|
24
|
+
register(config: SharedVehicleConfig): void;
|
|
25
|
+
private resolveStation;
|
|
26
|
+
claim(playerId: string, vehicleId: string, facetOrStationId: string): ClaimResult;
|
|
27
|
+
release(playerId: string): string | null;
|
|
28
|
+
facetOf(playerId: string): string | null;
|
|
29
|
+
stationOf(playerId: string): Station | null;
|
|
30
|
+
controllerOf(vehicleId: string, facet: string): string | null;
|
|
31
|
+
crew(vehicleId: string): {
|
|
32
|
+
playerId: string;
|
|
33
|
+
stationId: string;
|
|
34
|
+
facet: string;
|
|
35
|
+
}[];
|
|
36
|
+
stationsOf(vehicleId: string): readonly Station[];
|
|
37
|
+
openFacets(vehicleId: string): string[];
|
|
38
|
+
driver(vehicleId: string): string | null;
|
|
39
|
+
cameraTarget(playerId: string): string;
|
|
40
|
+
driveTarget(playerId: string): string | null;
|
|
41
|
+
get mounts(): MountController;
|
|
42
|
+
}
|
|
43
|
+
export declare function createStationClaim(controller?: MountController): StationClaim;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createMountController, MountController } from "./mount.js";
|
|
2
|
+
export class StationClaim {
|
|
3
|
+
controller;
|
|
4
|
+
vehicles = new Map();
|
|
5
|
+
constructor(controller) {
|
|
6
|
+
this.controller = controller ?? createMountController();
|
|
7
|
+
}
|
|
8
|
+
register(config) {
|
|
9
|
+
const byStationId = new Map();
|
|
10
|
+
const facetToStation = new Map();
|
|
11
|
+
for (const station of config.stations) {
|
|
12
|
+
byStationId.set(station.id, station);
|
|
13
|
+
if (!facetToStation.has(station.facet))
|
|
14
|
+
facetToStation.set(station.facet, station.id);
|
|
15
|
+
}
|
|
16
|
+
this.vehicles.set(config.id, { config, byStationId, facetToStation });
|
|
17
|
+
this.controller.register({
|
|
18
|
+
id: config.id,
|
|
19
|
+
kit: config.kit,
|
|
20
|
+
seats: config.stations.map((station) => ({
|
|
21
|
+
id: station.id,
|
|
22
|
+
offset: station.offset,
|
|
23
|
+
control: station.control,
|
|
24
|
+
})),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
resolveStation(vehicle, facetOrStationId) {
|
|
28
|
+
const direct = vehicle.byStationId.get(facetOrStationId);
|
|
29
|
+
if (direct !== undefined)
|
|
30
|
+
return direct;
|
|
31
|
+
const stationId = vehicle.facetToStation.get(facetOrStationId);
|
|
32
|
+
if (stationId === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
return vehicle.byStationId.get(stationId) ?? null;
|
|
35
|
+
}
|
|
36
|
+
claim(playerId, vehicleId, facetOrStationId) {
|
|
37
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
38
|
+
if (vehicle === undefined)
|
|
39
|
+
return { ok: false, reason: "unknown_vehicle" };
|
|
40
|
+
const station = this.resolveStation(vehicle, facetOrStationId);
|
|
41
|
+
if (station === null)
|
|
42
|
+
return { ok: false, reason: "unknown_facet" };
|
|
43
|
+
const result = this.controller.mount(playerId, vehicleId, station.id);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
return { ok: false, reason: result.reason === "seat_taken" ? "station_taken" : "vehicle_full" };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, station };
|
|
48
|
+
}
|
|
49
|
+
release(playerId) {
|
|
50
|
+
return this.controller.dismount(playerId);
|
|
51
|
+
}
|
|
52
|
+
facetOf(playerId) {
|
|
53
|
+
const ref = this.controller.seatOf(playerId);
|
|
54
|
+
if (ref === null)
|
|
55
|
+
return null;
|
|
56
|
+
return this.vehicles.get(ref.mountId)?.byStationId.get(ref.seatId)?.facet ?? null;
|
|
57
|
+
}
|
|
58
|
+
stationOf(playerId) {
|
|
59
|
+
const ref = this.controller.seatOf(playerId);
|
|
60
|
+
if (ref === null)
|
|
61
|
+
return null;
|
|
62
|
+
return this.vehicles.get(ref.mountId)?.byStationId.get(ref.seatId) ?? null;
|
|
63
|
+
}
|
|
64
|
+
controllerOf(vehicleId, facet) {
|
|
65
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
66
|
+
if (vehicle === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
for (const occupant of this.controller.occupants(vehicleId)) {
|
|
69
|
+
if (vehicle.byStationId.get(occupant.seatId)?.facet === facet)
|
|
70
|
+
return occupant.riderId;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
crew(vehicleId) {
|
|
75
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
76
|
+
if (vehicle === undefined)
|
|
77
|
+
return [];
|
|
78
|
+
return this.controller.occupants(vehicleId).map((occupant) => ({
|
|
79
|
+
playerId: occupant.riderId,
|
|
80
|
+
stationId: occupant.seatId,
|
|
81
|
+
facet: vehicle.byStationId.get(occupant.seatId)?.facet ?? "",
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
stationsOf(vehicleId) {
|
|
85
|
+
return this.vehicles.get(vehicleId)?.config.stations ?? [];
|
|
86
|
+
}
|
|
87
|
+
openFacets(vehicleId) {
|
|
88
|
+
const vehicle = this.vehicles.get(vehicleId);
|
|
89
|
+
if (vehicle === undefined)
|
|
90
|
+
return [];
|
|
91
|
+
const taken = new Set(this.controller.occupants(vehicleId).map((o) => o.seatId));
|
|
92
|
+
return vehicle.config.stations.filter((s) => !taken.has(s.id)).map((s) => s.facet);
|
|
93
|
+
}
|
|
94
|
+
driver(vehicleId) {
|
|
95
|
+
return this.controller.driver(vehicleId);
|
|
96
|
+
}
|
|
97
|
+
cameraTarget(playerId) {
|
|
98
|
+
return this.controller.cameraTarget(playerId);
|
|
99
|
+
}
|
|
100
|
+
driveTarget(playerId) {
|
|
101
|
+
return this.controller.driveTarget(playerId);
|
|
102
|
+
}
|
|
103
|
+
get mounts() {
|
|
104
|
+
return this.controller;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function createStationClaim(controller) {
|
|
108
|
+
return new StationClaim(controller);
|
|
109
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ColorHex = string;
|
|
2
|
+
export declare function colorDistance(a: ColorHex, b: ColorHex): number;
|
|
3
|
+
export declare function concealmentScore(entityColors: readonly ColorHex[], backgroundColors: readonly ColorHex[]): number;
|
|
4
|
+
export interface ConcealmentTarget {
|
|
5
|
+
id: string;
|
|
6
|
+
entityColors: readonly ColorHex[];
|
|
7
|
+
backgroundColors: readonly ColorHex[];
|
|
8
|
+
}
|
|
9
|
+
export interface ConcealmentSample {
|
|
10
|
+
id: string;
|
|
11
|
+
score: number;
|
|
12
|
+
concealed: boolean;
|
|
13
|
+
dwellSeconds: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ConcealmentSensor {
|
|
16
|
+
tick(targets: readonly ConcealmentTarget[], dt: number): ConcealmentSample[];
|
|
17
|
+
reset(id?: string): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createConcealmentSensor(config?: {
|
|
20
|
+
threshold?: number;
|
|
21
|
+
}): ConcealmentSensor;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function parseHex(hex) {
|
|
2
|
+
const match = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim());
|
|
3
|
+
if (match === null)
|
|
4
|
+
return [0, 0, 0];
|
|
5
|
+
const value = parseInt(match[1], 16);
|
|
6
|
+
return [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];
|
|
7
|
+
}
|
|
8
|
+
function redmean(a, b) {
|
|
9
|
+
const rmean = (a[0] + b[0]) / 2;
|
|
10
|
+
const dr = a[0] - b[0];
|
|
11
|
+
const dg = a[1] - b[1];
|
|
12
|
+
const db = a[2] - b[2];
|
|
13
|
+
return Math.sqrt((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db);
|
|
14
|
+
}
|
|
15
|
+
const REDMEAN_MAX = redmean([0, 0, 0], [255, 255, 255]);
|
|
16
|
+
export function colorDistance(a, b) {
|
|
17
|
+
const distance = redmean(parseHex(a), parseHex(b));
|
|
18
|
+
return REDMEAN_MAX === 0 ? 0 : Math.min(1, distance / REDMEAN_MAX);
|
|
19
|
+
}
|
|
20
|
+
export function concealmentScore(entityColors, backgroundColors) {
|
|
21
|
+
if (entityColors.length === 0 || backgroundColors.length === 0)
|
|
22
|
+
return 0;
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const entityColor of entityColors) {
|
|
25
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
26
|
+
for (const backgroundColor of backgroundColors) {
|
|
27
|
+
const distance = colorDistance(entityColor, backgroundColor);
|
|
28
|
+
if (distance < nearest)
|
|
29
|
+
nearest = distance;
|
|
30
|
+
}
|
|
31
|
+
total += nearest;
|
|
32
|
+
}
|
|
33
|
+
return 1 - total / entityColors.length;
|
|
34
|
+
}
|
|
35
|
+
export function createConcealmentSensor(config) {
|
|
36
|
+
const threshold = config?.threshold ?? 0.7;
|
|
37
|
+
const dwell = new Map();
|
|
38
|
+
return {
|
|
39
|
+
tick(targets, dt) {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const samples = [];
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
seen.add(target.id);
|
|
44
|
+
const score = concealmentScore(target.entityColors, target.backgroundColors);
|
|
45
|
+
const concealed = score >= threshold;
|
|
46
|
+
const nextDwell = concealed ? (dwell.get(target.id) ?? 0) + dt : 0;
|
|
47
|
+
dwell.set(target.id, nextDwell);
|
|
48
|
+
samples.push({ id: target.id, score, concealed, dwellSeconds: nextDwell });
|
|
49
|
+
}
|
|
50
|
+
for (const id of dwell.keys()) {
|
|
51
|
+
if (!seen.has(id))
|
|
52
|
+
dwell.delete(id);
|
|
53
|
+
}
|
|
54
|
+
return samples;
|
|
55
|
+
},
|
|
56
|
+
reset(id) {
|
|
57
|
+
if (id === undefined)
|
|
58
|
+
dwell.clear();
|
|
59
|
+
else
|
|
60
|
+
dwell.delete(id);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface FreezeSubject {
|
|
2
|
+
id: string;
|
|
3
|
+
groundSpeed: number;
|
|
4
|
+
}
|
|
5
|
+
export interface FreezeViolation {
|
|
6
|
+
id: string;
|
|
7
|
+
speed: number;
|
|
8
|
+
movedSeconds: number;
|
|
9
|
+
}
|
|
10
|
+
export interface FreezeMonitor {
|
|
11
|
+
tick(subjects: readonly FreezeSubject[], frozenIds: ReadonlySet<string>, dt: number): FreezeViolation[];
|
|
12
|
+
reset(id?: string): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createFreezeMonitor(config?: {
|
|
15
|
+
toleranceSpeed?: number;
|
|
16
|
+
graceSeconds?: number;
|
|
17
|
+
}): FreezeMonitor;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function createFreezeMonitor(config) {
|
|
2
|
+
const toleranceSpeed = config?.toleranceSpeed ?? 0.05;
|
|
3
|
+
const graceSeconds = config?.graceSeconds ?? 0;
|
|
4
|
+
const moved = new Map();
|
|
5
|
+
return {
|
|
6
|
+
tick(subjects, frozenIds, dt) {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
const violations = [];
|
|
9
|
+
for (const subject of subjects) {
|
|
10
|
+
seen.add(subject.id);
|
|
11
|
+
const shouldHoldStill = frozenIds.has(subject.id) && subject.groundSpeed > toleranceSpeed;
|
|
12
|
+
if (!shouldHoldStill) {
|
|
13
|
+
moved.delete(subject.id);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const movedSeconds = (moved.get(subject.id) ?? 0) + dt;
|
|
17
|
+
moved.set(subject.id, movedSeconds);
|
|
18
|
+
if (movedSeconds > graceSeconds) {
|
|
19
|
+
violations.push({ id: subject.id, speed: subject.groundSpeed, movedSeconds });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const id of moved.keys()) {
|
|
23
|
+
if (!seen.has(id))
|
|
24
|
+
moved.delete(id);
|
|
25
|
+
}
|
|
26
|
+
return violations;
|
|
27
|
+
},
|
|
28
|
+
reset(id) {
|
|
29
|
+
if (id === undefined)
|
|
30
|
+
moved.clear();
|
|
31
|
+
else
|
|
32
|
+
moved.delete(id);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export interface FrustumCamera {
|
|
3
|
+
position: EntityPosition;
|
|
4
|
+
lookAt: EntityPosition;
|
|
5
|
+
/** Vertical field of view in degrees. Default 55. */
|
|
6
|
+
fovDeg?: number;
|
|
7
|
+
/** Width / height. Default 16 / 9. */
|
|
8
|
+
aspect?: number;
|
|
9
|
+
near?: number;
|
|
10
|
+
far?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FrustumTarget {
|
|
13
|
+
id: string;
|
|
14
|
+
position: EntityPosition;
|
|
15
|
+
/** Subject radius in world units, used by the framing-size score. Default 0.5. */
|
|
16
|
+
radius?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface FrustumProjection {
|
|
19
|
+
inView: boolean;
|
|
20
|
+
distance: number;
|
|
21
|
+
/** -1..1, 0 = screen center, negative = left/down. */
|
|
22
|
+
screenX: number;
|
|
23
|
+
screenY: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Project a world point into the camera's view. Pure geometry — no three.js —
|
|
27
|
+
* so a game can run "is this framed?" checks headlessly (photo-mode scoring,
|
|
28
|
+
* server-side kill-cam validation) as well as from the live render camera.
|
|
29
|
+
*/
|
|
30
|
+
export declare function projectToView(camera: FrustumCamera, point: EntityPosition): FrustumProjection;
|
|
31
|
+
export interface FramingConfig {
|
|
32
|
+
/** World-unit distance from camera that reads as "ideally framed" size. Default 6. */
|
|
33
|
+
idealDistance?: number;
|
|
34
|
+
/** Weight of screen-center placement in the framing score. Default 0.6. */
|
|
35
|
+
centerWeight?: number;
|
|
36
|
+
/** Weight of subject size (distance-to-ideal) in the framing score. Default 0.4. */
|
|
37
|
+
sizeWeight?: number;
|
|
38
|
+
}
|
|
39
|
+
/** 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection. */
|
|
40
|
+
export declare function framingScore(projection: FrustumProjection, config?: FramingConfig): number;
|
|
41
|
+
export interface FrustumSample {
|
|
42
|
+
id: string;
|
|
43
|
+
inView: boolean;
|
|
44
|
+
distance: number;
|
|
45
|
+
screenX: number;
|
|
46
|
+
screenY: number;
|
|
47
|
+
framing: number;
|
|
48
|
+
/** Seconds continuously in view, up to and including this tick; resets to 0 the instant the subject leaves view. */
|
|
49
|
+
dwellSeconds: number;
|
|
50
|
+
}
|
|
51
|
+
export interface FrustumSensor {
|
|
52
|
+
tick(camera: FrustumCamera, targets: readonly FrustumTarget[], dt: number): FrustumSample[];
|
|
53
|
+
reset(id?: string): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A view-frustum sensor on a held camera object: which entities are in frame,
|
|
57
|
+
* how well framed they are, and how long they've stayed on-screen (photo-mode
|
|
58
|
+
* "is this subject framed", Content Warning-style monster-filming scoring).
|
|
59
|
+
*/
|
|
60
|
+
export declare function createFrustumSensor(config?: FramingConfig): FrustumSensor;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
function subtract(a, b) {
|
|
2
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
3
|
+
}
|
|
4
|
+
function length(v) {
|
|
5
|
+
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
6
|
+
}
|
|
7
|
+
function normalize(v) {
|
|
8
|
+
const len = length(v);
|
|
9
|
+
return len === 0 ? [0, 0, 0] : [v[0] / len, v[1] / len, v[2] / len];
|
|
10
|
+
}
|
|
11
|
+
function cross(a, b) {
|
|
12
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
13
|
+
}
|
|
14
|
+
function dot(a, b) {
|
|
15
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
16
|
+
}
|
|
17
|
+
const WORLD_UP = [0, 1, 0];
|
|
18
|
+
/**
|
|
19
|
+
* Project a world point into the camera's view. Pure geometry — no three.js —
|
|
20
|
+
* so a game can run "is this framed?" checks headlessly (photo-mode scoring,
|
|
21
|
+
* server-side kill-cam validation) as well as from the live render camera.
|
|
22
|
+
*/
|
|
23
|
+
export function projectToView(camera, point) {
|
|
24
|
+
const forward = normalize(subtract(camera.lookAt, camera.position));
|
|
25
|
+
const relative = subtract(point, camera.position);
|
|
26
|
+
const distance = length(relative);
|
|
27
|
+
const depth = dot(relative, forward);
|
|
28
|
+
const near = camera.near ?? 0.1;
|
|
29
|
+
const far = camera.far ?? Number.POSITIVE_INFINITY;
|
|
30
|
+
if (forward[0] === 0 && forward[1] === 0 && forward[2] === 0) {
|
|
31
|
+
return { inView: false, distance, screenX: 0, screenY: 0 };
|
|
32
|
+
}
|
|
33
|
+
if (depth <= near || depth >= far) {
|
|
34
|
+
return { inView: false, distance, screenX: 0, screenY: 0 };
|
|
35
|
+
}
|
|
36
|
+
let right = normalize(cross(forward, WORLD_UP));
|
|
37
|
+
if (right[0] === 0 && right[1] === 0 && right[2] === 0) {
|
|
38
|
+
right = normalize(cross(forward, [1, 0, 0]));
|
|
39
|
+
}
|
|
40
|
+
const up = cross(right, forward);
|
|
41
|
+
const fovDeg = camera.fovDeg ?? 55;
|
|
42
|
+
const aspect = camera.aspect ?? 16 / 9;
|
|
43
|
+
const halfHeight = Math.tan((fovDeg * Math.PI) / 360);
|
|
44
|
+
const halfWidth = halfHeight * aspect;
|
|
45
|
+
const screenX = dot(relative, right) / (depth * halfWidth);
|
|
46
|
+
const screenY = dot(relative, up) / (depth * halfHeight);
|
|
47
|
+
const inView = Math.abs(screenX) <= 1 && Math.abs(screenY) <= 1;
|
|
48
|
+
return { inView, distance, screenX, screenY };
|
|
49
|
+
}
|
|
50
|
+
/** 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection. */
|
|
51
|
+
export function framingScore(projection, config) {
|
|
52
|
+
if (!projection.inView)
|
|
53
|
+
return 0;
|
|
54
|
+
const idealDistance = config?.idealDistance ?? 6;
|
|
55
|
+
const centerWeight = config?.centerWeight ?? 0.6;
|
|
56
|
+
const sizeWeight = config?.sizeWeight ?? 0.4;
|
|
57
|
+
const radial = Math.min(1, Math.sqrt(projection.screenX * projection.screenX + projection.screenY * projection.screenY));
|
|
58
|
+
const centerScore = 1 - radial;
|
|
59
|
+
const sizeScore = idealDistance <= 0
|
|
60
|
+
? 0
|
|
61
|
+
: Math.max(0, 1 - Math.abs(projection.distance - idealDistance) / idealDistance);
|
|
62
|
+
return Math.max(0, Math.min(1, centerWeight * centerScore + sizeWeight * sizeScore));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A view-frustum sensor on a held camera object: which entities are in frame,
|
|
66
|
+
* how well framed they are, and how long they've stayed on-screen (photo-mode
|
|
67
|
+
* "is this subject framed", Content Warning-style monster-filming scoring).
|
|
68
|
+
*/
|
|
69
|
+
export function createFrustumSensor(config) {
|
|
70
|
+
const dwell = new Map();
|
|
71
|
+
return {
|
|
72
|
+
tick(camera, targets, dt) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const samples = [];
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
seen.add(target.id);
|
|
77
|
+
const projection = projectToView(camera, target.position);
|
|
78
|
+
const framing = framingScore(projection, config);
|
|
79
|
+
const nextDwell = projection.inView ? (dwell.get(target.id) ?? 0) + dt : 0;
|
|
80
|
+
dwell.set(target.id, nextDwell);
|
|
81
|
+
samples.push({
|
|
82
|
+
id: target.id,
|
|
83
|
+
inView: projection.inView,
|
|
84
|
+
distance: projection.distance,
|
|
85
|
+
screenX: projection.screenX,
|
|
86
|
+
screenY: projection.screenY,
|
|
87
|
+
framing,
|
|
88
|
+
dwellSeconds: nextDwell,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const id of dwell.keys()) {
|
|
92
|
+
if (!seen.has(id))
|
|
93
|
+
dwell.delete(id);
|
|
94
|
+
}
|
|
95
|
+
return samples;
|
|
96
|
+
},
|
|
97
|
+
reset(id) {
|
|
98
|
+
if (id === undefined)
|
|
99
|
+
dwell.clear();
|
|
100
|
+
else
|
|
101
|
+
dwell.delete(id);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export type HiddenStateValue = number | string | boolean;
|
|
3
|
+
export interface HiddenStateSource {
|
|
4
|
+
id: string;
|
|
5
|
+
position: EntityPosition;
|
|
6
|
+
variables: Readonly<Record<string, HiddenStateValue>>;
|
|
7
|
+
}
|
|
8
|
+
export interface SensorProbeOptions {
|
|
9
|
+
range: number;
|
|
10
|
+
variableId: string;
|
|
11
|
+
/** How reading `strength` falls off with distance. Default "linear". */
|
|
12
|
+
falloff?: "linear" | "none";
|
|
13
|
+
}
|
|
14
|
+
export interface SensorReading {
|
|
15
|
+
sourceId: string;
|
|
16
|
+
variableId: string;
|
|
17
|
+
value: HiddenStateValue;
|
|
18
|
+
/** 1 at the source, 0 at the edge of `range` ("none" falloff always reads 1 in range). */
|
|
19
|
+
strength: number;
|
|
20
|
+
distance: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A sensor verb: probe a hidden zone/entity state variable in range and surface
|
|
24
|
+
* a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike
|
|
25
|
+
* `proximityPrompt` (a UI affordance), this reads world state the player has no
|
|
26
|
+
* other way to see — the strongest in-range reading wins, mirroring how a real
|
|
27
|
+
* handheld sensor needle settles on the loudest nearby source.
|
|
28
|
+
*/
|
|
29
|
+
export declare function probeHiddenState(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading | null;
|
|
30
|
+
/** All in-range readings for `variableId`, strongest first. */
|
|
31
|
+
export declare function probeHiddenStateAll(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { distanceBetween } from "../scene/spatial.js";
|
|
2
|
+
function strengthAt(distance, range, falloff) {
|
|
3
|
+
if (falloff === "none")
|
|
4
|
+
return 1;
|
|
5
|
+
if (range <= 0)
|
|
6
|
+
return 0;
|
|
7
|
+
return Math.max(0, 1 - distance / range);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A sensor verb: probe a hidden zone/entity state variable in range and surface
|
|
11
|
+
* a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike
|
|
12
|
+
* `proximityPrompt` (a UI affordance), this reads world state the player has no
|
|
13
|
+
* other way to see — the strongest in-range reading wins, mirroring how a real
|
|
14
|
+
* handheld sensor needle settles on the loudest nearby source.
|
|
15
|
+
*/
|
|
16
|
+
export function probeHiddenState(origin, sources, options) {
|
|
17
|
+
const readings = probeHiddenStateAll(origin, sources, options);
|
|
18
|
+
return readings.length === 0 ? null : readings[0];
|
|
19
|
+
}
|
|
20
|
+
/** All in-range readings for `variableId`, strongest first. */
|
|
21
|
+
export function probeHiddenStateAll(origin, sources, options) {
|
|
22
|
+
const { range, variableId, falloff = "linear" } = options;
|
|
23
|
+
const readings = [];
|
|
24
|
+
for (const source of sources) {
|
|
25
|
+
if (!(variableId in source.variables))
|
|
26
|
+
continue;
|
|
27
|
+
const distance = distanceBetween(origin, source.position);
|
|
28
|
+
if (distance > range)
|
|
29
|
+
continue;
|
|
30
|
+
readings.push({
|
|
31
|
+
sourceId: source.id,
|
|
32
|
+
variableId,
|
|
33
|
+
value: source.variables[variableId],
|
|
34
|
+
strength: strengthAt(distance, range, falloff),
|
|
35
|
+
distance,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
readings.sort((a, b) => b.strength - a.strength);
|
|
39
|
+
return readings;
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RecordingFrame<T> {
|
|
2
|
+
/** Game-time seconds (from `ctx.time`), so pause/fast-forward scrub the recording consistently with the sim. */
|
|
3
|
+
t: number;
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
export interface RecordingBufferOptions {
|
|
7
|
+
/** Evict frames older than `latestT - maxDurationSeconds`. Default unlimited. */
|
|
8
|
+
maxDurationSeconds?: number;
|
|
9
|
+
/** Hard cap on frame count, oldest evicted first. Default unlimited. */
|
|
10
|
+
maxFrames?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RecordingBuffer<T> {
|
|
13
|
+
append(t: number, data: T): void;
|
|
14
|
+
/** Nearest frame at or before `t`, or null if the buffer is empty or `t` precedes every frame. */
|
|
15
|
+
seek(t: number): RecordingFrame<T> | null;
|
|
16
|
+
/** Frames with `fromT <= t <= toT`, in recorded order. */
|
|
17
|
+
range(fromT: number, toT: number): RecordingFrame<T>[];
|
|
18
|
+
clear(): void;
|
|
19
|
+
duration(): number;
|
|
20
|
+
frames(): readonly RecordingFrame<T>[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A session-recording buffer for replay / photo mode / kill-cam: append
|
|
24
|
+
* timestamped snapshots (poses, camera state, whatever `T` is) on game-time,
|
|
25
|
+
* then seek/scrub the recording independent of the live sim. Frames are
|
|
26
|
+
* expected in non-decreasing `t` order (one push per tick); `seek`/`range`
|
|
27
|
+
* binary-search on that assumption.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createRecordingBuffer<T>(options?: RecordingBufferOptions): RecordingBuffer<T>;
|