@jgengine/core 0.7.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 +63 -0
- package/README.md +2 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +14 -2
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- 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 +22 -2
- package/dist/combat/abilityKit.js +72 -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/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +64 -6
- package/dist/combat/projectiles.js +232 -74
- 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 +30 -1
- package/dist/combat/telegraph.js +61 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/recipe.js +4 -1
- 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 +205 -0
- package/dist/devtools/devtools.js +832 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- 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 +68 -0
- package/dist/game/chat.js +173 -0
- 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 +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +35 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +286 -6
- 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/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +44 -1
- package/dist/game/race.js +116 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -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/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +89 -0
- package/dist/input/touchScheme.js +153 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +59 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- 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/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +17 -2
- package/dist/multiplayer/presenceModel.js +30 -4
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -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 +10 -0
- package/dist/nav/navGrid.js +48 -5
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- 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/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -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 +43 -5
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +54 -8
- package/dist/physics/physicsWorld.js +400 -79
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +18 -18
- 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/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +15 -0
- package/dist/runtime/cameraDirector.js +31 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +65 -2
- package/dist/runtime/gameContext.js +280 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +30 -1
- package/dist/runtime/hostPersistence.js +70 -5
- package/dist/runtime/inputSnapshot.d.ts +12 -0
- package/dist/runtime/inputSnapshot.js +17 -0
- package/dist/runtime/motionIntents.d.ts +33 -0
- package/dist/runtime/motionIntents.js +43 -0
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/transport.d.ts +32 -0
- 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 +30 -5
- package/dist/scene/entityStore.js +83 -13
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +29 -0
- package/dist/scene/objectQuery.js +140 -0
- package/dist/scene/objectStore.d.ts +13 -0
- package/dist/scene/objectStore.js +130 -6
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +17 -1
- package/dist/scene/spatial.js +140 -17
- 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/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/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- 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/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- 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/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- 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/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- 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 +61 -0
- package/dist/world/environmentSummary.js +138 -0
- package/dist/world/features.d.ts +138 -9
- package/dist/world/features.js +73 -2
- package/dist/world/gridInstances.d.ts +12 -0
- package/dist/world/gridInstances.js +160 -0
- 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/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +62 -1
- package/dist/world/terrain.js +253 -9
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- 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 +4983 -0
- package/package.json +14 -3
package/dist/scene/spatial.js
CHANGED
|
@@ -14,11 +14,121 @@ 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
|
+
const CELL_KEY_OFFSET = 0x8000;
|
|
21
|
+
function packCell(cx, cz) {
|
|
22
|
+
return ((cx + CELL_KEY_OFFSET) << 16) | ((cz + CELL_KEY_OFFSET) & 0xffff);
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_CELL_SIZE = 8;
|
|
17
25
|
export function createSpatialApi(options) {
|
|
18
|
-
const { resolvePosition, candidates, occluder } = options;
|
|
26
|
+
const { resolvePosition, candidates, occluder, getVersion } = options;
|
|
27
|
+
const gridConfig = options.grid === false ? undefined : (options.grid ?? { cellSize: DEFAULT_CELL_SIZE });
|
|
28
|
+
const cellSize = gridConfig?.cellSize;
|
|
29
|
+
let gridIndex = null;
|
|
30
|
+
let gridDirty = true;
|
|
31
|
+
let lastVersion;
|
|
19
32
|
function resolveTarget(target) {
|
|
20
33
|
return typeof target === "string" ? resolvePosition(target) : target;
|
|
21
34
|
}
|
|
35
|
+
function takeBucket(index) {
|
|
36
|
+
if (index.poolCursor < index.bucketPool.length) {
|
|
37
|
+
const bucket = index.bucketPool[index.poolCursor];
|
|
38
|
+
index.poolCursor += 1;
|
|
39
|
+
bucket.length = 0;
|
|
40
|
+
return bucket;
|
|
41
|
+
}
|
|
42
|
+
const bucket = [];
|
|
43
|
+
index.bucketPool.push(bucket);
|
|
44
|
+
index.poolCursor += 1;
|
|
45
|
+
return bucket;
|
|
46
|
+
}
|
|
47
|
+
function ensureGrid() {
|
|
48
|
+
if (cellSize === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
const version = getVersion?.();
|
|
51
|
+
if (version !== undefined && version !== lastVersion) {
|
|
52
|
+
gridDirty = true;
|
|
53
|
+
lastVersion = version;
|
|
54
|
+
}
|
|
55
|
+
if (!gridDirty && gridIndex !== null)
|
|
56
|
+
return gridIndex;
|
|
57
|
+
const previous = gridIndex;
|
|
58
|
+
const cells = previous?.cells ?? new Map();
|
|
59
|
+
cells.clear();
|
|
60
|
+
const indexed = previous?.indexed ?? new Set();
|
|
61
|
+
indexed.clear();
|
|
62
|
+
const index = {
|
|
63
|
+
cells,
|
|
64
|
+
indexed,
|
|
65
|
+
bucketPool: previous?.bucketPool ?? [],
|
|
66
|
+
poolCursor: 0,
|
|
67
|
+
};
|
|
68
|
+
for (const instanceId of candidates()) {
|
|
69
|
+
const position = resolvePosition(instanceId);
|
|
70
|
+
if (position === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
indexed.add(instanceId);
|
|
73
|
+
const key = packCell(cellCoord(position[0], cellSize), cellCoord(position[2], cellSize));
|
|
74
|
+
let bucket = cells.get(key);
|
|
75
|
+
if (bucket === undefined) {
|
|
76
|
+
bucket = takeBucket(index);
|
|
77
|
+
cells.set(key, bucket);
|
|
78
|
+
}
|
|
79
|
+
bucket.push(instanceId);
|
|
80
|
+
}
|
|
81
|
+
gridIndex = index;
|
|
82
|
+
gridDirty = false;
|
|
83
|
+
return index;
|
|
84
|
+
}
|
|
85
|
+
function collectNear(index, centerX, centerZ, radius, out) {
|
|
86
|
+
const size = cellSize;
|
|
87
|
+
const cxMin = cellCoord(centerX - radius, size);
|
|
88
|
+
const cxMax = cellCoord(centerX + radius, size);
|
|
89
|
+
const czMin = cellCoord(centerZ - radius, size);
|
|
90
|
+
const czMax = cellCoord(centerZ + radius, size);
|
|
91
|
+
out.length = 0;
|
|
92
|
+
const seen = collectNearSeen;
|
|
93
|
+
seen.clear();
|
|
94
|
+
for (let cz = czMin; cz <= czMax; cz += 1) {
|
|
95
|
+
for (let cx = cxMin; cx <= cxMax; cx += 1) {
|
|
96
|
+
const bucket = index.cells.get(packCell(cx, cz));
|
|
97
|
+
if (bucket === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
for (const id of bucket) {
|
|
100
|
+
if (seen.has(id))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(id);
|
|
103
|
+
out.push(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const collectNearSeen = new Set();
|
|
109
|
+
const nearScratch = [];
|
|
110
|
+
const resultScratch = [];
|
|
111
|
+
function appendUnindexed(index, out) {
|
|
112
|
+
for (const instanceId of candidates()) {
|
|
113
|
+
if (index.indexed.has(instanceId))
|
|
114
|
+
continue;
|
|
115
|
+
out.push(instanceId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function filterInRadius(ids, centerId, centerPosition, radius, filter) {
|
|
119
|
+
resultScratch.length = 0;
|
|
120
|
+
for (const instanceId of ids) {
|
|
121
|
+
if (instanceId === centerId)
|
|
122
|
+
continue;
|
|
123
|
+
if (filter !== undefined && !filter(instanceId))
|
|
124
|
+
continue;
|
|
125
|
+
const position = resolvePosition(instanceId);
|
|
126
|
+
if (position !== undefined && distanceBetween(centerPosition, position) <= radius) {
|
|
127
|
+
resultScratch.push(instanceId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return resultScratch.slice();
|
|
131
|
+
}
|
|
22
132
|
return {
|
|
23
133
|
distance(aInstanceId, bInstanceId) {
|
|
24
134
|
const a = resolvePosition(aInstanceId);
|
|
@@ -32,14 +142,13 @@ export function createSpatialApi(options) {
|
|
|
32
142
|
const centerPosition = resolveTarget(center);
|
|
33
143
|
if (centerPosition === undefined)
|
|
34
144
|
return [];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
145
|
+
const index = ensureGrid();
|
|
146
|
+
if (index === null) {
|
|
147
|
+
return filterInRadius(candidates(), centerId, centerPosition, radius, filter);
|
|
148
|
+
}
|
|
149
|
+
collectNear(index, centerPosition[0], centerPosition[2], radius, nearScratch);
|
|
150
|
+
appendUnindexed(index, nearScratch);
|
|
151
|
+
return filterInRadius(nearScratch, centerId, centerPosition, radius, filter);
|
|
43
152
|
},
|
|
44
153
|
hasLineOfSight(fromInstanceId, toInstanceId) {
|
|
45
154
|
const from = resolvePosition(fromInstanceId);
|
|
@@ -58,22 +167,36 @@ export function createSpatialApi(options) {
|
|
|
58
167
|
if (forward === null)
|
|
59
168
|
return [];
|
|
60
169
|
const minDot = Math.cos((halfAngleDeg * Math.PI) / 180);
|
|
61
|
-
|
|
170
|
+
const index = ensureGrid();
|
|
171
|
+
const ids = index === null
|
|
172
|
+
? candidates()
|
|
173
|
+
: (collectNear(index, origin[0], origin[2], radius, nearScratch),
|
|
174
|
+
appendUnindexed(index, nearScratch),
|
|
175
|
+
nearScratch);
|
|
176
|
+
resultScratch.length = 0;
|
|
177
|
+
for (const instanceId of ids) {
|
|
62
178
|
if (instanceId === from)
|
|
63
|
-
|
|
179
|
+
continue;
|
|
64
180
|
const position = resolvePosition(instanceId);
|
|
65
181
|
if (position === undefined)
|
|
66
|
-
|
|
182
|
+
continue;
|
|
67
183
|
const dx = position[0] - origin[0];
|
|
68
184
|
const dz = position[2] - origin[2];
|
|
69
185
|
const planarDistance = Math.sqrt(dx * dx + dz * dz);
|
|
70
186
|
if (planarDistance > radius)
|
|
71
|
-
|
|
72
|
-
if (planarDistance === 0)
|
|
73
|
-
|
|
187
|
+
continue;
|
|
188
|
+
if (planarDistance === 0) {
|
|
189
|
+
resultScratch.push(instanceId);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
74
192
|
const dot = (forward[0] * dx + forward[1] * dz) / planarDistance;
|
|
75
|
-
|
|
76
|
-
|
|
193
|
+
if (dot >= minDot)
|
|
194
|
+
resultScratch.push(instanceId);
|
|
195
|
+
}
|
|
196
|
+
return resultScratch.slice();
|
|
197
|
+
},
|
|
198
|
+
invalidate() {
|
|
199
|
+
gridDirty = true;
|
|
77
200
|
},
|
|
78
201
|
moveToward(instanceId, target, { speed, stopDistance = 0, dt }) {
|
|
79
202
|
const current = resolvePosition(instanceId);
|
|
@@ -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
|
+
}
|
|
@@ -9,10 +9,16 @@ export interface RecordingBufferOptions {
|
|
|
9
9
|
/** Hard cap on frame count, oldest evicted first. Default unlimited. */
|
|
10
10
|
maxFrames?: number;
|
|
11
11
|
}
|
|
12
|
+
export interface RecordingPair<T> {
|
|
13
|
+
before: RecordingFrame<T> | null;
|
|
14
|
+
after: RecordingFrame<T> | null;
|
|
15
|
+
}
|
|
12
16
|
export interface RecordingBuffer<T> {
|
|
13
17
|
append(t: number, data: T): void;
|
|
14
18
|
/** Nearest frame at or before `t`, or null if the buffer is empty or `t` precedes every frame. */
|
|
15
19
|
seek(t: number): RecordingFrame<T> | null;
|
|
20
|
+
/** The frames bracketing `t` for interpolated playback: `before` is `seek(t)`, `after` is the next recorded frame (null past the end). */
|
|
21
|
+
seekPair(t: number): RecordingPair<T>;
|
|
16
22
|
/** Frames with `fromT <= t <= toT`, in recorded order. */
|
|
17
23
|
range(fromT: number, toT: number): RecordingFrame<T>[];
|
|
18
24
|
clear(): void;
|
|
@@ -45,6 +45,13 @@ export function createRecordingBuffer(options = {}) {
|
|
|
45
45
|
const index = floorIndex(t);
|
|
46
46
|
return index === -1 ? null : frames[index];
|
|
47
47
|
},
|
|
48
|
+
seekPair(t) {
|
|
49
|
+
const index = floorIndex(t);
|
|
50
|
+
return {
|
|
51
|
+
before: index === -1 ? null : frames[index],
|
|
52
|
+
after: frames[index + 1] ?? null,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
48
55
|
range(fromT, toT) {
|
|
49
56
|
return frames.filter((frame) => frame.t >= fromT && frame.t <= toT);
|
|
50
57
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RecordingBuffer } from "./recordingBuffer.js";
|
|
2
|
+
export interface ReplayLoopOptions<T> {
|
|
3
|
+
/** Blend between the two frames straddling the seek time; omit to hold the earlier frame. */
|
|
4
|
+
interpolate?: (before: T, after: T, alpha: number) => T;
|
|
5
|
+
/** Returns `null` for this long at the start of every loop pass — a spawn-grace gap so a looping ghost blinks out instead of teleporting. Default `0`. */
|
|
6
|
+
spawnGraceSeconds?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Replays a finished `RecordingBuffer` on a loop — modulo-time seek, frame interpolation, and
|
|
10
|
+
* spawn grace, the pieces every ghost-lap/echo feature hand-rolled (#286.4). `sample(t)` is a pure
|
|
11
|
+
* function of absolute time; `null` means "the ghost isn't on track right now".
|
|
12
|
+
*/
|
|
13
|
+
export interface ReplayLoop<T> {
|
|
14
|
+
duration(): number;
|
|
15
|
+
sample(t: number): T | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function createReplayLoop<T>(buffer: RecordingBuffer<T>, options?: ReplayLoopOptions<T>): ReplayLoop<T>;
|
|
18
|
+
export interface ReplayEntityDeps {
|
|
19
|
+
has(id: string): boolean;
|
|
20
|
+
spawn(id: string): void;
|
|
21
|
+
setPose(id: string, pose: {
|
|
22
|
+
position: readonly [number, number, number];
|
|
23
|
+
rotationY?: number;
|
|
24
|
+
dt?: number;
|
|
25
|
+
}): unknown;
|
|
26
|
+
despawn(id: string): unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface RecordedPoseLike {
|
|
29
|
+
position: readonly [number, number, number];
|
|
30
|
+
rotationY?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Linear pose blend for `ReplayLoopOptions.interpolate` — position lerp plus shortest-arc yaw. */
|
|
33
|
+
export declare function interpolateRecordedPose<T extends RecordedPoseLike>(before: T, after: T, alpha: number): T;
|
|
34
|
+
/**
|
|
35
|
+
* Reconcile a replayed pose against an entity store: spawns the ghost on the first non-null sample,
|
|
36
|
+
* poses it while samples flow, despawns it through grace gaps or after the recording empties.
|
|
37
|
+
* `deps` matches `ctx.scene.entity` structurally — pass `{ has: (id) => ctx.scene.entity.get(id) !== null, spawn, setPose, despawn }`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function syncReplayEntity(deps: ReplayEntityDeps, id: string, pose: RecordedPoseLike | null, dt?: number): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function createReplayLoop(buffer, options = {}) {
|
|
2
|
+
const grace = Math.max(0, options.spawnGraceSeconds ?? 0);
|
|
3
|
+
const interpolate = options.interpolate;
|
|
4
|
+
return {
|
|
5
|
+
duration: () => buffer.duration(),
|
|
6
|
+
sample(t) {
|
|
7
|
+
const frames = buffer.frames();
|
|
8
|
+
if (frames.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
const first = frames[0];
|
|
11
|
+
const duration = buffer.duration();
|
|
12
|
+
if (duration <= 0)
|
|
13
|
+
return first.data;
|
|
14
|
+
const local = ((t % duration) + duration) % duration;
|
|
15
|
+
if (local < grace)
|
|
16
|
+
return null;
|
|
17
|
+
const seekT = first.t + local;
|
|
18
|
+
const pair = buffer.seekPair(seekT);
|
|
19
|
+
if (pair.before === null)
|
|
20
|
+
return pair.after?.data ?? null;
|
|
21
|
+
if (pair.after === null || interpolate === undefined)
|
|
22
|
+
return pair.before.data;
|
|
23
|
+
const span = pair.after.t - pair.before.t;
|
|
24
|
+
const alpha = span <= 1e-9 ? 0 : (seekT - pair.before.t) / span;
|
|
25
|
+
return interpolate(pair.before.data, pair.after.data, alpha);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Linear pose blend for `ReplayLoopOptions.interpolate` — position lerp plus shortest-arc yaw. */
|
|
30
|
+
export function interpolateRecordedPose(before, after, alpha) {
|
|
31
|
+
let yawDelta = (after.rotationY ?? 0) - (before.rotationY ?? 0);
|
|
32
|
+
while (yawDelta > Math.PI)
|
|
33
|
+
yawDelta -= Math.PI * 2;
|
|
34
|
+
while (yawDelta < -Math.PI)
|
|
35
|
+
yawDelta += Math.PI * 2;
|
|
36
|
+
return {
|
|
37
|
+
...before,
|
|
38
|
+
position: [
|
|
39
|
+
before.position[0] + (after.position[0] - before.position[0]) * alpha,
|
|
40
|
+
before.position[1] + (after.position[1] - before.position[1]) * alpha,
|
|
41
|
+
before.position[2] + (after.position[2] - before.position[2]) * alpha,
|
|
42
|
+
],
|
|
43
|
+
...(before.rotationY === undefined ? {} : { rotationY: (before.rotationY ?? 0) + yawDelta * alpha }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reconcile a replayed pose against an entity store: spawns the ghost on the first non-null sample,
|
|
48
|
+
* poses it while samples flow, despawns it through grace gaps or after the recording empties.
|
|
49
|
+
* `deps` matches `ctx.scene.entity` structurally — pass `{ has: (id) => ctx.scene.entity.get(id) !== null, spawn, setPose, despawn }`.
|
|
50
|
+
*/
|
|
51
|
+
export function syncReplayEntity(deps, id, pose, dt) {
|
|
52
|
+
if (pose === null) {
|
|
53
|
+
if (deps.has(id))
|
|
54
|
+
deps.despawn(id);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!deps.has(id))
|
|
58
|
+
deps.spawn(id);
|
|
59
|
+
deps.setPose(id, {
|
|
60
|
+
position: pose.position,
|
|
61
|
+
...(pose.rotationY === undefined ? {} : { rotationY: pose.rotationY }),
|
|
62
|
+
...(dt === undefined ? {} : { dt }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type VisionPoint = readonly [number, number];
|
|
2
|
+
/** Structurally matches `world/walls` `WallSegment` — pass those straight in as occluders. */
|
|
3
|
+
export interface VisionWall {
|
|
4
|
+
from: VisionPoint;
|
|
5
|
+
to: VisionPoint;
|
|
6
|
+
}
|
|
7
|
+
export interface VisionConeConfig {
|
|
8
|
+
range: number;
|
|
9
|
+
/** Full cone opening angle in radians (matches `TelegraphShape`'s cone `angle`). */
|
|
10
|
+
angle: number;
|
|
11
|
+
}
|
|
12
|
+
/** 2D segment intersection on the XZ plane (collinear overlaps count) — the LoS building block `revealQuery`/`frustumSensor` never shipped (#286.5). */
|
|
13
|
+
export declare function segmentsIntersect(a1: VisionPoint, a2: VisionPoint, b1: VisionPoint, b2: VisionPoint): boolean;
|
|
14
|
+
/** True when no wall segment crosses the sight line. */
|
|
15
|
+
export declare function hasWallLineOfSight(from: VisionPoint, to: VisionPoint, walls: readonly VisionWall[]): boolean;
|
|
16
|
+
/** Angle + range test only — no occlusion. `heading` is engine yaw (radians, `atan2(dx, dz)`). */
|
|
17
|
+
export declare function pointInCone(origin: VisionPoint, heading: number, config: VisionConeConfig, point: VisionPoint): boolean;
|
|
18
|
+
export interface VisionTarget<TId extends string = string> {
|
|
19
|
+
id: TId;
|
|
20
|
+
at: VisionPoint;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The guard-sight primitive every stealth game hand-rolled: an angle+range cone on the XZ plane
|
|
24
|
+
* whose sight lines are blocked by 2D wall segments (`world/walls` segments fit structurally).
|
|
25
|
+
* Pure math — feed entity positions and heading each tick.
|
|
26
|
+
*/
|
|
27
|
+
export interface VisionCone {
|
|
28
|
+
canSee(origin: VisionPoint, heading: number, target: VisionPoint): boolean;
|
|
29
|
+
visibleTargets<TId extends string>(origin: VisionPoint, heading: number, targets: readonly VisionTarget<TId>[]): TId[];
|
|
30
|
+
}
|
|
31
|
+
export declare function createVisionCone(config: VisionConeConfig, walls?: readonly VisionWall[]): VisionCone;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function orientation(ax, az, bx, bz, cx, cz) {
|
|
2
|
+
const value = (bz - az) * (cx - bx) - (bx - ax) * (cz - bz);
|
|
3
|
+
if (value > 1e-9)
|
|
4
|
+
return 1;
|
|
5
|
+
if (value < -1e-9)
|
|
6
|
+
return -1;
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
function onSegment(ax, az, bx, bz, cx, cz) {
|
|
10
|
+
return (Math.min(ax, bx) - 1e-9 <= cx &&
|
|
11
|
+
cx <= Math.max(ax, bx) + 1e-9 &&
|
|
12
|
+
Math.min(az, bz) - 1e-9 <= cz &&
|
|
13
|
+
cz <= Math.max(az, bz) + 1e-9);
|
|
14
|
+
}
|
|
15
|
+
/** 2D segment intersection on the XZ plane (collinear overlaps count) — the LoS building block `revealQuery`/`frustumSensor` never shipped (#286.5). */
|
|
16
|
+
export function segmentsIntersect(a1, a2, b1, b2) {
|
|
17
|
+
const o1 = orientation(a1[0], a1[1], a2[0], a2[1], b1[0], b1[1]);
|
|
18
|
+
const o2 = orientation(a1[0], a1[1], a2[0], a2[1], b2[0], b2[1]);
|
|
19
|
+
const o3 = orientation(b1[0], b1[1], b2[0], b2[1], a1[0], a1[1]);
|
|
20
|
+
const o4 = orientation(b1[0], b1[1], b2[0], b2[1], a2[0], a2[1]);
|
|
21
|
+
if (o1 !== o2 && o3 !== o4)
|
|
22
|
+
return true;
|
|
23
|
+
if (o1 === 0 && onSegment(a1[0], a1[1], a2[0], a2[1], b1[0], b1[1]))
|
|
24
|
+
return true;
|
|
25
|
+
if (o2 === 0 && onSegment(a1[0], a1[1], a2[0], a2[1], b2[0], b2[1]))
|
|
26
|
+
return true;
|
|
27
|
+
if (o3 === 0 && onSegment(b1[0], b1[1], b2[0], b2[1], a1[0], a1[1]))
|
|
28
|
+
return true;
|
|
29
|
+
if (o4 === 0 && onSegment(b1[0], b1[1], b2[0], b2[1], a2[0], a2[1]))
|
|
30
|
+
return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/** True when no wall segment crosses the sight line. */
|
|
34
|
+
export function hasWallLineOfSight(from, to, walls) {
|
|
35
|
+
for (const wall of walls) {
|
|
36
|
+
if (segmentsIntersect(from, to, wall.from, wall.to))
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Angle + range test only — no occlusion. `heading` is engine yaw (radians, `atan2(dx, dz)`). */
|
|
42
|
+
export function pointInCone(origin, heading, config, point) {
|
|
43
|
+
const dx = point[0] - origin[0];
|
|
44
|
+
const dz = point[1] - origin[1];
|
|
45
|
+
const distance = Math.hypot(dx, dz);
|
|
46
|
+
if (distance > config.range)
|
|
47
|
+
return false;
|
|
48
|
+
if (distance < 1e-9)
|
|
49
|
+
return true;
|
|
50
|
+
let delta = Math.atan2(dx, dz) - heading;
|
|
51
|
+
while (delta > Math.PI)
|
|
52
|
+
delta -= Math.PI * 2;
|
|
53
|
+
while (delta < -Math.PI)
|
|
54
|
+
delta += Math.PI * 2;
|
|
55
|
+
return Math.abs(delta) <= config.angle / 2;
|
|
56
|
+
}
|
|
57
|
+
export function createVisionCone(config, walls = []) {
|
|
58
|
+
function canSee(origin, heading, target) {
|
|
59
|
+
return pointInCone(origin, heading, config, target) && hasWallLineOfSight(origin, target, walls);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
canSee,
|
|
63
|
+
visibleTargets(origin, heading, targets) {
|
|
64
|
+
const seen = [];
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
if (canSee(origin, heading, target.at))
|
|
67
|
+
seen.push(target.id);
|
|
68
|
+
}
|
|
69
|
+
return seen;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -38,11 +38,11 @@ export interface RaidPlayerSnapshot {
|
|
|
38
38
|
remaining: number;
|
|
39
39
|
}
|
|
40
40
|
export interface RaidSession {
|
|
41
|
-
beginExtract(userId: string, extractId: string, team?: string): ContestedEvent | null;
|
|
41
|
+
beginExtract(userId: string, extractId: string, position: RingPoint, team?: string): ContestedEvent | null;
|
|
42
42
|
tickExtract(userId: string, dt: number, occupants?: Record<string, number>): ContestedEvent[];
|
|
43
43
|
damage(userId: string, reason?: string): ContestedEvent | null;
|
|
44
44
|
cancelExtract(userId: string): void;
|
|
45
|
-
resolveExtraction(userId: string, containers: readonly ContainerSnapshot[]): ExtractionResult;
|
|
45
|
+
resolveExtraction(userId: string, containers: readonly ContainerSnapshot[]): ExtractionResult | null;
|
|
46
46
|
resolveDeath(userId: string, containers: readonly ContainerSnapshot[], now: number, rng?: () => number): DeathResult;
|
|
47
47
|
claimDeliveries(now: number): readonly DeliveryEntry[];
|
|
48
48
|
pendingDeliveries(userId?: string): readonly DeliveryEntry[];
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createDeliveryQueue, insureLost, partitionOnDeath, resolveConsolation, } from "../inventory/storageTier.js";
|
|
2
2
|
import { createContestedChannel, } from "./contestedChannel.js";
|
|
3
|
+
function inExtractZone(point, position) {
|
|
4
|
+
const dx = position[0] - point.center[0];
|
|
5
|
+
const dz = position[1] - point.center[1];
|
|
6
|
+
return dx * dx + dz * dz <= point.radius * point.radius;
|
|
7
|
+
}
|
|
3
8
|
function mergeAll(containers) {
|
|
4
9
|
const totals = new Map();
|
|
5
10
|
for (const container of containers) {
|
|
@@ -24,12 +29,14 @@ export function createRaidSession(config) {
|
|
|
24
29
|
return statuses.get(userId) ?? "in-raid";
|
|
25
30
|
}
|
|
26
31
|
return {
|
|
27
|
-
beginExtract(userId, extractId, team) {
|
|
32
|
+
beginExtract(userId, extractId, position, team) {
|
|
28
33
|
const point = points.get(extractId);
|
|
29
34
|
if (point === undefined)
|
|
30
35
|
return null;
|
|
31
36
|
if (statusOf(userId) === "extracted" || statusOf(userId) === "dead")
|
|
32
37
|
return null;
|
|
38
|
+
if (!inExtractZone(point, position))
|
|
39
|
+
return null;
|
|
33
40
|
const channel = createContestedChannel({
|
|
34
41
|
duration: point.holdSeconds,
|
|
35
42
|
interruptOnDamage: point.interruptOnDamage ?? true,
|
|
@@ -65,7 +72,11 @@ export function createRaidSession(config) {
|
|
|
65
72
|
},
|
|
66
73
|
resolveExtraction(userId, containers) {
|
|
67
74
|
const attempt = attempts.get(userId);
|
|
68
|
-
|
|
75
|
+
if (attempt === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
if (attempt.channel.phase() !== "complete" && statusOf(userId) !== "extracted")
|
|
78
|
+
return null;
|
|
79
|
+
const extractId = attempt.extractId;
|
|
69
80
|
attempts.delete(userId);
|
|
70
81
|
statuses.set(userId, "extracted");
|
|
71
82
|
return { userId, extractId, banked: mergeAll(containers) };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { shuffleWithRng } from "../cards/cardPile.js";
|
|
2
|
+
export function assignRoles(userIds, roles, rng = Math.random) {
|
|
3
|
+
if (roles.length === 0)
|
|
4
|
+
throw new Error("assignRoles needs at least one role");
|
|
5
|
+
const shuffled = shuffleWithRng(userIds, rng);
|
|
6
|
+
const assignment = {};
|
|
7
|
+
let index = 0;
|
|
8
|
+
const countRoles = roles.filter((role) => role.count !== undefined);
|
|
9
|
+
const ratioRoles = roles.filter((role) => role.count === undefined && role.ratio !== undefined);
|
|
10
|
+
const fillRole = roles.find((role) => role.count === undefined && role.ratio === undefined);
|
|
11
|
+
const fallbackRole = fillRole ?? roles[roles.length - 1];
|
|
12
|
+
for (const role of countRoles) {
|
|
13
|
+
const take = Math.max(0, Math.min(role.count, shuffled.length - index));
|
|
14
|
+
for (let i = 0; i < take; i += 1) {
|
|
15
|
+
assignment[shuffled[index]] = role.id;
|
|
16
|
+
index += 1;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const role of ratioRoles) {
|
|
20
|
+
const wanted = Math.round(role.ratio * shuffled.length);
|
|
21
|
+
const take = Math.max(0, Math.min(wanted, shuffled.length - index));
|
|
22
|
+
for (let i = 0; i < take; i += 1) {
|
|
23
|
+
assignment[shuffled[index]] = role.id;
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
while (index < shuffled.length) {
|
|
28
|
+
assignment[shuffled[index]] = fallbackRole.id;
|
|
29
|
+
index += 1;
|
|
30
|
+
}
|
|
31
|
+
return assignment;
|
|
32
|
+
}
|