@jgengine/core 0.8.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 +16 -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.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -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/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- 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 +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/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/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- 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 +29 -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 +1 -0
- package/dist/index.js +1 -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/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/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -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/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/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- 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 +21 -0
- package/dist/runtime/gameContext.js +184 -9
- 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/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -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/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -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.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- 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/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 +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- 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/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -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 +2585 -1704
- package/package.json +12 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function createGrappleSwing(config = {}) {
|
|
2
|
+
const reelSpeed = config.reelSpeed ?? 0;
|
|
3
|
+
const minLength = config.minLength ?? 1;
|
|
4
|
+
const stiffness = config.stiffness ?? 0;
|
|
5
|
+
const damping = config.damping ?? 0.1;
|
|
6
|
+
let anchor = null;
|
|
7
|
+
let ropeLength = 0;
|
|
8
|
+
return {
|
|
9
|
+
fire(nextAnchor, from) {
|
|
10
|
+
anchor = nextAnchor;
|
|
11
|
+
ropeLength = Math.max(minLength, Math.hypot(from[0] - nextAnchor[0], from[1] - nextAnchor[1], from[2] - nextAnchor[2]));
|
|
12
|
+
},
|
|
13
|
+
release() {
|
|
14
|
+
anchor = null;
|
|
15
|
+
},
|
|
16
|
+
state: () => ({ attached: anchor !== null, anchor, ropeLength }),
|
|
17
|
+
step(position, velocity, dt, reeling = false) {
|
|
18
|
+
if (anchor === null || dt <= 0)
|
|
19
|
+
return { position, velocity };
|
|
20
|
+
if (reeling && reelSpeed > 0)
|
|
21
|
+
ropeLength = Math.max(minLength, ropeLength - reelSpeed * dt);
|
|
22
|
+
let px = position[0];
|
|
23
|
+
let py = position[1];
|
|
24
|
+
let pz = position[2];
|
|
25
|
+
let vx = velocity[0];
|
|
26
|
+
let vy = velocity[1];
|
|
27
|
+
let vz = velocity[2];
|
|
28
|
+
const dx = px - anchor[0];
|
|
29
|
+
const dy = py - anchor[1];
|
|
30
|
+
const dz = pz - anchor[2];
|
|
31
|
+
const distance = Math.hypot(dx, dy, dz);
|
|
32
|
+
if (distance > ropeLength && distance > 1e-9) {
|
|
33
|
+
const nx = dx / distance;
|
|
34
|
+
const ny = dy / distance;
|
|
35
|
+
const nz = dz / distance;
|
|
36
|
+
px = anchor[0] + nx * ropeLength;
|
|
37
|
+
py = anchor[1] + ny * ropeLength;
|
|
38
|
+
pz = anchor[2] + nz * ropeLength;
|
|
39
|
+
const radial = vx * nx + vy * ny + vz * nz;
|
|
40
|
+
if (radial > 0) {
|
|
41
|
+
vx -= nx * radial;
|
|
42
|
+
vy -= ny * radial;
|
|
43
|
+
vz -= nz * radial;
|
|
44
|
+
}
|
|
45
|
+
if (stiffness > 0) {
|
|
46
|
+
const pull = (distance - ropeLength) * stiffness * dt;
|
|
47
|
+
vx -= nx * pull;
|
|
48
|
+
vy -= ny * pull;
|
|
49
|
+
vz -= nz * pull;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const damp = Math.max(0, 1 - damping * dt);
|
|
53
|
+
return { position: [px, py, pz], velocity: [vx * damp, vy * damp, vz * damp] };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type TrailPoint = readonly [number, number, number];
|
|
2
|
+
export interface LeaderTrailConfig {
|
|
3
|
+
/** Arc-length gap between consecutive followers. */
|
|
4
|
+
spacing: number;
|
|
5
|
+
/** Followers the trail must be able to serve; history is capped to cover them. Default `16`. */
|
|
6
|
+
maxFollowers?: number;
|
|
7
|
+
/** Minimum leader movement before a new trail sample is recorded; default `spacing / 8`. */
|
|
8
|
+
sampleDistance?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TrailPose {
|
|
11
|
+
position: TrailPoint;
|
|
12
|
+
/** Yaw (radians) of the trail direction at this point, matching engine `rotationY`. */
|
|
13
|
+
heading: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A live breadcrumb trail recorded from a moving leader, with followers placed by arc-length
|
|
17
|
+
* behind the head — conga lines, convoys, duckling chains, snake bodies. Call `record` with the
|
|
18
|
+
* leader's position every tick; read `followerAt(i)` for the pose `spacing * (i + 1)` behind.
|
|
19
|
+
*/
|
|
20
|
+
export interface LeaderTrail {
|
|
21
|
+
record(position: TrailPoint): void;
|
|
22
|
+
/** Pose for follower `index` (0 = closest behind the leader); `null` until enough trail exists. */
|
|
23
|
+
followerAt(index: number): TrailPose | null;
|
|
24
|
+
/** Recorded arc length available behind the leader. */
|
|
25
|
+
length(): number;
|
|
26
|
+
/** Drop all history (teleport, respawn) and optionally restart from a position. */
|
|
27
|
+
reset(position?: TrailPoint): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function createLeaderTrail(config: LeaderTrailConfig): LeaderTrail;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function createLeaderTrail(config) {
|
|
2
|
+
if (!(config.spacing > 0))
|
|
3
|
+
throw new Error(`leader trail spacing must be positive, got ${config.spacing}`);
|
|
4
|
+
const spacing = config.spacing;
|
|
5
|
+
const maxFollowers = Math.max(1, config.maxFollowers ?? 16);
|
|
6
|
+
const sampleDistance = config.sampleDistance ?? spacing / 8;
|
|
7
|
+
const capacity = spacing * (maxFollowers + 1);
|
|
8
|
+
let points = [];
|
|
9
|
+
let lengths = [];
|
|
10
|
+
let total = 0;
|
|
11
|
+
function trim() {
|
|
12
|
+
while (total > capacity && lengths.length > 0) {
|
|
13
|
+
const tailSegment = lengths[0];
|
|
14
|
+
if (total - tailSegment < capacity)
|
|
15
|
+
break;
|
|
16
|
+
lengths.shift();
|
|
17
|
+
points.shift();
|
|
18
|
+
total -= tailSegment;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
record(position) {
|
|
23
|
+
const head = points[points.length - 1];
|
|
24
|
+
if (head === undefined) {
|
|
25
|
+
points.push(position);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const step = Math.hypot(position[0] - head[0], position[1] - head[1], position[2] - head[2]);
|
|
29
|
+
if (step < sampleDistance)
|
|
30
|
+
return;
|
|
31
|
+
points.push(position);
|
|
32
|
+
lengths.push(step);
|
|
33
|
+
total += step;
|
|
34
|
+
trim();
|
|
35
|
+
},
|
|
36
|
+
followerAt(index) {
|
|
37
|
+
const behind = spacing * (index + 1);
|
|
38
|
+
if (behind > total)
|
|
39
|
+
return null;
|
|
40
|
+
let remaining = behind;
|
|
41
|
+
for (let i = lengths.length - 1; i >= 0; i -= 1) {
|
|
42
|
+
const segment = lengths[i];
|
|
43
|
+
const to = points[i + 1];
|
|
44
|
+
const from = points[i];
|
|
45
|
+
if (remaining <= segment) {
|
|
46
|
+
const fraction = segment <= 1e-9 ? 0 : remaining / segment;
|
|
47
|
+
return {
|
|
48
|
+
position: [
|
|
49
|
+
to[0] + (from[0] - to[0]) * fraction,
|
|
50
|
+
to[1] + (from[1] - to[1]) * fraction,
|
|
51
|
+
to[2] + (from[2] - to[2]) * fraction,
|
|
52
|
+
],
|
|
53
|
+
heading: Math.atan2(to[0] - from[0], to[2] - from[2]),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
remaining -= segment;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
},
|
|
60
|
+
length: () => total,
|
|
61
|
+
reset(position) {
|
|
62
|
+
points = position === undefined ? [] : [position];
|
|
63
|
+
lengths = [];
|
|
64
|
+
total = 0;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yaw-frame steering math. The engine's heading convention is
|
|
3
|
+
* `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a
|
|
4
|
+
* positive yaw increment rotate counterclockwise seen from above — a turn to
|
|
5
|
+
* the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt`
|
|
6
|
+
* therefore turns the wrong way; integrate steering through `steerYaw` instead.
|
|
7
|
+
*/
|
|
8
|
+
export type YawVectorXZ = readonly [number, number];
|
|
9
|
+
/** XZ forward direction of a yaw (`rotationY`): `(sin yaw, cos yaw)`. */
|
|
10
|
+
export declare function yawForward(yaw: number): YawVectorXZ;
|
|
11
|
+
/** XZ screen-right of a yaw — `forward × up` with up = +Y: `(-cos yaw, sin yaw)`. */
|
|
12
|
+
export declare function yawRight(yaw: number): YawVectorXZ;
|
|
13
|
+
/**
|
|
14
|
+
* Integrate one steering step. `steerRight` is the signed steer input
|
|
15
|
+
* (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight),
|
|
16
|
+
* `turnRatePerSecond` is radians per second at full lock. Steering right
|
|
17
|
+
* decreases yaw in the engine frame; this helper owns that sign so game
|
|
18
|
+
* code never re-derives it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function steerYaw(yaw: number, steerRight: number, turnRatePerSecond: number, dt: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Signed steer-right input that closes on `desiredYaw` when integrated
|
|
23
|
+
* through `steerYaw` — the seek half every AI driver and homing script
|
|
24
|
+
* needs. Magnitude is the wrapped radian error in (−π, π]; scale and clamp
|
|
25
|
+
* it into [-1, 1] at the call site.
|
|
26
|
+
*/
|
|
27
|
+
export declare function steerToward(yaw: number, desiredYaw: number): number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yaw-frame steering math. The engine's heading convention is
|
|
3
|
+
* `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a
|
|
4
|
+
* positive yaw increment rotate counterclockwise seen from above — a turn to
|
|
5
|
+
* the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt`
|
|
6
|
+
* therefore turns the wrong way; integrate steering through `steerYaw` instead.
|
|
7
|
+
*/
|
|
8
|
+
/** XZ forward direction of a yaw (`rotationY`): `(sin yaw, cos yaw)`. */
|
|
9
|
+
export function yawForward(yaw) {
|
|
10
|
+
return [Math.sin(yaw), Math.cos(yaw)];
|
|
11
|
+
}
|
|
12
|
+
/** XZ screen-right of a yaw — `forward × up` with up = +Y: `(-cos yaw, sin yaw)`. */
|
|
13
|
+
export function yawRight(yaw) {
|
|
14
|
+
return [-Math.cos(yaw), Math.sin(yaw)];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Integrate one steering step. `steerRight` is the signed steer input
|
|
18
|
+
* (+1 = turn right, matching `DRIVE_AXIS_BINDINGS`' KeyD/ArrowRight),
|
|
19
|
+
* `turnRatePerSecond` is radians per second at full lock. Steering right
|
|
20
|
+
* decreases yaw in the engine frame; this helper owns that sign so game
|
|
21
|
+
* code never re-derives it.
|
|
22
|
+
*/
|
|
23
|
+
export function steerYaw(yaw, steerRight, turnRatePerSecond, dt) {
|
|
24
|
+
return yaw - steerRight * turnRatePerSecond * dt;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Signed steer-right input that closes on `desiredYaw` when integrated
|
|
28
|
+
* through `steerYaw` — the seek half every AI driver and homing script
|
|
29
|
+
* needs. Magnitude is the wrapped radian error in (−π, π]; scale and clamp
|
|
30
|
+
* it into [-1, 1] at the call site.
|
|
31
|
+
*/
|
|
32
|
+
export function steerToward(yaw, desiredYaw) {
|
|
33
|
+
let delta = (desiredYaw - yaw) % TWO_PI;
|
|
34
|
+
if (delta > Math.PI)
|
|
35
|
+
delta -= TWO_PI;
|
|
36
|
+
if (delta <= -Math.PI)
|
|
37
|
+
delta += TWO_PI;
|
|
38
|
+
return -delta;
|
|
39
|
+
}
|
|
40
|
+
const TWO_PI = Math.PI * 2;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FeedWriteGate = {
|
|
2
|
+
allowedActions: readonly string[];
|
|
3
|
+
};
|
|
4
|
+
export declare function createFeedWriteGate(allowedActions?: readonly string[]): FeedWriteGate;
|
|
5
|
+
export declare function validateFeedWrite(gate: FeedWriteGate | undefined, action: string): {
|
|
6
|
+
ok: true;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createFeedWriteGate(allowedActions = []) {
|
|
2
|
+
return { allowedActions: [...allowedActions] };
|
|
3
|
+
}
|
|
4
|
+
export function validateFeedWrite(gate, action) {
|
|
5
|
+
if (gate === undefined || gate.allowedActions.length === 0) {
|
|
6
|
+
return { ok: false, reason: "client feed writes are disabled" };
|
|
7
|
+
}
|
|
8
|
+
if (!gate.allowedActions.includes(action)) {
|
|
9
|
+
return { ok: false, reason: `feed action not allowed: ${action}` };
|
|
10
|
+
}
|
|
11
|
+
return { ok: true };
|
|
12
|
+
}
|
|
@@ -22,8 +22,10 @@ export interface IncomingPose {
|
|
|
22
22
|
export interface PoseSyncRules {
|
|
23
23
|
/** Speed cap (units/sec) for client-authoritative movement. */
|
|
24
24
|
maxSpeed: number;
|
|
25
|
-
/** Vertical offset clamp (e.g. peak jump height). */
|
|
25
|
+
/** Vertical offset clamp above floorY (e.g. peak jump height). */
|
|
26
26
|
maxVerticalOffset: number;
|
|
27
|
+
/** World-floor Y used as the base of the jump band. Defaults to 0. */
|
|
28
|
+
floorY?: number;
|
|
27
29
|
/** Elapsed-time clamp so stale or bursty clients cannot teleport. */
|
|
28
30
|
minElapsedSec: number;
|
|
29
31
|
maxElapsedSec: number;
|
|
@@ -44,7 +46,17 @@ export interface PoseSyncDecision {
|
|
|
44
46
|
/** True when only the keep-alive stamp should be written. */
|
|
45
47
|
refreshKeepAlive: boolean;
|
|
46
48
|
}
|
|
47
|
-
export declare
|
|
49
|
+
export declare const DEFAULT_SPAWN_ORIGIN: {
|
|
50
|
+
readonly x: 0;
|
|
51
|
+
readonly y: 0;
|
|
52
|
+
readonly z: 0;
|
|
53
|
+
};
|
|
54
|
+
export declare function spawnPresenceState(origin: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
z: number;
|
|
58
|
+
} | undefined, nowMs: number, rules: Pick<PoseSyncRules, "maxElapsedSec">): PresencePoseState;
|
|
59
|
+
export declare function decidePoseSync(current: PresencePoseState, incoming: IncomingPose, rules: PoseSyncRules, nowMs: number, floorY?: number): PoseSyncDecision;
|
|
48
60
|
export declare function shouldRefreshKeepAlive(lastSeenAtMs: number | undefined, nowMs: number, rules: Pick<PoseSyncRules, "keepAliveRefreshMs">): boolean;
|
|
49
61
|
export declare function shouldPersistWorldSnapshot(lastSavedAtMs: number | undefined, nowMs: number, intervalMs: number): boolean;
|
|
50
62
|
export interface ActivePresenceResolution<T> {
|
|
@@ -9,8 +9,18 @@ function appearanceEqual(a, b) {
|
|
|
9
9
|
return false;
|
|
10
10
|
return aKeys.every((key) => a[key] === b[key]);
|
|
11
11
|
}
|
|
12
|
-
export
|
|
13
|
-
|
|
12
|
+
export const DEFAULT_SPAWN_ORIGIN = { x: 0, y: 0, z: 0 };
|
|
13
|
+
export function spawnPresenceState(origin = DEFAULT_SPAWN_ORIGIN, nowMs, rules) {
|
|
14
|
+
return {
|
|
15
|
+
position: { x: origin.x, y: origin.y, z: origin.z },
|
|
16
|
+
rotationY: 0,
|
|
17
|
+
rotationPitch: 0,
|
|
18
|
+
lastSeenAtMs: nowMs - rules.maxElapsedSec * 1000,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function decidePoseSync(current, incoming, rules, nowMs, floorY) {
|
|
22
|
+
const rawElapsedSec = Math.max(0, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000);
|
|
23
|
+
const elapsedSec = Math.min(rules.maxElapsedSec, rawElapsedSec);
|
|
14
24
|
const maxDist = rules.maxSpeed * elapsedSec;
|
|
15
25
|
const maxDistSq = maxDist * maxDist;
|
|
16
26
|
let targetX = incoming.position.x;
|
|
@@ -23,9 +33,11 @@ export function decidePoseSync(current, incoming, rules, nowMs) {
|
|
|
23
33
|
targetX = current.position.x + dx * scale;
|
|
24
34
|
targetZ = current.position.z + dz * scale;
|
|
25
35
|
}
|
|
36
|
+
const floor = floorY ?? rules.floorY ?? 0;
|
|
37
|
+
const ceiling = floor + rules.maxVerticalOffset;
|
|
26
38
|
const nextY = incoming.position.y === undefined
|
|
27
39
|
? current.position.y
|
|
28
|
-
: Math.max(
|
|
40
|
+
: Math.max(floor, Math.min(ceiling, incoming.position.y));
|
|
29
41
|
const nextRotationY = incoming.rotationY ?? current.rotationY;
|
|
30
42
|
const nextRotationPitch = incoming.rotationPitch ?? current.rotationPitch ?? 0;
|
|
31
43
|
const nextAppearance = incoming.appearance ?? current.appearance;
|
|
@@ -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
|
+
}
|