@inventure71/paddockjs 2.0.0 → 3.0.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 +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/restartOptions.js +34 -0
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createSpatialGrid, insertIdIntoGridBounds, candidateIdsFromGrid } from './trackQueryGrid.js';
|
|
2
|
+
import { recordFallback, recordStat } from './trackQueryStats.js';
|
|
3
|
+
|
|
4
|
+
export function createPitQueryIndex(track, cellSize) {
|
|
5
|
+
const pitLane = track.pitLane;
|
|
6
|
+
if (!pitLane?.enabled) return null;
|
|
7
|
+
const bounds = {
|
|
8
|
+
minX: pitLane.bounds.minX - cellSize,
|
|
9
|
+
maxX: pitLane.bounds.maxX + cellSize,
|
|
10
|
+
minY: pitLane.bounds.minY - cellSize,
|
|
11
|
+
maxY: pitLane.bounds.maxY + cellSize,
|
|
12
|
+
};
|
|
13
|
+
const roadGrid = createSpatialGrid(bounds, cellSize);
|
|
14
|
+
const boxGrid = createSpatialGrid(bounds, cellSize);
|
|
15
|
+
const roadSegments = [];
|
|
16
|
+
const routes = {};
|
|
17
|
+
|
|
18
|
+
[
|
|
19
|
+
['entry', pitLane.entry?.roadCenterline],
|
|
20
|
+
['main', pitLane.mainLane?.points],
|
|
21
|
+
['working', pitLane.workingLane?.points],
|
|
22
|
+
['exit', pitLane.exit?.roadCenterline],
|
|
23
|
+
].forEach(([routeId, points]) => {
|
|
24
|
+
routes[routeId] = { start: roadSegments.length };
|
|
25
|
+
indexPitRoute(roadGrid, roadSegments, routeId, points, cellSize);
|
|
26
|
+
routes[routeId].end = roadSegments.length;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const boxCandidates = [];
|
|
30
|
+
[
|
|
31
|
+
...(pitLane.serviceAreas ?? []).flatMap((area) => [
|
|
32
|
+
{ type: 'service-area', target: area, polygon: area.corners },
|
|
33
|
+
{ type: 'service-queue', target: area, polygon: area.queueCorners },
|
|
34
|
+
]),
|
|
35
|
+
...(pitLane.boxes ?? []).map((box) => ({ type: 'garage-box', target: box, polygon: box.corners })),
|
|
36
|
+
]
|
|
37
|
+
.filter((candidate) => Array.isArray(candidate.polygon) && candidate.polygon.length >= 3)
|
|
38
|
+
.forEach((candidate) => {
|
|
39
|
+
const id = boxCandidates.length;
|
|
40
|
+
boxCandidates.push(candidate);
|
|
41
|
+
insertIdIntoGridBounds(boxGrid, id, polygonBounds(candidate.polygon, cellSize));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
roadGrid,
|
|
46
|
+
boxGrid,
|
|
47
|
+
routes,
|
|
48
|
+
roadSegments,
|
|
49
|
+
boxCandidates,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function queryPitBoxCandidates(track, position) {
|
|
54
|
+
const index = track?.queryIndex;
|
|
55
|
+
const pit = index?.pit;
|
|
56
|
+
if (!pit?.boxGrid) {
|
|
57
|
+
recordFallback(index, 'pitFallbackReasons', 'missing-box-index');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!finitePoint(position)) {
|
|
61
|
+
recordFallback(index, 'pitFallbackReasons', 'invalid-position');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
index.stats.pitQueries += 1;
|
|
65
|
+
const ids = candidateIdsFromGrid(index, pit.boxGrid, position, 1);
|
|
66
|
+
if (!ids.length) {
|
|
67
|
+
recordStat(index, 'pitPaths', 'box-grid-miss');
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
recordStat(index, 'pitPaths', 'box-grid-hit');
|
|
71
|
+
return ids.map((id) => pit.boxCandidates[id]).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function queryPitRoadSegmentCandidates(track, routeId, position) {
|
|
75
|
+
const candidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
|
|
76
|
+
return candidatesByRoute?.[routeId] ?? candidatesByRoute;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function queryPitRoadSegmentCandidatesByRoute(track, position) {
|
|
80
|
+
const index = track?.queryIndex;
|
|
81
|
+
const pit = index?.pit;
|
|
82
|
+
if (!pit?.roadGrid) {
|
|
83
|
+
recordFallback(index, 'pitFallbackReasons', 'missing-road-index');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!finitePoint(position)) {
|
|
87
|
+
recordFallback(index, 'pitFallbackReasons', 'invalid-position');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
index.stats.pitQueries += 1;
|
|
91
|
+
const ids = candidateIdsFromGrid(index, pit.roadGrid, position, 1);
|
|
92
|
+
if (!ids.length) {
|
|
93
|
+
recordStat(index, 'pitPaths', 'road-grid-miss');
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const candidatesByRoute = { entry: [], main: [], working: [], exit: [] };
|
|
97
|
+
ids.forEach((id) => {
|
|
98
|
+
const segment = pit.roadSegments[id];
|
|
99
|
+
if (!segment || !candidatesByRoute[segment.routeId]) return;
|
|
100
|
+
candidatesByRoute[segment.routeId].push(segment.segmentIndex);
|
|
101
|
+
});
|
|
102
|
+
recordStat(index, 'pitPaths', 'road-grid-hit');
|
|
103
|
+
return candidatesByRoute;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function indexPitRoute(grid, roadSegments, routeId, points, expansion) {
|
|
107
|
+
if (!Array.isArray(points) || points.length < 2) return;
|
|
108
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
109
|
+
const start = points[index];
|
|
110
|
+
const end = points[index + 1];
|
|
111
|
+
const id = roadSegments.length;
|
|
112
|
+
roadSegments.push({ routeId, segmentIndex: index });
|
|
113
|
+
const bounds = {
|
|
114
|
+
minX: Math.min(start.x, end.x) - expansion,
|
|
115
|
+
maxX: Math.max(start.x, end.x) + expansion,
|
|
116
|
+
minY: Math.min(start.y, end.y) - expansion,
|
|
117
|
+
maxY: Math.max(start.y, end.y) + expansion,
|
|
118
|
+
};
|
|
119
|
+
insertIdIntoGridBounds(grid, id, bounds);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function polygonBounds(polygon, expansion) {
|
|
124
|
+
const bounds = polygon.reduce((current, point) => ({
|
|
125
|
+
minX: Math.min(current.minX, point.x),
|
|
126
|
+
maxX: Math.max(current.maxX, point.x),
|
|
127
|
+
minY: Math.min(current.minY, point.y),
|
|
128
|
+
maxY: Math.max(current.maxY, point.y),
|
|
129
|
+
}), { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
|
|
130
|
+
return {
|
|
131
|
+
minX: bounds.minX - expansion,
|
|
132
|
+
maxX: bounds.maxX + expansion,
|
|
133
|
+
minY: bounds.minY - expansion,
|
|
134
|
+
maxY: bounds.maxY + expansion,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function finitePoint(point) {
|
|
139
|
+
return Number.isFinite(point?.x) && Number.isFinite(point?.y);
|
|
140
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createQueryScratch(index) {
|
|
2
|
+
const segmentCount = Math.max(0, index?.centerline?.segmentCount ?? 0);
|
|
3
|
+
const maxId = Math.max(
|
|
4
|
+
segmentCount,
|
|
5
|
+
index?.pit?.roadSegments?.length ?? 0,
|
|
6
|
+
index?.pit?.boxCandidates?.length ?? 0,
|
|
7
|
+
);
|
|
8
|
+
const grid = index?.segmentGrid ?? index?.grid;
|
|
9
|
+
const cellCount = Math.max(0, (grid?.columns ?? 0) * (grid?.rows ?? 0));
|
|
10
|
+
return {
|
|
11
|
+
candidateMarks: new Uint32Array(Math.max(1, maxId)),
|
|
12
|
+
candidateEpoch: 1,
|
|
13
|
+
sampleMarks: new Uint32Array(Math.max(1, segmentCount)),
|
|
14
|
+
sampleEpoch: 1,
|
|
15
|
+
raySegmentMarks: new Uint32Array(Math.max(1, segmentCount)),
|
|
16
|
+
raySegmentEpoch: 1,
|
|
17
|
+
rayCellMarks: new Uint32Array(Math.max(1, cellCount)),
|
|
18
|
+
rayCellEpoch: 1,
|
|
19
|
+
raySegmentIds: [],
|
|
20
|
+
rayVisitedCells: [],
|
|
21
|
+
rayTraceCache: new Map(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ensureQueryScratch(index) {
|
|
26
|
+
if (index.queryScratch) return index.queryScratch;
|
|
27
|
+
index.queryScratch = createQueryScratch(index);
|
|
28
|
+
return index.queryScratch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ensureScratchArray(scratch, key, minimumLength) {
|
|
32
|
+
if (!scratch[key] || scratch[key].length < minimumLength) {
|
|
33
|
+
scratch[key] = new Uint32Array(Math.max(1, minimumLength));
|
|
34
|
+
}
|
|
35
|
+
return scratch[key];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function nextScratchEpoch(scratch, key, marks) {
|
|
39
|
+
let epoch = (scratch[key] ?? 0) + 1;
|
|
40
|
+
if (epoch >= 0xFFFFFFFF) {
|
|
41
|
+
marks.fill(0);
|
|
42
|
+
epoch = 1;
|
|
43
|
+
}
|
|
44
|
+
scratch[key] = epoch;
|
|
45
|
+
return epoch;
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createQueryStats() {
|
|
2
|
+
return {
|
|
3
|
+
nearestQueries: 0,
|
|
4
|
+
nearestFallbacks: 0,
|
|
5
|
+
nearestPaths: {},
|
|
6
|
+
nearestFallbackReasons: {
|
|
7
|
+
'spatial-grid-no-candidates': 0,
|
|
8
|
+
},
|
|
9
|
+
pitQueries: 0,
|
|
10
|
+
pitFallbacks: 0,
|
|
11
|
+
pitPaths: {},
|
|
12
|
+
pitFallbackReasons: {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function recordFallback(index, bucket, reason) {
|
|
17
|
+
if (!index?.stats) return;
|
|
18
|
+
if (bucket === 'nearestFallbackReasons') index.stats.nearestFallbacks += 1;
|
|
19
|
+
if (bucket === 'pitFallbackReasons') index.stats.pitFallbacks += 1;
|
|
20
|
+
recordStat(index, bucket, reason);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function recordStat(index, bucket, key) {
|
|
24
|
+
if (!index?.stats || !bucket || !key) return;
|
|
25
|
+
const target = index.stats[bucket] ?? {};
|
|
26
|
+
target[key] = (target[key] ?? 0) + 1;
|
|
27
|
+
index.stats[bucket] = target;
|
|
28
|
+
}
|
|
@@ -17,7 +17,6 @@ export function nearestTrackStateForCar(
|
|
|
17
17
|
) {
|
|
18
18
|
const resolvedAllowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
|
|
19
19
|
return nearestTrackState(track, position, progressHint, {
|
|
20
|
-
indexMode: options.indexMode ?? 'legacy',
|
|
21
20
|
...options,
|
|
22
21
|
allowPitOverride: resolvedAllowPitOverride,
|
|
23
22
|
});
|
|
@@ -15,10 +15,32 @@ function forwardVector(car) {
|
|
|
15
15
|
return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function velocityVector(car) {
|
|
19
|
+
if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
|
|
20
|
+
return { x: car.velocityX, y: car.velocityY };
|
|
21
|
+
}
|
|
22
|
+
const forward = forwardVector(car);
|
|
23
|
+
return { x: forward.x * car.speed, y: forward.y * car.speed };
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
function dot(a, b) {
|
|
19
27
|
return a.x * b.x + a.y * b.y;
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
function applyVelocityVector(car, velocity) {
|
|
31
|
+
const speed = clamp(Math.hypot(velocity.x, velocity.y), 0, VEHICLE_LIMITS.maxSpeed);
|
|
32
|
+
if (speed <= 0) {
|
|
33
|
+
car.velocityX = 0;
|
|
34
|
+
car.velocityY = 0;
|
|
35
|
+
car.speed = 0;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const scale = speed / Math.max(1e-9, Math.hypot(velocity.x, velocity.y));
|
|
39
|
+
car.velocityX = velocity.x * scale;
|
|
40
|
+
car.velocityY = velocity.y * scale;
|
|
41
|
+
car.speed = speed;
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
export function resolveCollisionsForSimulation(sim) {
|
|
23
45
|
const collidableCars = sim.cars.filter(isCollidable);
|
|
24
46
|
if (collidableCars.length < 2) return;
|
|
@@ -57,8 +79,8 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
if (oneCarFixed) {
|
|
60
|
-
if (!firstPitControlled)
|
|
61
|
-
if (!secondPitControlled)
|
|
82
|
+
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'advanced');
|
|
83
|
+
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'advanced');
|
|
62
84
|
} else {
|
|
63
85
|
applyContactVelocityResponse(sim, first, second, collision.axis);
|
|
64
86
|
}
|
|
@@ -99,6 +121,37 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
124
|
+
if (
|
|
125
|
+
sim.physicsMode === 'advanced' ||
|
|
126
|
+
Number.isFinite(first.velocityX) ||
|
|
127
|
+
Number.isFinite(second.velocityX)
|
|
128
|
+
) {
|
|
129
|
+
const firstVelocity = velocityVector(first);
|
|
130
|
+
const secondVelocity = velocityVector(second);
|
|
131
|
+
const relativeNormalVelocity = dot({
|
|
132
|
+
x: secondVelocity.x - firstVelocity.x,
|
|
133
|
+
y: secondVelocity.y - firstVelocity.y,
|
|
134
|
+
}, axis);
|
|
135
|
+
|
|
136
|
+
if (relativeNormalVelocity < 0) {
|
|
137
|
+
const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
|
|
138
|
+
firstVelocity.x -= axis.x * impulse;
|
|
139
|
+
firstVelocity.y -= axis.y * impulse;
|
|
140
|
+
secondVelocity.x += axis.x * impulse;
|
|
141
|
+
secondVelocity.y += axis.y * impulse;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
applyVelocityVector(first, {
|
|
145
|
+
x: firstVelocity.x * 0.997,
|
|
146
|
+
y: firstVelocity.y * 0.997,
|
|
147
|
+
});
|
|
148
|
+
applyVelocityVector(second, {
|
|
149
|
+
x: secondVelocity.x * 0.997,
|
|
150
|
+
y: secondVelocity.y * 0.997,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
102
155
|
const firstForward = forwardVector(first);
|
|
103
156
|
const secondForward = forwardVector(second);
|
|
104
157
|
const firstNormal = dot(firstForward, axis);
|
|
@@ -114,3 +167,15 @@ export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
|
114
167
|
first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
115
168
|
second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
116
169
|
}
|
|
170
|
+
|
|
171
|
+
function dampPitContactVelocity(car, factor, syncVelocity = false) {
|
|
172
|
+
if (syncVelocity || Number.isFinite(car.velocityX) || Number.isFinite(car.velocityY)) {
|
|
173
|
+
const velocity = velocityVector(car);
|
|
174
|
+
applyVelocityVector(car, {
|
|
175
|
+
x: velocity.x * factor,
|
|
176
|
+
y: velocity.y * factor,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
car.speed = clamp(car.speed * factor, 0, VEHICLE_LIMITS.maxSpeed);
|
|
181
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { VEHICLE_GEOMETRY, getVehicleGeometryState, vehicleAxes } from './vehicleGeometry.js';
|
|
2
2
|
import { applyWheelSurfaceState } from './wheelSurface.js';
|
|
3
|
+
import { freezeVehicleMotion } from './vehicleKinematics.js';
|
|
3
4
|
import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
|
|
4
5
|
import { markCarDnf } from '../race/retirements.js';
|
|
5
6
|
|
|
6
7
|
export function applyRunoffResponseForSimulation(sim, car) {
|
|
7
8
|
if (car.destroyed) {
|
|
8
|
-
car.
|
|
9
|
+
freezeBarrierMotion(car, sim.physicsMode);
|
|
9
10
|
applyWheelSurfaceState(car, sim.track);
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
@@ -67,17 +68,7 @@ function destroyCarOnBarrier(sim, car) {
|
|
|
67
68
|
car.drsEligible = false;
|
|
68
69
|
car.drsZoneId = null;
|
|
69
70
|
car.drsZoneEnabled = false;
|
|
70
|
-
car.
|
|
71
|
-
car.throttle = 0;
|
|
72
|
-
car.brake = 1;
|
|
73
|
-
car.steeringAngle = 0;
|
|
74
|
-
car.yawRate = 0;
|
|
75
|
-
car.longitudinalAcceleration = 0;
|
|
76
|
-
car.lateralAcceleration = 0;
|
|
77
|
-
car.longitudinalG = 0;
|
|
78
|
-
car.lateralG = 0;
|
|
79
|
-
car.tractionLimited = false;
|
|
80
|
-
car.stabilityState = 'destroyed';
|
|
71
|
+
freezeBarrierMotion(car, sim.physicsMode);
|
|
81
72
|
sim.events.unshift({
|
|
82
73
|
type: 'car-destroyed',
|
|
83
74
|
at: sim.time,
|
|
@@ -86,3 +77,11 @@ function destroyCarOnBarrier(sim, car) {
|
|
|
86
77
|
reason: 'barrier',
|
|
87
78
|
});
|
|
88
79
|
}
|
|
80
|
+
|
|
81
|
+
function freezeBarrierMotion(car, physicsMode) {
|
|
82
|
+
freezeVehicleMotion(car, {
|
|
83
|
+
clearManualControls: true,
|
|
84
|
+
physicsMode,
|
|
85
|
+
stabilityState: 'destroyed',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function freezeVehicleMotion(car, {
|
|
2
|
+
brake = 1,
|
|
3
|
+
clearManualControls = false,
|
|
4
|
+
physicsMode = 'arcade',
|
|
5
|
+
stabilityState = car.stabilityState ?? 'stable',
|
|
6
|
+
} = {}) {
|
|
7
|
+
car.speed = 0;
|
|
8
|
+
if (physicsMode === 'advanced') {
|
|
9
|
+
car.velocityX = 0;
|
|
10
|
+
car.velocityY = 0;
|
|
11
|
+
} else {
|
|
12
|
+
car.velocityX = null;
|
|
13
|
+
car.velocityY = null;
|
|
14
|
+
}
|
|
15
|
+
car.throttle = 0;
|
|
16
|
+
car.brake = brake;
|
|
17
|
+
car.appliedControls = { steering: 0, throttle: 0, brake };
|
|
18
|
+
if (clearManualControls) car.manualControls = null;
|
|
19
|
+
car.steeringAngle = 0;
|
|
20
|
+
car.yawRate = 0;
|
|
21
|
+
car.turnRadius = Infinity;
|
|
22
|
+
car.longitudinalAcceleration = 0;
|
|
23
|
+
car.lateralAcceleration = 0;
|
|
24
|
+
car.longitudinalG = 0;
|
|
25
|
+
car.lateralG = 0;
|
|
26
|
+
car.gripUsage = 0;
|
|
27
|
+
car.slipAngleRadians = 0;
|
|
28
|
+
car.tractionLimited = false;
|
|
29
|
+
car.stabilityState = stabilityState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function syncVehicleVelocityToHeading(car) {
|
|
33
|
+
const speed = Number.isFinite(car.speed) ? car.speed : 0;
|
|
34
|
+
const heading = Number.isFinite(car.heading) ? car.heading : 0;
|
|
35
|
+
car.velocityX = Math.cos(heading) * speed;
|
|
36
|
+
car.velocityY = Math.sin(heading) * speed;
|
|
37
|
+
}
|
|
@@ -11,7 +11,7 @@ import { VEHICLE_GEOMETRY, getCarCorners, vehicleAxes } from './vehicleGeometry.
|
|
|
11
11
|
|
|
12
12
|
const G = 9.80665;
|
|
13
13
|
|
|
14
|
-
export const PHYSICS_MODES = ['arcade', '
|
|
14
|
+
export const PHYSICS_MODES = ['arcade', 'advanced'];
|
|
15
15
|
export const DEFAULT_PHYSICS_MODE = 'arcade';
|
|
16
16
|
|
|
17
17
|
export const VEHICLE_LIMITS = {
|
|
@@ -50,7 +50,7 @@ const SIMULATOR_SURFACE_MODEL = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export function normalizePhysicsMode(value) {
|
|
53
|
-
return value === '
|
|
53
|
+
return value === 'advanced' ? 'advanced' : DEFAULT_PHYSICS_MODE;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function accelerationLimit(speed, surfaceGrip) {
|
|
@@ -111,7 +111,7 @@ function setPhysicsTelemetry(car, {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function integrateVehiclePhysics(car, controls, dt, options = {}) {
|
|
114
|
-
if (normalizePhysicsMode(options.physicsMode) === '
|
|
114
|
+
if (normalizePhysicsMode(options.physicsMode) === 'advanced') {
|
|
115
115
|
return integrateSimulatorVehiclePhysics(car, controls, dt, options);
|
|
116
116
|
}
|
|
117
117
|
return integrateArcadeVehiclePhysics(car, controls, dt, options);
|
|
@@ -305,12 +305,13 @@ function integrateSimulatorVehiclePhysics(car, controls, dt, options = {}) {
|
|
|
305
305
|
? clamp(1 - (gripUsage - 1) * 0.55 - longitudinalUsage * 0.18, 0.12, 1)
|
|
306
306
|
: 1;
|
|
307
307
|
const brakingDirection = forwardSpeedMps >= -0.2 ? 1 : -1;
|
|
308
|
+
const scrubDirection = forwardSpeedMps >= 0 ? 1 : -1;
|
|
308
309
|
const longitudinalAcceleration =
|
|
309
310
|
driveAcceleration * tractionPowerScale -
|
|
310
311
|
brakeDeceleration * brakingDirection -
|
|
311
312
|
dragAcceleration * Math.sign(forwardSpeedMps || 1) -
|
|
312
313
|
rollingAcceleration * Math.sign(forwardSpeedMps || 1) -
|
|
313
|
-
steeringScrubAcceleration;
|
|
314
|
+
steeringScrubAcceleration * scrubDirection;
|
|
314
315
|
|
|
315
316
|
const requestedWheelDragYawRate = wheelDragYawRate(car, SIMULATOR_SURFACE_MODEL) * (surfaceName === 'kerb' ? 1.6 : 1);
|
|
316
317
|
const maxYawRate = speedBeforeMps <= 0.05 ? 0 : totalGripAcceleration / Math.max(speedBeforeMps, 0.1);
|
|
@@ -16,6 +16,11 @@ function serializeAppliedControls(car, dependencies) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function serializeVelocityComponent(car, component, dependencies) {
|
|
20
|
+
if (dependencies.physicsMode !== 'advanced') return null;
|
|
21
|
+
return finiteOrNull(car[component]);
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export function serializeWheels(wheels = []) {
|
|
20
25
|
return wheels.map((wheel) => ({
|
|
21
26
|
id: wheel.id,
|
|
@@ -99,6 +104,8 @@ export function serializeCar(car, rank, penaltySeconds = 0, dependencies) {
|
|
|
99
104
|
heading: car.heading,
|
|
100
105
|
steeringAngle: car.steeringAngle,
|
|
101
106
|
yawRate: car.yawRate,
|
|
107
|
+
velocityX: serializeVelocityComponent(car, 'velocityX', dependencies),
|
|
108
|
+
velocityY: serializeVelocityComponent(car, 'velocityY', dependencies),
|
|
102
109
|
turnRadius: car.turnRadius,
|
|
103
110
|
speed: car.speed,
|
|
104
111
|
speedKph: simSpeedToKph(car.speed),
|
|
@@ -134,6 +141,17 @@ export function serializeCar(car, rank, penaltySeconds = 0, dependencies) {
|
|
|
134
141
|
signedOffset: car.trackState?.signedOffset ?? 0,
|
|
135
142
|
crossTrackError: car.trackState?.crossTrackError ?? 0,
|
|
136
143
|
surface: car.trackState?.surface ?? 'track',
|
|
144
|
+
trackState: car.trackState ? {
|
|
145
|
+
distance: car.trackState.distance,
|
|
146
|
+
signedOffset: car.trackState.signedOffset,
|
|
147
|
+
crossTrackError: car.trackState.crossTrackError,
|
|
148
|
+
surface: car.trackState.surface,
|
|
149
|
+
inPitLane: Boolean(car.trackState.inPitLane),
|
|
150
|
+
pitLanePart: car.trackState.pitLanePart ?? null,
|
|
151
|
+
pitBoxId: car.trackState.pitBoxId ?? null,
|
|
152
|
+
curvature: car.trackState.curvature ?? 0,
|
|
153
|
+
heading: car.trackState.heading,
|
|
154
|
+
} : null,
|
|
137
155
|
wheels: serializeWheels(car.wheelStates),
|
|
138
156
|
inPitLane: Boolean(car.trackState?.inPitLane),
|
|
139
157
|
pitLanePart: car.trackState?.pitLanePart ?? null,
|
|
@@ -193,6 +211,8 @@ export function serializeObservationCar(car, rank, dependencies) {
|
|
|
193
211
|
heading: car.heading,
|
|
194
212
|
steeringAngle: car.steeringAngle,
|
|
195
213
|
yawRate: car.yawRate,
|
|
214
|
+
velocityX: serializeVelocityComponent(car, 'velocityX', dependencies),
|
|
215
|
+
velocityY: serializeVelocityComponent(car, 'velocityY', dependencies),
|
|
196
216
|
speed: car.speed,
|
|
197
217
|
speedKph: simSpeedToKph(car.speed),
|
|
198
218
|
throttle: car.throttle,
|
|
@@ -261,6 +281,8 @@ export function serializeTrainingCar(car, rank, dependencies) {
|
|
|
261
281
|
heading: car.heading,
|
|
262
282
|
steeringAngle: car.steeringAngle,
|
|
263
283
|
yawRate: car.yawRate,
|
|
284
|
+
velocityX: serializeVelocityComponent(car, 'velocityX', dependencies),
|
|
285
|
+
velocityY: serializeVelocityComponent(car, 'velocityY', dependencies),
|
|
264
286
|
speed: car.speed,
|
|
265
287
|
speedKph: simSpeedToKph(car.speed),
|
|
266
288
|
throttle: car.throttle,
|
|
@@ -121,6 +121,7 @@ export function applyExternalCarState(car, partial, context) {
|
|
|
121
121
|
cars,
|
|
122
122
|
computeLap,
|
|
123
123
|
nearestDistanceOnRoute,
|
|
124
|
+
physicsMode,
|
|
124
125
|
progressDelta,
|
|
125
126
|
raceControl,
|
|
126
127
|
releaseRaceStart,
|
|
@@ -137,6 +138,7 @@ export function applyExternalCarState(car, partial, context) {
|
|
|
137
138
|
if (partial.destroyed === false) nextPartial.destroyed = car.destroyed;
|
|
138
139
|
if (partial.outOfRace === false) nextPartial.outOfRace = car.outOfRace;
|
|
139
140
|
}
|
|
141
|
+
assertFiniteExternalCarState(nextPartial);
|
|
140
142
|
Object.assign(car, nextPartial);
|
|
141
143
|
if (
|
|
142
144
|
!raceControl.finished &&
|
|
@@ -160,8 +162,9 @@ export function applyExternalCarState(car, partial, context) {
|
|
|
160
162
|
releaseRaceStart();
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
|
-
car.speed = clamp(car.speed, 0, VEHICLE_LIMITS.maxSpeed);
|
|
164
|
-
car.heading = normalizeAngle(car.heading);
|
|
165
|
+
car.speed = clamp(Number.isFinite(car.speed) ? car.speed : 0, 0, VEHICLE_LIMITS.maxSpeed);
|
|
166
|
+
car.heading = normalizeAngle(Number.isFinite(car.heading) ? car.heading : 0);
|
|
167
|
+
syncExternalVelocityState(car, nextPartial, physicsMode === 'advanced');
|
|
165
168
|
const centerState = nearestTrackStateForCar(track, car, car, car.progress ?? car.raceDistance);
|
|
166
169
|
applyWheelSurfaceState(car, track, { centerState });
|
|
167
170
|
if (
|
|
@@ -184,3 +187,34 @@ export function applyExternalCarState(car, partial, context) {
|
|
|
184
187
|
resetTimingLineCrossings(car, time);
|
|
185
188
|
resetLapTelemetry(car, time, track, totalLaps);
|
|
186
189
|
}
|
|
190
|
+
|
|
191
|
+
function assertFiniteExternalCarState(partial) {
|
|
192
|
+
Object.entries(partial ?? {}).forEach(([key, value]) => {
|
|
193
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
194
|
+
throw new Error(`External car state ${key} must be a finite number.`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function syncExternalVelocityState(car, partial, simulatorMode = false) {
|
|
200
|
+
const hasVelocityX = Object.hasOwn(partial, 'velocityX');
|
|
201
|
+
const hasVelocityY = Object.hasOwn(partial, 'velocityY');
|
|
202
|
+
const hasVelocity = hasVelocityX || hasVelocityY;
|
|
203
|
+
if (hasVelocity && (partial.velocityX == null || partial.velocityY == null)) {
|
|
204
|
+
car.velocityX = null;
|
|
205
|
+
car.velocityY = null;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (hasVelocity) {
|
|
209
|
+
const fallbackX = Math.cos(car.heading) * car.speed;
|
|
210
|
+
const fallbackY = Math.sin(car.heading) * car.speed;
|
|
211
|
+
car.velocityX = Number.isFinite(car.velocityX) ? car.velocityX : fallbackX;
|
|
212
|
+
car.velocityY = Number.isFinite(car.velocityY) ? car.velocityY : fallbackY;
|
|
213
|
+
car.speed = clamp(Math.hypot(car.velocityX, car.velocityY), 0, VEHICLE_LIMITS.maxSpeed);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (simulatorMode && (Object.hasOwn(partial, 'speed') || Object.hasOwn(partial, 'heading'))) {
|
|
217
|
+
car.velocityX = Math.cos(car.heading) * car.speed;
|
|
218
|
+
car.velocityY = Math.sin(car.heading) * car.speed;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -121,12 +121,14 @@ export function applyWheelSurfaceState(car, track, options = {}) {
|
|
|
121
121
|
track,
|
|
122
122
|
geometrySignature: geometry.signature,
|
|
123
123
|
centerSignature: centerStateSignature(options.centerState),
|
|
124
|
+
pitOverrideAllowed: pitOverrideAllowedForCar(car),
|
|
124
125
|
};
|
|
125
126
|
const cached = car.wheelSurfaceCache;
|
|
126
127
|
if (
|
|
127
128
|
cached?.track === cacheKey.track &&
|
|
128
129
|
cached.geometrySignature === cacheKey.geometrySignature &&
|
|
129
|
-
cached.centerSignature === cacheKey.centerSignature
|
|
130
|
+
cached.centerSignature === cacheKey.centerSignature &&
|
|
131
|
+
cached.pitOverrideAllowed === cacheKey.pitOverrideAllowed
|
|
130
132
|
) {
|
|
131
133
|
car.wheelStates = cached.result.wheels;
|
|
132
134
|
car.trackLimitState = cached.result.trackLimits;
|
|
@@ -116,7 +116,6 @@ function createWarmupFingerprint(options, { surface, steps }) {
|
|
|
116
116
|
trackGeneration: options.trackGeneration ?? null,
|
|
117
117
|
track: summarizeTrack(options.track),
|
|
118
118
|
physicsMode: options.physicsMode ?? null,
|
|
119
|
-
trackQueryIndex: options.trackQueryIndex !== false,
|
|
120
119
|
rules: options.rules ?? null,
|
|
121
120
|
participantInteractions: options.participantInteractions ?? null,
|
|
122
121
|
replayGhosts: summarizeReplayGhosts(options.replayGhosts),
|