@jgengine/core 0.8.0 → 0.10.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 +36 -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 +13 -0
- package/dist/ai/spawnDirector.js +41 -17
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +214 -129
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- 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 +52 -0
- package/dist/combat/shotOrigin.js +86 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +44 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +25 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +14 -1
- package/dist/game/lootTable.js +28 -7
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +116 -4
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +27 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +33 -3
- package/dist/input/axisInput.js +60 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +44 -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 +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +72 -8
- package/dist/runtime/gameContext.js +374 -61
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +18 -0
- package/dist/runtime/inputSnapshot.js +7 -0
- package/dist/runtime/motionIntents.d.ts +16 -6
- package/dist/runtime/motionIntents.js +31 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +50 -0
- package/dist/scene/colliders.js +101 -0
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +51 -5
- package/dist/scene/entityStore.js +119 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +17 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- 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/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -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 +212 -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 +21 -4
- package/dist/world/environmentSummary.js +22 -3
- package/dist/world/features.d.ts +185 -8
- package/dist/world/features.js +64 -6
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +69 -3
- package/dist/world/terrain.js +309 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +3013 -1726
- package/package.json +12 -2
|
@@ -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
|
+
}
|
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
|
|
@@ -152,8 +178,8 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
152
178
|
const stepCost = options.stepCost;
|
|
153
179
|
const startCell = grid.cellAt(from);
|
|
154
180
|
const goalCell = grid.cellAt(to);
|
|
155
|
-
const start = passable(startCell.col, startCell.row) ? startCell : grid
|
|
156
|
-
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);
|
|
157
183
|
if (start === null || goal === null)
|
|
158
184
|
return null;
|
|
159
185
|
const cols = grid.cols;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Waypoint } from "./pathFollow.js";
|
|
2
|
+
export interface TimetableStop {
|
|
3
|
+
at: Waypoint;
|
|
4
|
+
/** Seconds the mover holds at this waypoint before departing. */
|
|
5
|
+
dwellSeconds?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface RouteTimetableConfig {
|
|
8
|
+
waypoints: readonly (Waypoint | TimetableStop)[];
|
|
9
|
+
/** World units per second between stops. */
|
|
10
|
+
speed: number;
|
|
11
|
+
/** Wrap from the last waypoint back to the first (a circular line). Default `false` — the mover holds at the terminus. */
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
/** Default dwell applied at every waypoint without its own `dwellSeconds`; default `0`. */
|
|
14
|
+
dwellSeconds?: number;
|
|
15
|
+
/** Shifts the whole timetable so `t = 0` lands mid-route — staggers several movers on one route. */
|
|
16
|
+
offsetSeconds?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface TimetablePose {
|
|
19
|
+
position: Waypoint;
|
|
20
|
+
/** Yaw (radians) of the travel direction, matching engine `rotationY`; holds through dwells. */
|
|
21
|
+
heading: number;
|
|
22
|
+
/** Index of the segment being travelled (or the stop being dwelt at). */
|
|
23
|
+
segment: number;
|
|
24
|
+
/** True while holding at a stop. */
|
|
25
|
+
dwelling: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A deterministic mover over an authored route: position is a pure function of absolute time, so a
|
|
29
|
+
* forecast preview (`poseAt(t + 30)`) and the live mover can never disagree — the seam `nav/pathFollow`'s
|
|
30
|
+
* dt-incremental state can't offer. Trains, ferries, patrol shuttles, crushers: anything on a timetable.
|
|
31
|
+
*/
|
|
32
|
+
export interface RouteTimetable {
|
|
33
|
+
/** Seconds for one full cycle (travel + dwells); non-loop routes hold at the terminus after this. */
|
|
34
|
+
readonly cycleSeconds: number;
|
|
35
|
+
readonly distance: number;
|
|
36
|
+
poseAt(t: number): TimetablePose;
|
|
37
|
+
positionAt(t: number): Waypoint;
|
|
38
|
+
/** Cycle progress in `[0, 1]` — clamps at `1` on a non-looping route's terminus. */
|
|
39
|
+
progressAt(t: number): number;
|
|
40
|
+
}
|
|
41
|
+
export declare function createRouteTimetable(config: RouteTimetableConfig): RouteTimetable;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
function stopOf(entry) {
|
|
2
|
+
return Array.isArray(entry) ? { at: entry } : entry;
|
|
3
|
+
}
|
|
4
|
+
export function createRouteTimetable(config) {
|
|
5
|
+
if (!(config.speed > 0))
|
|
6
|
+
throw new Error(`timetable speed must be positive, got ${config.speed}`);
|
|
7
|
+
const stops = config.waypoints.map(stopOf);
|
|
8
|
+
if (stops.length < 2)
|
|
9
|
+
throw new Error("timetable needs at least two waypoints");
|
|
10
|
+
const loop = config.loop ?? false;
|
|
11
|
+
const defaultDwell = Math.max(0, config.dwellSeconds ?? 0);
|
|
12
|
+
const offset = config.offsetSeconds ?? 0;
|
|
13
|
+
const legs = [];
|
|
14
|
+
let clock = 0;
|
|
15
|
+
let distance = 0;
|
|
16
|
+
const legCount = loop ? stops.length : stops.length - 1;
|
|
17
|
+
for (let i = 0; i < legCount; i += 1) {
|
|
18
|
+
const from = stops[i];
|
|
19
|
+
const to = stops[(i + 1) % stops.length];
|
|
20
|
+
const dwell = Math.max(0, from.dwellSeconds ?? defaultDwell);
|
|
21
|
+
const dx = to.at[0] - from.at[0];
|
|
22
|
+
const dy = to.at[1] - from.at[1];
|
|
23
|
+
const dz = to.at[2] - from.at[2];
|
|
24
|
+
const length = Math.hypot(dx, dy, dz);
|
|
25
|
+
const heading = length > 1e-9 ? Math.atan2(dx, dz) : 0;
|
|
26
|
+
legs.push({
|
|
27
|
+
from: from.at,
|
|
28
|
+
to: to.at,
|
|
29
|
+
heading,
|
|
30
|
+
startSeconds: clock,
|
|
31
|
+
travelSeconds: length / config.speed,
|
|
32
|
+
dwellSeconds: dwell,
|
|
33
|
+
});
|
|
34
|
+
clock += dwell + length / config.speed;
|
|
35
|
+
distance += length;
|
|
36
|
+
}
|
|
37
|
+
const cycleSeconds = clock;
|
|
38
|
+
const terminus = stops[stops.length - 1].at;
|
|
39
|
+
const lastHeading = legs[legs.length - 1].heading;
|
|
40
|
+
function poseAt(t) {
|
|
41
|
+
let local = t + offset;
|
|
42
|
+
if (loop) {
|
|
43
|
+
local = ((local % cycleSeconds) + cycleSeconds) % cycleSeconds;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
local = Math.max(0, local);
|
|
47
|
+
if (local >= cycleSeconds) {
|
|
48
|
+
return { position: terminus, heading: lastHeading, segment: legs.length - 1, dwelling: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let leg = legs[legs.length - 1];
|
|
52
|
+
let segment = legs.length - 1;
|
|
53
|
+
for (let i = 0; i < legs.length; i += 1) {
|
|
54
|
+
const candidate = legs[i];
|
|
55
|
+
if (local < candidate.startSeconds + candidate.dwellSeconds + candidate.travelSeconds) {
|
|
56
|
+
leg = candidate;
|
|
57
|
+
segment = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const intoLeg = local - leg.startSeconds;
|
|
62
|
+
if (intoLeg < leg.dwellSeconds) {
|
|
63
|
+
const previous = legs[(segment - 1 + legs.length) % legs.length];
|
|
64
|
+
const heading = segment === 0 && !loop ? leg.heading : previous.heading;
|
|
65
|
+
return { position: leg.from, heading, segment, dwelling: true };
|
|
66
|
+
}
|
|
67
|
+
const travelled = intoLeg - leg.dwellSeconds;
|
|
68
|
+
const fraction = leg.travelSeconds <= 0 ? 1 : Math.min(1, travelled / leg.travelSeconds);
|
|
69
|
+
return {
|
|
70
|
+
position: [
|
|
71
|
+
leg.from[0] + (leg.to[0] - leg.from[0]) * fraction,
|
|
72
|
+
leg.from[1] + (leg.to[1] - leg.from[1]) * fraction,
|
|
73
|
+
leg.from[2] + (leg.to[2] - leg.from[2]) * fraction,
|
|
74
|
+
],
|
|
75
|
+
heading: leg.heading,
|
|
76
|
+
segment,
|
|
77
|
+
dwelling: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
cycleSeconds,
|
|
82
|
+
distance,
|
|
83
|
+
poseAt,
|
|
84
|
+
positionAt: (t) => poseAt(t).position,
|
|
85
|
+
progressAt(t) {
|
|
86
|
+
if (cycleSeconds <= 0)
|
|
87
|
+
return 1;
|
|
88
|
+
const local = t + offset;
|
|
89
|
+
if (loop)
|
|
90
|
+
return (((local % cycleSeconds) + cycleSeconds) % cycleSeconds) / cycleSeconds;
|
|
91
|
+
return Math.max(0, Math.min(1, local / cycleSeconds));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -15,6 +15,10 @@ export interface BuoyantBodyConfig {
|
|
|
15
15
|
topSpeed?: number;
|
|
16
16
|
turnRate?: number;
|
|
17
17
|
turnSpeedRef?: number;
|
|
18
|
+
/** Water-current acceleration (units/s²) sampled at the hull each tick and applied while submerged — feed a `windZones`/`flowTube` sample or a fixed vector. */
|
|
19
|
+
current?: (x: number, z: number, time: number) => readonly [number, number];
|
|
20
|
+
/** How much more current a broadside hull catches than a bow-on one (`1` = uniform, default; `2` = side-on drifts twice as hard). */
|
|
21
|
+
currentBroadside?: number;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Floats a {@link PhysicsWorld} body on a CPU {@link WaterSurface}: each hull sample point pushes the
|
|
@@ -38,6 +42,8 @@ export declare class BuoyantBody {
|
|
|
38
42
|
private readonly topSpeed;
|
|
39
43
|
private readonly turnRate;
|
|
40
44
|
private readonly turnSpeedRef;
|
|
45
|
+
private readonly current;
|
|
46
|
+
private readonly currentBroadside;
|
|
41
47
|
private submergedFlag;
|
|
42
48
|
constructor(world: PhysicsWorld, config: BuoyantBodyConfig);
|
|
43
49
|
get position(): [number, number, number];
|
package/dist/physics/buoyancy.js
CHANGED
|
@@ -26,6 +26,8 @@ export class BuoyantBody {
|
|
|
26
26
|
topSpeed;
|
|
27
27
|
turnRate;
|
|
28
28
|
turnSpeedRef;
|
|
29
|
+
current;
|
|
30
|
+
currentBroadside;
|
|
29
31
|
submergedFlag = false;
|
|
30
32
|
constructor(world, config) {
|
|
31
33
|
this.world = world;
|
|
@@ -42,6 +44,8 @@ export class BuoyantBody {
|
|
|
42
44
|
this.topSpeed = config.topSpeed ?? 14;
|
|
43
45
|
this.turnRate = config.turnRate ?? 1.2;
|
|
44
46
|
this.turnSpeedRef = config.turnSpeedRef ?? 4;
|
|
47
|
+
this.current = config.current ?? null;
|
|
48
|
+
this.currentBroadside = config.currentBroadside ?? 1;
|
|
45
49
|
}
|
|
46
50
|
get position() {
|
|
47
51
|
return [this.world.posX[this.body], this.world.posY[this.body], this.world.posZ[this.body]];
|
|
@@ -81,6 +85,16 @@ export class BuoyantBody {
|
|
|
81
85
|
const drag = Math.max(0, 1 - this.linearDrag * dt);
|
|
82
86
|
w.velX[b] *= drag;
|
|
83
87
|
w.velZ[b] *= drag;
|
|
88
|
+
if (this.current !== null) {
|
|
89
|
+
const [cx, cz] = this.current(w.posX[b], w.posZ[b], time);
|
|
90
|
+
const magnitude = Math.hypot(cx, cz);
|
|
91
|
+
if (magnitude > 0) {
|
|
92
|
+
const alignment = Math.abs((cx * sin + cz * cos) / magnitude);
|
|
93
|
+
const scale = this.currentBroadside + (1 - this.currentBroadside) * alignment;
|
|
94
|
+
w.velX[b] += cx * scale * dt;
|
|
95
|
+
w.velZ[b] += cz * scale * dt;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
84
98
|
this.submergedFlag = true;
|
|
85
99
|
w.wake(b);
|
|
86
100
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type FlowVec3 = readonly [number, number, number];
|
|
2
|
+
export interface FlowTubeConfig {
|
|
3
|
+
from: FlowVec3;
|
|
4
|
+
to: FlowVec3;
|
|
5
|
+
/** Tube radius; flow strength fades from the axis to zero here. */
|
|
6
|
+
radius: number;
|
|
7
|
+
/** Axial flow speed on the core at full spool. */
|
|
8
|
+
strength: number;
|
|
9
|
+
/** Radial falloff exponent — `1` linear, `2` (default) keeps a fat core with soft edges. */
|
|
10
|
+
falloff?: number;
|
|
11
|
+
/** Extra reach past each end where the axial envelope fades to zero; default `0` (hard caps). */
|
|
12
|
+
capFalloff?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* An axial corridor of directional flow with radial core falloff and a spool scalar — fan tunnels,
|
|
16
|
+
* updraft shafts, river narrows, thruster wash. Pure math: sample `velocityAt(point, spool)` and add
|
|
17
|
+
* it to whatever integrator moves the body (walk controller drift, `BuoyantBody`, a custom sim).
|
|
18
|
+
*/
|
|
19
|
+
export interface FlowTube {
|
|
20
|
+
readonly axis: FlowVec3;
|
|
21
|
+
readonly length: number;
|
|
22
|
+
/** Flow velocity at a world point, scaled by `spool` in `[0, 1]` (default `1`); zero outside the tube. */
|
|
23
|
+
velocityAt(point: FlowVec3, spool?: number): FlowVec3;
|
|
24
|
+
/** The radial × axial envelope in `[0, 1]` at a world point — feed particle density, audio gain, rumble. */
|
|
25
|
+
intensityAt(point: FlowVec3): number;
|
|
26
|
+
}
|
|
27
|
+
export declare function createFlowTube(config: FlowTubeConfig): FlowTube;
|
|
28
|
+
/** Sum several tubes' flow at one point — each entry carries its own spool. */
|
|
29
|
+
export declare function combineFlowVelocity(tubes: readonly (FlowTube | {
|
|
30
|
+
tube: FlowTube;
|
|
31
|
+
spool: number;
|
|
32
|
+
})[], point: FlowVec3): FlowVec3;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function createFlowTube(config) {
|
|
2
|
+
if (!(config.radius > 0))
|
|
3
|
+
throw new Error(`flow tube radius must be positive, got ${config.radius}`);
|
|
4
|
+
const [fx, fy, fz] = config.from;
|
|
5
|
+
const dx = config.to[0] - fx;
|
|
6
|
+
const dy = config.to[1] - fy;
|
|
7
|
+
const dz = config.to[2] - fz;
|
|
8
|
+
const length = Math.hypot(dx, dy, dz);
|
|
9
|
+
if (length <= 1e-9)
|
|
10
|
+
throw new Error("flow tube needs distinct from/to points");
|
|
11
|
+
const axis = [dx / length, dy / length, dz / length];
|
|
12
|
+
const falloff = config.falloff ?? 2;
|
|
13
|
+
const capFalloff = Math.max(0, config.capFalloff ?? 0);
|
|
14
|
+
function envelopeAt(point) {
|
|
15
|
+
const px = point[0] - fx;
|
|
16
|
+
const py = point[1] - fy;
|
|
17
|
+
const pz = point[2] - fz;
|
|
18
|
+
const along = px * axis[0] + py * axis[1] + pz * axis[2];
|
|
19
|
+
if (along < -capFalloff || along > length + capFalloff)
|
|
20
|
+
return 0;
|
|
21
|
+
const axial = capFalloff <= 0
|
|
22
|
+
? along >= 0 && along <= length
|
|
23
|
+
? 1
|
|
24
|
+
: 0
|
|
25
|
+
: along < 0
|
|
26
|
+
? 1 + along / capFalloff
|
|
27
|
+
: along > length
|
|
28
|
+
? 1 - (along - length) / capFalloff
|
|
29
|
+
: 1;
|
|
30
|
+
if (axial <= 0)
|
|
31
|
+
return 0;
|
|
32
|
+
const rx = px - axis[0] * along;
|
|
33
|
+
const ry = py - axis[1] * along;
|
|
34
|
+
const rz = pz - axis[2] * along;
|
|
35
|
+
const radial = Math.hypot(rx, ry, rz);
|
|
36
|
+
if (radial >= config.radius)
|
|
37
|
+
return 0;
|
|
38
|
+
return axial * Math.pow(1 - radial / config.radius, falloff);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
axis,
|
|
42
|
+
length,
|
|
43
|
+
velocityAt(point, spool = 1) {
|
|
44
|
+
const scale = envelopeAt(point) * config.strength * Math.max(0, Math.min(1, spool));
|
|
45
|
+
return scale <= 0 ? [0, 0, 0] : [axis[0] * scale, axis[1] * scale, axis[2] * scale];
|
|
46
|
+
},
|
|
47
|
+
intensityAt: envelopeAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Sum several tubes' flow at one point — each entry carries its own spool. */
|
|
51
|
+
export function combineFlowVelocity(tubes, point) {
|
|
52
|
+
let x = 0;
|
|
53
|
+
let y = 0;
|
|
54
|
+
let z = 0;
|
|
55
|
+
for (const entry of tubes) {
|
|
56
|
+
const tube = "velocityAt" in entry ? entry : entry.tube;
|
|
57
|
+
const spool = "velocityAt" in entry ? 1 : entry.spool;
|
|
58
|
+
const [vx, vy, vz] = tube.velocityAt(point, spool);
|
|
59
|
+
x += vx;
|
|
60
|
+
y += vy;
|
|
61
|
+
z += vz;
|
|
62
|
+
}
|
|
63
|
+
return [x, y, z];
|
|
64
|
+
}
|
|
@@ -27,6 +27,33 @@ export declare class ForceVolume {
|
|
|
27
27
|
constructor(config: ForceVolumeConfig);
|
|
28
28
|
apply(world: PhysicsWorld, dt: number): void;
|
|
29
29
|
}
|
|
30
|
+
/** One tick's force math for a body outside `PhysicsWorld` — apply the returned velocity in any custom integrator. */
|
|
31
|
+
export declare function applyVolumeForce(velocity: readonly [number, number, number], force: readonly [number, number, number], mode: ForceMode, dt: number): readonly [number, number, number];
|
|
32
|
+
export interface VolumeTriggerConfig {
|
|
33
|
+
bounds: PhysicsBounds;
|
|
34
|
+
}
|
|
35
|
+
export interface VolumeTriggerStep<TId> {
|
|
36
|
+
/** Ids inside this tick that were outside last tick — the boost-pad edge. */
|
|
37
|
+
entered: readonly TId[];
|
|
38
|
+
/** Every id inside this tick. */
|
|
39
|
+
inside: readonly TId[];
|
|
40
|
+
/** Ids inside last tick that left. */
|
|
41
|
+
exited: readonly TId[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The enter-once membership tracking from `ForceVolume`, freed from `PhysicsWorld`'s body indices
|
|
45
|
+
* (#286.8): feed any integrator's `{ id, position }` list each tick and act on the edges — apply
|
|
46
|
+
* `applyVolumeForce` on `entered` for a boost pad, on `inside` for a fan.
|
|
47
|
+
*/
|
|
48
|
+
export interface VolumeTrigger<TId> {
|
|
49
|
+
readonly bounds: PhysicsBounds;
|
|
50
|
+
step(bodies: Iterable<{
|
|
51
|
+
id: TId;
|
|
52
|
+
position: readonly [number, number, number];
|
|
53
|
+
}>): VolumeTriggerStep<TId>;
|
|
54
|
+
reset(): void;
|
|
55
|
+
}
|
|
56
|
+
export declare function createVolumeTrigger<TId = string>(config: VolumeTriggerConfig): VolumeTrigger<TId>;
|
|
30
57
|
export interface PlatformCarryConfig {
|
|
31
58
|
/** Vertical gap between a rider's base and the platform top counted as "standing on". Default 0.12. */
|
|
32
59
|
contactTolerance?: number;
|