@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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface VoiceParticipant {
|
|
2
|
+
userId: string;
|
|
3
|
+
streamId?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface VoiceRoute {
|
|
6
|
+
fromUserId: string;
|
|
7
|
+
channelId: string;
|
|
8
|
+
gain: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Signaling seam for voice: who is in a channel and which media stream
|
|
12
|
+
* descriptor they published. The media plane (WebRTC, SFU, or anything else
|
|
13
|
+
* that moves audio bytes) stays behind this seam, host-supplied — the engine
|
|
14
|
+
* never touches it. subscribers delivers the channel roster on every change,
|
|
15
|
+
* starting with the current roster.
|
|
16
|
+
*/
|
|
17
|
+
export interface VoiceTransport {
|
|
18
|
+
join(channelId: string, streamId?: string): Promise<void>;
|
|
19
|
+
leave(channelId: string): Promise<void>;
|
|
20
|
+
publish(channelId: string, streamId: string): Promise<void>;
|
|
21
|
+
subscribers(channelId: string, onChange: (participants: readonly VoiceParticipant[]) => void): () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createLocalVoiceTransport(options?: {
|
|
24
|
+
userId?: string;
|
|
25
|
+
}): {
|
|
26
|
+
transport: VoiceTransport;
|
|
27
|
+
participants(channelId: string): readonly VoiceParticipant[];
|
|
28
|
+
};
|
|
29
|
+
export type PushToTalkMode = "hold" | "toggle" | "openMic";
|
|
30
|
+
export type PushToTalkStatus = "idle" | "keyed" | "open";
|
|
31
|
+
export interface PushToTalk {
|
|
32
|
+
mode(): PushToTalkMode;
|
|
33
|
+
setMode(mode: PushToTalkMode): void;
|
|
34
|
+
keyDown(): void;
|
|
35
|
+
keyUp(): void;
|
|
36
|
+
muted(): boolean;
|
|
37
|
+
setMuted(muted: boolean): void;
|
|
38
|
+
status(): PushToTalkStatus;
|
|
39
|
+
transmitting(): boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function createPushToTalk(config?: {
|
|
42
|
+
mode?: PushToTalkMode;
|
|
43
|
+
onChange?: (transmitting: boolean) => void;
|
|
44
|
+
}): PushToTalk;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function createLocalVoiceTransport(options) {
|
|
2
|
+
const userId = options?.userId ?? "local";
|
|
3
|
+
const rosters = new Map();
|
|
4
|
+
const listeners = new Map();
|
|
5
|
+
function roster(channelId) {
|
|
6
|
+
let entries = rosters.get(channelId);
|
|
7
|
+
if (entries === undefined) {
|
|
8
|
+
entries = new Map();
|
|
9
|
+
rosters.set(channelId, entries);
|
|
10
|
+
}
|
|
11
|
+
return entries;
|
|
12
|
+
}
|
|
13
|
+
function snapshot(channelId) {
|
|
14
|
+
return [...roster(channelId).values()].map((participant) => ({ ...participant }));
|
|
15
|
+
}
|
|
16
|
+
function notify(channelId) {
|
|
17
|
+
const current = snapshot(channelId);
|
|
18
|
+
for (const listener of listeners.get(channelId) ?? [])
|
|
19
|
+
listener(current);
|
|
20
|
+
}
|
|
21
|
+
const transport = {
|
|
22
|
+
join(channelId, streamId) {
|
|
23
|
+
const participant = { userId };
|
|
24
|
+
if (streamId !== undefined)
|
|
25
|
+
participant.streamId = streamId;
|
|
26
|
+
roster(channelId).set(userId, participant);
|
|
27
|
+
notify(channelId);
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
},
|
|
30
|
+
leave(channelId) {
|
|
31
|
+
if (roster(channelId).delete(userId))
|
|
32
|
+
notify(channelId);
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
},
|
|
35
|
+
publish(channelId, streamId) {
|
|
36
|
+
const participant = roster(channelId).get(userId);
|
|
37
|
+
if (participant !== undefined) {
|
|
38
|
+
participant.streamId = streamId;
|
|
39
|
+
notify(channelId);
|
|
40
|
+
}
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
},
|
|
43
|
+
subscribers(channelId, onChange) {
|
|
44
|
+
let set = listeners.get(channelId);
|
|
45
|
+
if (set === undefined) {
|
|
46
|
+
set = new Set();
|
|
47
|
+
listeners.set(channelId, set);
|
|
48
|
+
}
|
|
49
|
+
set.add(onChange);
|
|
50
|
+
onChange(snapshot(channelId));
|
|
51
|
+
return () => {
|
|
52
|
+
set.delete(onChange);
|
|
53
|
+
if (set.size === 0)
|
|
54
|
+
listeners.delete(channelId);
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return { transport, participants: snapshot };
|
|
59
|
+
}
|
|
60
|
+
export function createPushToTalk(config) {
|
|
61
|
+
let mode = config?.mode ?? "hold";
|
|
62
|
+
let keyed = false;
|
|
63
|
+
let muted = false;
|
|
64
|
+
let lastTransmitting = false;
|
|
65
|
+
function transmitting() {
|
|
66
|
+
if (muted)
|
|
67
|
+
return false;
|
|
68
|
+
return mode === "openMic" ? true : keyed;
|
|
69
|
+
}
|
|
70
|
+
function emit() {
|
|
71
|
+
const current = transmitting();
|
|
72
|
+
if (current === lastTransmitting)
|
|
73
|
+
return;
|
|
74
|
+
lastTransmitting = current;
|
|
75
|
+
config?.onChange?.(current);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
mode: () => mode,
|
|
79
|
+
setMode(next) {
|
|
80
|
+
mode = next;
|
|
81
|
+
keyed = false;
|
|
82
|
+
emit();
|
|
83
|
+
},
|
|
84
|
+
keyDown() {
|
|
85
|
+
if (mode === "hold")
|
|
86
|
+
keyed = true;
|
|
87
|
+
else if (mode === "toggle")
|
|
88
|
+
keyed = !keyed;
|
|
89
|
+
emit();
|
|
90
|
+
},
|
|
91
|
+
keyUp() {
|
|
92
|
+
if (mode === "hold")
|
|
93
|
+
keyed = false;
|
|
94
|
+
emit();
|
|
95
|
+
},
|
|
96
|
+
muted: () => muted,
|
|
97
|
+
setMuted(next) {
|
|
98
|
+
muted = next;
|
|
99
|
+
emit();
|
|
100
|
+
},
|
|
101
|
+
status() {
|
|
102
|
+
if (mode === "openMic")
|
|
103
|
+
return "open";
|
|
104
|
+
return keyed ? "keyed" : "idle";
|
|
105
|
+
},
|
|
106
|
+
transmitting,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type CorridorPoint = readonly [number, number];
|
|
2
|
+
export interface CorridorEdge {
|
|
3
|
+
/** Centerline polyline in XZ; at least two points. */
|
|
4
|
+
points: readonly CorridorPoint[];
|
|
5
|
+
/** Full corridor width around the centerline. */
|
|
6
|
+
width: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CorridorSample {
|
|
9
|
+
/** Distance from the query point to the nearest centerline. */
|
|
10
|
+
distance: number;
|
|
11
|
+
/** Nearest point on that centerline. */
|
|
12
|
+
closest: CorridorPoint;
|
|
13
|
+
/** Index of the edge owning the nearest centerline point. */
|
|
14
|
+
edgeIndex: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Organic corridor/path collision (#284.9): a distance-to-polyline width clamp over an edge graph —
|
|
18
|
+
* cave tunnels, river channels, winding roads. Grid/navmesh/voxel collision can't express a smooth
|
|
19
|
+
* ribbon; this can. Pure math over authored centerlines.
|
|
20
|
+
*/
|
|
21
|
+
export interface CorridorField {
|
|
22
|
+
contains(x: number, z: number): boolean;
|
|
23
|
+
/** Nearest-centerline sample, or `null` for an empty field. */
|
|
24
|
+
nearest(x: number, z: number): CorridorSample | null;
|
|
25
|
+
/** The input point when inside; otherwise the nearest point on the corridor boundary — the movement clamp. */
|
|
26
|
+
clamp(x: number, z: number): CorridorPoint;
|
|
27
|
+
/** Signed distance to the corridor edge: negative inside, positive outside. */
|
|
28
|
+
distanceToBoundary(x: number, z: number): number;
|
|
29
|
+
}
|
|
30
|
+
export declare function createCorridorField(edges: readonly CorridorEdge[]): CorridorField;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function closestOnSegment(px, pz, a, b) {
|
|
2
|
+
const abx = b[0] - a[0];
|
|
3
|
+
const abz = b[1] - a[1];
|
|
4
|
+
const lengthSq = abx * abx + abz * abz;
|
|
5
|
+
if (lengthSq <= 1e-12)
|
|
6
|
+
return a;
|
|
7
|
+
const t = Math.max(0, Math.min(1, ((px - a[0]) * abx + (pz - a[1]) * abz) / lengthSq));
|
|
8
|
+
return [a[0] + abx * t, a[1] + abz * t];
|
|
9
|
+
}
|
|
10
|
+
export function createCorridorField(edges) {
|
|
11
|
+
for (const edge of edges) {
|
|
12
|
+
if (edge.points.length < 2)
|
|
13
|
+
throw new Error("corridor edge needs at least two centerline points");
|
|
14
|
+
if (!(edge.width > 0))
|
|
15
|
+
throw new Error(`corridor width must be positive, got ${edge.width}`);
|
|
16
|
+
}
|
|
17
|
+
function nearest(x, z) {
|
|
18
|
+
let best = null;
|
|
19
|
+
for (let edgeIndex = 0; edgeIndex < edges.length; edgeIndex += 1) {
|
|
20
|
+
const edge = edges[edgeIndex];
|
|
21
|
+
for (let i = 0; i < edge.points.length - 1; i += 1) {
|
|
22
|
+
const closest = closestOnSegment(x, z, edge.points[i], edge.points[i + 1]);
|
|
23
|
+
const distance = Math.hypot(x - closest[0], z - closest[1]);
|
|
24
|
+
if (best === null || distance < best.distance)
|
|
25
|
+
best = { distance, closest, edgeIndex };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return best;
|
|
29
|
+
}
|
|
30
|
+
function distanceToBoundary(x, z) {
|
|
31
|
+
let signed = Number.POSITIVE_INFINITY;
|
|
32
|
+
for (let edgeIndex = 0; edgeIndex < edges.length; edgeIndex += 1) {
|
|
33
|
+
const edge = edges[edgeIndex];
|
|
34
|
+
for (let i = 0; i < edge.points.length - 1; i += 1) {
|
|
35
|
+
const closest = closestOnSegment(x, z, edge.points[i], edge.points[i + 1]);
|
|
36
|
+
const toBoundary = Math.hypot(x - closest[0], z - closest[1]) - edge.width / 2;
|
|
37
|
+
if (toBoundary < signed)
|
|
38
|
+
signed = toBoundary;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return signed;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
contains: (x, z) => distanceToBoundary(x, z) <= 0,
|
|
45
|
+
nearest,
|
|
46
|
+
clamp(x, z) {
|
|
47
|
+
let best = null;
|
|
48
|
+
for (const edge of edges) {
|
|
49
|
+
for (let i = 0; i < edge.points.length - 1; i += 1) {
|
|
50
|
+
const closest = closestOnSegment(x, z, edge.points[i], edge.points[i + 1]);
|
|
51
|
+
const distance = Math.hypot(x - closest[0], z - closest[1]);
|
|
52
|
+
const half = edge.width / 2;
|
|
53
|
+
if (distance <= half)
|
|
54
|
+
return [x, z];
|
|
55
|
+
const overshoot = distance - half;
|
|
56
|
+
if (best === null || overshoot < best.overshoot) {
|
|
57
|
+
const scale = half / distance;
|
|
58
|
+
best = {
|
|
59
|
+
point: [closest[0] + (x - closest[0]) * scale, closest[1] + (z - closest[1]) * scale],
|
|
60
|
+
overshoot,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return best === null ? [x, z] : best.point;
|
|
66
|
+
},
|
|
67
|
+
distanceToBoundary,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
import type { NavGrid, NavPoint } from "./navGrid.js";
|
|
3
|
+
export interface NavConstrainProposed {
|
|
4
|
+
position: EntityPosition;
|
|
5
|
+
rotationY: number;
|
|
6
|
+
grounded: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface NavConstrainEntity {
|
|
9
|
+
position: EntityPosition;
|
|
10
|
+
}
|
|
11
|
+
export interface NavConstrainOptions {
|
|
12
|
+
y?: (point: NavPoint) => number;
|
|
13
|
+
}
|
|
14
|
+
export declare function constrainToNavGrid(grid: NavGrid, options?: NavConstrainOptions): (proposed: NavConstrainProposed, entity: NavConstrainEntity) => NavConstrainProposed | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function withYRemap(proposed, options) {
|
|
2
|
+
if (options?.y === undefined)
|
|
3
|
+
return proposed;
|
|
4
|
+
const y = options.y([proposed.position[0], proposed.position[2]]);
|
|
5
|
+
return { ...proposed, position: [proposed.position[0], y, proposed.position[2]] };
|
|
6
|
+
}
|
|
7
|
+
function walkable(grid, point) {
|
|
8
|
+
const cell = grid.cellAt(point);
|
|
9
|
+
return grid.isWalkable(cell.col, cell.row);
|
|
10
|
+
}
|
|
11
|
+
export function constrainToNavGrid(grid, options) {
|
|
12
|
+
return (proposed, entity) => {
|
|
13
|
+
const target = [proposed.position[0], proposed.position[2]];
|
|
14
|
+
if (walkable(grid, target))
|
|
15
|
+
return withYRemap(proposed, options);
|
|
16
|
+
const slideX = [entity.position[0], proposed.position[2]];
|
|
17
|
+
if (walkable(grid, slideX)) {
|
|
18
|
+
return withYRemap({ ...proposed, position: [entity.position[0], proposed.position[1], proposed.position[2]] }, options);
|
|
19
|
+
}
|
|
20
|
+
const slideZ = [proposed.position[0], entity.position[2]];
|
|
21
|
+
if (walkable(grid, slideZ)) {
|
|
22
|
+
return withYRemap({ ...proposed, position: [proposed.position[0], proposed.position[1], entity.position[2]] }, options);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EnvironmentWorldFeature } from "../world/features.js";
|
|
2
|
+
import type { Aabb } from "../world/geometry.js";
|
|
3
|
+
export interface NavObstacleGrid {
|
|
4
|
+
blockAabb(aabb: Aabb): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Expands every structure descriptor on an environment world feature into its
|
|
8
|
+
* generated buildings and blocks their footprints on `grid`. Returns the
|
|
9
|
+
* number of buildings blocked.
|
|
10
|
+
*/
|
|
11
|
+
export declare function populateNavGridFromEnvironment(grid: NavObstacleGrid, world: EnvironmentWorldFeature): number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { resolveStructureBuildings } from "../world/environmentSummary.js";
|
|
2
|
+
/**
|
|
3
|
+
* Expands every structure descriptor on an environment world feature into its
|
|
4
|
+
* generated buildings and blocks their footprints on `grid`. Returns the
|
|
5
|
+
* number of buildings blocked.
|
|
6
|
+
*/
|
|
7
|
+
export function populateNavGridFromEnvironment(grid, world) {
|
|
8
|
+
let blocked = 0;
|
|
9
|
+
for (const descriptor of world.structures ?? []) {
|
|
10
|
+
for (const building of resolveStructureBuildings(descriptor)) {
|
|
11
|
+
grid.blockAabb(building.bounds);
|
|
12
|
+
blocked += 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return blocked;
|
|
16
|
+
}
|
package/dist/nav/navGrid.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface FindPathOptions {
|
|
|
15
15
|
smooth?: boolean;
|
|
16
16
|
/** Diameter of the mover in world units — blocks cells within this margin of an obstacle. Default 0. */
|
|
17
17
|
clearance?: number;
|
|
18
|
+
/** Multiplier (>=1) applied to the base cost of stepping from one cell center to a neighbour's. Use to penalize slopes, terrain, etc. */
|
|
19
|
+
stepCost?: (from: NavPoint, to: NavPoint) => number;
|
|
18
20
|
}
|
|
19
21
|
export interface NavGrid {
|
|
20
22
|
readonly cols: number;
|
|
@@ -42,5 +44,13 @@ export declare function createNavGrid(config: NavGridConfig): NavGrid;
|
|
|
42
44
|
* the nearest walkable cell so a click on an obstacle still routes to its edge.
|
|
43
45
|
*/
|
|
44
46
|
export declare function findPath(grid: NavGrid, from: NavPoint, to: NavPoint, options?: FindPathOptions): NavPoint[] | null;
|
|
47
|
+
/**
|
|
48
|
+
* `FindPathOptions.stepCost` factory that penalizes steep terrain: cost is
|
|
49
|
+
* `1 + weight * |Δheight| / horizontalDistance`, so with the default weight a
|
|
50
|
+
* 45° slope roughly doubles the step cost.
|
|
51
|
+
*/
|
|
52
|
+
export declare function slopeStepCost(field: {
|
|
53
|
+
sampleHeight(x: number, z: number): number;
|
|
54
|
+
}, weight?: number): (from: NavPoint, to: NavPoint) => number;
|
|
45
55
|
/** Remove waypoints the mover can skip because it has clear line-of-sight past them. */
|
|
46
56
|
export declare function smoothPath(grid: NavGrid, points: readonly NavPoint[]): NavPoint[];
|
package/dist/nav/navGrid.js
CHANGED
|
@@ -82,11 +82,19 @@ export function createNavGrid(config) {
|
|
|
82
82
|
if (x === b.col && y === b.row)
|
|
83
83
|
return true;
|
|
84
84
|
const e2 = 2 * err;
|
|
85
|
-
|
|
85
|
+
const stepX = e2 > -dy;
|
|
86
|
+
const stepY = e2 < dx;
|
|
87
|
+
if (stepX && stepY) {
|
|
88
|
+
if (walkable[index(x + sx, y)] === 0)
|
|
89
|
+
return false;
|
|
90
|
+
if (walkable[index(x, y + sy)] === 0)
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (stepX) {
|
|
86
94
|
err -= dy;
|
|
87
95
|
x += sx;
|
|
88
96
|
}
|
|
89
|
-
if (
|
|
97
|
+
if (stepY) {
|
|
90
98
|
err += dx;
|
|
91
99
|
y += sy;
|
|
92
100
|
}
|
|
@@ -142,6 +150,24 @@ function withClearance(grid, clearance) {
|
|
|
142
150
|
return true;
|
|
143
151
|
};
|
|
144
152
|
}
|
|
153
|
+
function nearestPassable(grid, point, passable) {
|
|
154
|
+
const start = grid.cellAt(point);
|
|
155
|
+
if (passable(start.col, start.row))
|
|
156
|
+
return start;
|
|
157
|
+
const maxRadius = Math.max(grid.cols, grid.rows);
|
|
158
|
+
for (let radius = 1; radius <= maxRadius; radius += 1) {
|
|
159
|
+
for (let row = start.row - radius; row <= start.row + radius; row += 1) {
|
|
160
|
+
for (let col = start.col - radius; col <= start.col + radius; col += 1) {
|
|
161
|
+
const onRing = Math.abs(col - start.col) === radius || Math.abs(row - start.row) === radius;
|
|
162
|
+
if (!onRing || !grid.inBounds(col, row))
|
|
163
|
+
continue;
|
|
164
|
+
if (passable(col, row))
|
|
165
|
+
return { col, row };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
145
171
|
/**
|
|
146
172
|
* A* over the walkable grid. Returns a polyline of world-space `[x, z]` waypoints
|
|
147
173
|
* from `from` to `to`, or `null` when no route exists. Blocked start/goal snap to
|
|
@@ -149,10 +175,11 @@ function withClearance(grid, clearance) {
|
|
|
149
175
|
*/
|
|
150
176
|
export function findPath(grid, from, to, options = {}) {
|
|
151
177
|
const passable = withClearance(grid, options.clearance ?? 0);
|
|
178
|
+
const stepCost = options.stepCost;
|
|
152
179
|
const startCell = grid.cellAt(from);
|
|
153
180
|
const goalCell = grid.cellAt(to);
|
|
154
|
-
const start = passable(startCell.col, startCell.row) ? startCell : grid
|
|
155
|
-
const goal = passable(goalCell.col, goalCell.row) ? goalCell : grid
|
|
181
|
+
const start = passable(startCell.col, startCell.row) ? startCell : nearestPassable(grid, from, passable);
|
|
182
|
+
const goal = passable(goalCell.col, goalCell.row) ? goalCell : nearestPassable(grid, to, passable);
|
|
156
183
|
if (start === null || goal === null)
|
|
157
184
|
return null;
|
|
158
185
|
const cols = grid.cols;
|
|
@@ -209,7 +236,8 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
209
236
|
const nKey = keyOf(nc, nr);
|
|
210
237
|
if (closed.has(nKey))
|
|
211
238
|
continue;
|
|
212
|
-
const
|
|
239
|
+
const multiplier = stepCost === undefined ? 1 : Math.max(1, stepCost(grid.center(col, row), grid.center(nc, nr)));
|
|
240
|
+
const tentative = g + step * multiplier;
|
|
213
241
|
if (tentative < (gScore.get(nKey) ?? Number.POSITIVE_INFINITY)) {
|
|
214
242
|
gScore.set(nKey, tentative);
|
|
215
243
|
cameFrom.set(nKey, current.key);
|
|
@@ -231,6 +259,21 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
231
259
|
raw[raw.length - 1] = [to[0], to[1]];
|
|
232
260
|
return options.smooth === false ? raw : smoothPath(grid, raw);
|
|
233
261
|
}
|
|
262
|
+
const DEFAULT_SLOPE_STEP_WEIGHT = 1;
|
|
263
|
+
/**
|
|
264
|
+
* `FindPathOptions.stepCost` factory that penalizes steep terrain: cost is
|
|
265
|
+
* `1 + weight * |Δheight| / horizontalDistance`, so with the default weight a
|
|
266
|
+
* 45° slope roughly doubles the step cost.
|
|
267
|
+
*/
|
|
268
|
+
export function slopeStepCost(field, weight = DEFAULT_SLOPE_STEP_WEIGHT) {
|
|
269
|
+
return (from, to) => {
|
|
270
|
+
const horizontal = Math.hypot(to[0] - from[0], to[1] - from[1]);
|
|
271
|
+
if (horizontal < 1e-6)
|
|
272
|
+
return 1;
|
|
273
|
+
const rise = field.sampleHeight(to[0], to[1]) - field.sampleHeight(from[0], from[1]);
|
|
274
|
+
return 1 + (weight * Math.abs(rise)) / horizontal;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
234
277
|
/** Remove waypoints the mover can skip because it has clear line-of-sight past them. */
|
|
235
278
|
export function smoothPath(grid, points) {
|
|
236
279
|
if (points.length <= 2)
|
package/dist/nav/pathFollow.d.ts
CHANGED
|
@@ -17,8 +17,10 @@ export interface PathFollowState {
|
|
|
17
17
|
/** Total world-distance covered so far — drives progress/leak logic in tower defense. */
|
|
18
18
|
distanceTravelled: number;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export interface HeightSampler {
|
|
21
|
+
sampleHeight(x: number, z: number): number;
|
|
22
|
+
}
|
|
23
|
+
export declare function pathFromNav(points: readonly NavPoint[], elevation?: number | HeightSampler, offset?: number): Waypoint[];
|
|
22
24
|
export declare function createPathFollow(config: PathFollowConfig): PathFollowState;
|
|
23
25
|
/**
|
|
24
26
|
* Advance a path-follower by `speed * dt` along its authored polyline. Pure — returns
|
package/dist/nav/pathFollow.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export function pathFromNav(points, elevation = 0, offset = 0) {
|
|
2
|
+
if (typeof elevation === "number") {
|
|
3
|
+
return points.map((point) => [point[0], elevation + offset, point[1]]);
|
|
4
|
+
}
|
|
5
|
+
return points.map((point) => [point[0], elevation.sampleHeight(point[0], point[1]) + offset, point[1]]);
|
|
4
6
|
}
|
|
5
7
|
export function createPathFollow(config) {
|
|
6
8
|
const first = config.waypoints[0];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Waypoint } from "./pathFollow.js";
|
|
2
|
+
export interface RailNode {
|
|
3
|
+
id: string;
|
|
4
|
+
at: Waypoint;
|
|
5
|
+
}
|
|
6
|
+
export interface RailEdge {
|
|
7
|
+
id: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
/** Intermediate shape points between the two node positions (curves, sidings). */
|
|
11
|
+
via?: readonly Waypoint[];
|
|
12
|
+
}
|
|
13
|
+
export interface RailGraphConfig {
|
|
14
|
+
nodes: readonly RailNode[];
|
|
15
|
+
edges: readonly RailEdge[];
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedRailEdge {
|
|
18
|
+
id: string;
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
points: readonly Waypoint[];
|
|
22
|
+
length: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A directed graph of track segments with junction switch-throw semantics — the topological seam
|
|
26
|
+
* `nav/navGrid`'s walkable cells can't express. Each node with several out-edges is a switch whose
|
|
27
|
+
* thrown edge decides where traffic exits; riders (`createRailRider`) run along edge polylines and
|
|
28
|
+
* take the thrown edge at every junction.
|
|
29
|
+
*/
|
|
30
|
+
export interface RailGraph {
|
|
31
|
+
node(id: string): RailNode | null;
|
|
32
|
+
edge(id: string): ResolvedRailEdge | null;
|
|
33
|
+
edges(): readonly string[];
|
|
34
|
+
/** Out-edge ids leaving a node, in declaration order. */
|
|
35
|
+
outEdges(nodeId: string): readonly string[];
|
|
36
|
+
/** The edge a rider leaves this node on — the thrown switch, defaulting to the first declared out-edge; `null` at a dead end. */
|
|
37
|
+
thrownEdge(nodeId: string): string | null;
|
|
38
|
+
/** Throw the switch at `nodeId` to `edgeId`; `false` unless `edgeId` leaves that node. */
|
|
39
|
+
throwSwitch(nodeId: string, edgeId: string): boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface RailRiderConfig {
|
|
42
|
+
edgeId: string;
|
|
43
|
+
/** Arc-length position along the starting edge; default `0`. */
|
|
44
|
+
s?: number;
|
|
45
|
+
/** World units per second; mutate via `setSpeed`. */
|
|
46
|
+
speed: number;
|
|
47
|
+
}
|
|
48
|
+
export interface RailRiderPose {
|
|
49
|
+
position: Waypoint;
|
|
50
|
+
/** Yaw (radians) of the travel direction, matching engine `rotationY`. */
|
|
51
|
+
heading: number;
|
|
52
|
+
edgeId: string;
|
|
53
|
+
/** Arc-length along the current edge. */
|
|
54
|
+
s: number;
|
|
55
|
+
/** True once the rider stops at a node with no thrown out-edge. */
|
|
56
|
+
atDeadEnd: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface RailRider {
|
|
59
|
+
advance(dt: number): RailRiderPose;
|
|
60
|
+
pose(): RailRiderPose;
|
|
61
|
+
setSpeed(speed: number): void;
|
|
62
|
+
/** Teleport onto an edge (a respawn or re-rail); throws on an unknown edge. */
|
|
63
|
+
place(edgeId: string, s?: number): void;
|
|
64
|
+
}
|
|
65
|
+
export declare function createRailGraph(config: RailGraphConfig): RailGraph;
|
|
66
|
+
export declare function createRailRider(graph: RailGraph, config: RailRiderConfig): RailRider;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
function segmentLengths(points) {
|
|
2
|
+
const lengths = [];
|
|
3
|
+
let total = 0;
|
|
4
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
5
|
+
const a = points[i];
|
|
6
|
+
const b = points[i + 1];
|
|
7
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
|
|
8
|
+
lengths.push(length);
|
|
9
|
+
total += length;
|
|
10
|
+
}
|
|
11
|
+
return { lengths, total };
|
|
12
|
+
}
|
|
13
|
+
export function createRailGraph(config) {
|
|
14
|
+
const nodes = new Map();
|
|
15
|
+
for (const node of config.nodes) {
|
|
16
|
+
if (nodes.has(node.id))
|
|
17
|
+
throw new Error(`duplicate rail node: ${node.id}`);
|
|
18
|
+
nodes.set(node.id, node);
|
|
19
|
+
}
|
|
20
|
+
const edges = new Map();
|
|
21
|
+
const outByNode = new Map();
|
|
22
|
+
const switches = new Map();
|
|
23
|
+
const edgeOrder = [];
|
|
24
|
+
for (const edge of config.edges) {
|
|
25
|
+
if (edges.has(edge.id))
|
|
26
|
+
throw new Error(`duplicate rail edge: ${edge.id}`);
|
|
27
|
+
const from = nodes.get(edge.from);
|
|
28
|
+
const to = nodes.get(edge.to);
|
|
29
|
+
if (from === undefined || to === undefined) {
|
|
30
|
+
throw new Error(`rail edge "${edge.id}" references unknown node "${from === undefined ? edge.from : edge.to}"`);
|
|
31
|
+
}
|
|
32
|
+
const points = [from.at, ...(edge.via ?? []), to.at];
|
|
33
|
+
const { total } = segmentLengths(points);
|
|
34
|
+
edges.set(edge.id, { id: edge.id, from: edge.from, to: edge.to, points, length: total });
|
|
35
|
+
edgeOrder.push(edge.id);
|
|
36
|
+
const out = outByNode.get(edge.from) ?? [];
|
|
37
|
+
out.push(edge.id);
|
|
38
|
+
outByNode.set(edge.from, out);
|
|
39
|
+
if (!switches.has(edge.from))
|
|
40
|
+
switches.set(edge.from, edge.id);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
node: (id) => nodes.get(id) ?? null,
|
|
44
|
+
edge: (id) => edges.get(id) ?? null,
|
|
45
|
+
edges: () => edgeOrder.slice(),
|
|
46
|
+
outEdges: (nodeId) => outByNode.get(nodeId) ?? [],
|
|
47
|
+
thrownEdge: (nodeId) => switches.get(nodeId) ?? null,
|
|
48
|
+
throwSwitch(nodeId, edgeId) {
|
|
49
|
+
const out = outByNode.get(nodeId);
|
|
50
|
+
if (out === undefined || !out.includes(edgeId))
|
|
51
|
+
return false;
|
|
52
|
+
switches.set(nodeId, edgeId);
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function createRailRider(graph, config) {
|
|
58
|
+
let edge = graph.edge(config.edgeId);
|
|
59
|
+
if (edge === null)
|
|
60
|
+
throw new Error(`unknown rail edge: ${config.edgeId}`);
|
|
61
|
+
let s = Math.max(0, Math.min(config.s ?? 0, edge.length));
|
|
62
|
+
let speed = config.speed;
|
|
63
|
+
let deadEnd = false;
|
|
64
|
+
function poseOn(current, arc) {
|
|
65
|
+
const { lengths } = segmentLengths(current.points);
|
|
66
|
+
let remaining = arc;
|
|
67
|
+
for (let i = 0; i < lengths.length; i += 1) {
|
|
68
|
+
const length = lengths[i];
|
|
69
|
+
if (remaining <= length || i === lengths.length - 1) {
|
|
70
|
+
const a = current.points[i];
|
|
71
|
+
const b = current.points[i + 1];
|
|
72
|
+
const fraction = length <= 1e-9 ? 0 : Math.min(1, remaining / length);
|
|
73
|
+
return {
|
|
74
|
+
position: [
|
|
75
|
+
a[0] + (b[0] - a[0]) * fraction,
|
|
76
|
+
a[1] + (b[1] - a[1]) * fraction,
|
|
77
|
+
a[2] + (b[2] - a[2]) * fraction,
|
|
78
|
+
],
|
|
79
|
+
heading: Math.atan2(b[0] - a[0], b[2] - a[2]),
|
|
80
|
+
edgeId: current.id,
|
|
81
|
+
s: arc,
|
|
82
|
+
atDeadEnd: deadEnd,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
remaining -= length;
|
|
86
|
+
}
|
|
87
|
+
const last = current.points[current.points.length - 1];
|
|
88
|
+
return { position: last, heading: 0, edgeId: current.id, s: arc, atDeadEnd: deadEnd };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
advance(dt) {
|
|
92
|
+
if (deadEnd || dt <= 0 || speed <= 0)
|
|
93
|
+
return poseOn(edge, s);
|
|
94
|
+
let budget = speed * dt;
|
|
95
|
+
let guard = 64;
|
|
96
|
+
while (budget > 1e-9 && guard > 0) {
|
|
97
|
+
guard -= 1;
|
|
98
|
+
const room = edge.length - s;
|
|
99
|
+
if (budget < room) {
|
|
100
|
+
s += budget;
|
|
101
|
+
budget = 0;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
budget -= room;
|
|
105
|
+
const nextId = graph.thrownEdge(edge.to);
|
|
106
|
+
if (nextId === null) {
|
|
107
|
+
s = edge.length;
|
|
108
|
+
deadEnd = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
edge = graph.edge(nextId);
|
|
112
|
+
s = 0;
|
|
113
|
+
}
|
|
114
|
+
return poseOn(edge, s);
|
|
115
|
+
},
|
|
116
|
+
pose: () => poseOn(edge, s),
|
|
117
|
+
setSpeed(next) {
|
|
118
|
+
speed = next;
|
|
119
|
+
},
|
|
120
|
+
place(edgeId, arc = 0) {
|
|
121
|
+
const next = graph.edge(edgeId);
|
|
122
|
+
if (next === null)
|
|
123
|
+
throw new Error(`unknown rail edge: ${edgeId}`);
|
|
124
|
+
edge = next;
|
|
125
|
+
s = Math.max(0, Math.min(arc, next.length));
|
|
126
|
+
deadEnd = false;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|