@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +92 -17
  4. package/docs/architecture.md +26 -16
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +7 -5
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +121 -33
  9. package/docs/data_model.md +1 -1
  10. package/docs/index.md +10 -0
  11. package/docs/learnings.md +6 -6
  12. package/docs/rules.md +39 -9
  13. package/docs/sense_contract.md +59 -0
  14. package/docs/system_specs.md +76 -20
  15. package/docs/training.md +27 -20
  16. package/package.json +1 -1
  17. package/src/api/PaddockSimulatorController.js +75 -36
  18. package/src/app/BrowserExpertAdapter.js +9 -300
  19. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  20. package/src/app/F1SimulatorApp.js +190 -68
  21. package/src/app/banners/raceDataBannerController.js +71 -36
  22. package/src/app/banners/stewardMessageController.js +5 -4
  23. package/src/app/camera/cameraConstants.js +2 -0
  24. package/src/app/camera/cameraController.js +86 -29
  25. package/src/app/camera/cameraGeometry.js +12 -1
  26. package/src/app/domBindings.js +89 -9
  27. package/src/app/layoutScheduler.js +27 -0
  28. package/src/app/layoutSupport.js +165 -0
  29. package/src/app/raceOverlayClearanceSupport.js +158 -0
  30. package/src/app/readouts/carOverviewRenderer.js +16 -8
  31. package/src/app/readouts/controlStateRenderer.js +1 -1
  32. package/src/app/readouts/raceStatusRenderer.js +30 -21
  33. package/src/app/readouts/readoutFormatters.js +2 -0
  34. package/src/app/readouts/telemetryRenderer.js +17 -8
  35. package/src/app/readouts/timingTowerRenderer.js +62 -10
  36. package/src/app/runtime/runtimeVisibility.js +66 -5
  37. package/src/config/cssValues.js +16 -0
  38. package/src/config/defaultOptions.js +51 -38
  39. package/src/config/restartOptions.js +34 -0
  40. package/src/config/themeOptions.js +825 -0
  41. package/src/config/timingGapMode.js +13 -0
  42. package/src/config/urlOptions.js +25 -0
  43. package/src/data/championship.js +14 -9
  44. package/src/environment/controllerLoop.js +34 -5
  45. package/src/environment/episode.js +14 -2
  46. package/src/environment/index.d.ts +17 -4
  47. package/src/environment/observationVector.js +200 -0
  48. package/src/environment/observations.js +58 -285
  49. package/src/environment/options.js +1 -1
  50. package/src/environment/recorder.js +13 -6
  51. package/src/environment/rewards.js +13 -2
  52. package/src/environment/runtime.js +65 -25
  53. package/src/environment/scenarios.js +22 -3
  54. package/src/environment/sensors/index.js +3 -3
  55. package/src/environment/sensors/opponentRadar.js +13 -3
  56. package/src/environment/sensors/sensorTargets.js +8 -2
  57. package/src/environment/sensors/surfaceRays.js +84 -12
  58. package/src/environment/sensors/trackRays.js +173 -15
  59. package/src/environment/specs.js +78 -40
  60. package/src/index.d.ts +112 -9
  61. package/src/index.js +26 -33
  62. package/src/simulation/driver/driverController.js +1 -1
  63. package/src/simulation/driver/edgeRecovery.js +1 -1
  64. package/src/simulation/driver/racingControls.js +2 -2
  65. package/src/simulation/driver/rejoinControls.js +2 -2
  66. package/src/simulation/pit/pitFlow.js +5 -0
  67. package/src/simulation/pit/pitKinematics.js +24 -0
  68. package/src/simulation/pit/pitQueue.js +5 -0
  69. package/src/simulation/pit/pitRouteMovement.js +7 -0
  70. package/src/simulation/pit/pitRouting.js +2 -4
  71. package/src/simulation/pit/pitService.js +2 -0
  72. package/src/simulation/pit/pitTireService.js +2 -0
  73. package/src/simulation/race/raceLifecycle.js +2 -6
  74. package/src/simulation/race/raceProgress.js +6 -1
  75. package/src/simulation/race/raceSetup.js +2 -6
  76. package/src/simulation/race/raceStep.js +2 -2
  77. package/src/simulation/race/redFlag.js +3 -3
  78. package/src/simulation/race/safetyCar.js +10 -4
  79. package/src/simulation/race/stalledDnf.js +8 -17
  80. package/src/simulation/raceSimulation.js +1 -0
  81. package/src/simulation/rules/rulesReview.js +3 -0
  82. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  83. package/src/simulation/timing/lapTelemetry.js +28 -9
  84. package/src/simulation/track/pitLaneLayout.js +7 -3
  85. package/src/simulation/track/spatialQueries.js +5 -28
  86. package/src/simulation/track/trackQueryGrid.js +94 -0
  87. package/src/simulation/track/trackQueryIndex.js +193 -434
  88. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  89. package/src/simulation/track/trackQueryScratch.js +46 -0
  90. package/src/simulation/track/trackQueryStats.js +28 -0
  91. package/src/simulation/track/trackStatePolicy.js +0 -1
  92. package/src/simulation/vehicle/contactResolution.js +67 -2
  93. package/src/simulation/vehicle/runoffResponse.js +11 -12
  94. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  95. package/src/simulation/vehicle/vehiclePhysics.js +5 -4
  96. package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
  97. package/src/simulation/vehicle/vehicleState.js +36 -2
  98. package/src/simulation/vehicle/wheelSurface.js +3 -1
  99. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  100. package/src/styles.css +1185 -136
  101. package/src/ui/bannerTemplates.js +38 -28
  102. package/src/ui/cameraControlsTemplate.js +16 -5
  103. package/src/ui/carOverviewTemplate.js +12 -5
  104. package/src/ui/raceCanvasTemplate.js +31 -8
  105. package/src/ui/raceControlsTemplate.js +27 -10
  106. package/src/ui/shellTemplate.js +3 -1
  107. package/src/ui/telemetryDrawerTemplate.js +23 -10
  108. package/src/ui/telemetryTemplates.js +58 -32
  109. package/src/ui/templateUtils.js +48 -0
  110. 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) first.speed = clamp(first.speed * 0.985, 0, VEHICLE_LIMITS.maxSpeed);
61
- if (!secondPitControlled) second.speed = clamp(second.speed * 0.985, 0, VEHICLE_LIMITS.maxSpeed);
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.speed = 0;
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.speed = 0;
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', 'simulator'];
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 === 'simulator' ? 'simulator' : DEFAULT_PHYSICS_MODE;
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) === 'simulator') {
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),