@inventure71/paddockjs 2.0.1 → 4.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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -1,35 +1,40 @@
1
1
  import {
2
- createVehicleGeometry,
3
- createVehicleGeometryState,
4
2
  getVehicleGeometryState,
5
- interpolateVehiclePose,
3
+ vehicleAxes,
6
4
  } from './vehicle/vehicleGeometry.js';
5
+ import { normalizeAngle } from './simMath.js';
7
6
 
8
7
  const DEFAULT_SWEEP_STEPS = 16;
9
8
  const BROADPHASE_PADDING = 0.001;
10
9
  const DEFAULT_DISTANCE_WINDOW = 150;
10
+ const COLLISION_PAIR_FIRST_INDEX = Symbol('collisionPairFirstIndex');
11
+ const COLLISION_PAIR_SECOND_INDEX = Symbol('collisionPairSecondIndex');
11
12
 
12
13
  function dot(a, b) {
13
14
  return a.x * b.x + a.y * b.y;
14
15
  }
15
16
 
16
- function projectShape(shape, axis) {
17
+ function projectShapeInto(target, shape, axis) {
17
18
  let min = Infinity;
18
19
  let max = -Infinity;
19
20
  for (let index = 0; index < shape.corners.length; index += 1) {
20
- const value = dot(shape.corners[index], axis);
21
+ const corner = shape.corners[index];
22
+ const value = corner.x * axis.x + corner.y * axis.y;
21
23
  if (value < min) min = value;
22
24
  if (value > max) max = value;
23
25
  }
24
- return {
25
- min,
26
- max,
27
- };
26
+ target.min = min;
27
+ target.max = max;
28
+ return target;
28
29
  }
29
30
 
30
- function overlapOnAxis(first, second, axis) {
31
- const a = projectShape(first, axis);
32
- const b = projectShape(second, axis);
31
+ function overlapOnAxis(first, second, axis, projectionScratch = null) {
32
+ const projections = projectionScratch ?? {
33
+ first: { min: Infinity, max: -Infinity },
34
+ second: { min: Infinity, max: -Infinity },
35
+ };
36
+ const a = projectShapeInto(projections.first, first, axis);
37
+ const b = projectShapeInto(projections.second, second, axis);
33
38
  return Math.min(a.max, b.max) - Math.max(a.min, b.min);
34
39
  }
35
40
 
@@ -37,58 +42,44 @@ function shapeContactType(firstShape, secondShape) {
37
42
  return `${firstShape.type}-${secondShape.type}`;
38
43
  }
39
44
 
40
- export function detectShapeCollision(firstShape, secondShape) {
41
- const axes = [
42
- firstShape.forward,
43
- firstShape.right,
44
- secondShape.forward,
45
- secondShape.right,
46
- ];
47
- let depth = Infinity;
48
- let axis = null;
49
-
50
- for (const candidate of axes) {
51
- const overlap = overlapOnAxis(firstShape, secondShape, candidate);
52
- if (overlap <= 0) return null;
53
- if (overlap < depth) {
54
- depth = overlap;
55
- axis = candidate;
56
- }
57
- }
45
+ export function detectShapeCollision(firstShape, secondShape, scratch = null) {
46
+ const projectionScratch = ensureProjectionScratch(scratch);
47
+ let axis = firstShape.forward;
48
+ let depth = overlapOnAxis(firstShape, secondShape, axis, projectionScratch);
49
+ if (depth <= 0) return null;
58
50
 
59
- const direction = {
60
- x: secondShape.center.x - firstShape.center.x,
61
- y: secondShape.center.y - firstShape.center.y,
62
- };
63
- if (dot(direction, axis) < 0) {
64
- axis = { x: -axis.x, y: -axis.y };
51
+ let overlap = overlapOnAxis(firstShape, secondShape, firstShape.right, projectionScratch);
52
+ if (overlap <= 0) return null;
53
+ if (overlap < depth) {
54
+ depth = overlap;
55
+ axis = firstShape.right;
65
56
  }
66
57
 
67
- return {
68
- axis,
69
- depth,
70
- firstShapeId: firstShape.id,
71
- secondShapeId: secondShape.id,
72
- contactType: shapeContactType(firstShape, secondShape),
73
- };
74
- }
58
+ overlap = overlapOnAxis(firstShape, secondShape, secondShape.forward, projectionScratch);
59
+ if (overlap <= 0) return null;
60
+ if (overlap < depth) {
61
+ depth = overlap;
62
+ axis = secondShape.forward;
63
+ }
75
64
 
76
- function collisionShapes(geometry) {
77
- return [geometry.body];
78
- }
65
+ overlap = overlapOnAxis(firstShape, secondShape, secondShape.right, projectionScratch);
66
+ if (overlap <= 0) return null;
67
+ if (overlap < depth) {
68
+ depth = overlap;
69
+ axis = secondShape.right;
70
+ }
79
71
 
80
- function detectGeometryCollision(firstGeometry, secondGeometry) {
81
- let best = null;
82
- collisionShapes(firstGeometry).forEach((firstShape) => {
83
- collisionShapes(secondGeometry).forEach((secondShape) => {
84
- const collision = detectShapeCollision(firstShape, secondShape);
85
- if (!collision) return;
86
- if (!best || collision.depth < best.depth - 1e-6) {
87
- best = collision;
88
- }
89
- });
90
- });
91
- return best;
72
+ const directionX = secondShape.center.x - firstShape.center.x;
73
+ const directionY = secondShape.center.y - firstShape.center.y;
74
+ const directionSign = directionX * axis.x + directionY * axis.y < 0 ? -1 : 1;
75
+ return writeShapeCollisionResult(
76
+ scratch ? (scratch.shapeCollisionResult ??= { axis: {} }) : { axis: {} },
77
+ firstShape,
78
+ secondShape,
79
+ axis.x * directionSign,
80
+ axis.y * directionSign,
81
+ depth,
82
+ );
92
83
  }
93
84
 
94
85
  function aabbsOverlap(first, second) {
@@ -104,34 +95,60 @@ function createSweptAabb(car) {
104
95
  return getVehicleGeometryState(car).sweptBodyAabb;
105
96
  }
106
97
 
107
- function withCollisionMetadata(collision, timeOfImpact, swept) {
108
- return {
109
- ...collision,
110
- timeOfImpact,
111
- swept: Boolean(swept),
112
- };
98
+ function writeShapeCollisionResult(target, firstShape, secondShape, axisX, axisY, depth) {
99
+ target.axis ??= {};
100
+ target.axis.x = axisX;
101
+ target.axis.y = axisY;
102
+ target.depth = depth;
103
+ target.firstShapeId = firstShape.id;
104
+ target.secondShapeId = secondShape.id;
105
+ target.contactType = shapeContactType(firstShape, secondShape);
106
+ return target;
107
+ }
108
+
109
+ function withCollisionMetadata(collision, timeOfImpact, swept, scratch) {
110
+ const target = scratch ? (scratch.vehicleCollisionResult ??= { axis: {} }) : { axis: {} };
111
+ target.axis ??= {};
112
+ target.axis.x = collision.axis.x;
113
+ target.axis.y = collision.axis.y;
114
+ target.depth = collision.depth;
115
+ target.firstShapeId = collision.firstShapeId;
116
+ target.secondShapeId = collision.secondShapeId;
117
+ target.contactType = collision.contactType;
118
+ target.timeOfImpact = timeOfImpact;
119
+ target.swept = Boolean(swept);
120
+ return target;
113
121
  }
114
122
 
115
- export function detectVehicleCollision(first, second, { sweepSteps = DEFAULT_SWEEP_STEPS } = {}) {
123
+ function detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch) {
116
124
  const firstState = getVehicleGeometryState(first);
117
125
  const secondState = getVehicleGeometryState(second);
118
- const current = detectGeometryCollision(firstState.current, secondState.current);
119
- if (current) return withCollisionMetadata(current, 1, false);
126
+ const current = detectShapeCollision(firstState.current.body, secondState.current.body, scratch);
127
+ if (current) return withCollisionMetadata(current, 1, false, scratch);
120
128
 
121
129
  if (!aabbsOverlap(createSweptAabb(first), createSweptAabb(second))) return null;
122
130
 
131
+ const sweepShapes = ensureSweepShapes(scratch, firstState.current.body, secondState.current.body);
123
132
  for (let step = 1; step < sweepSteps; step += 1) {
124
133
  const amount = step / sweepSteps;
125
- const collision = detectGeometryCollision(
126
- createVehicleGeometry(interpolateVehiclePose(first, amount)),
127
- createVehicleGeometry(interpolateVehiclePose(second, amount)),
134
+ const collision = detectShapeCollision(
135
+ writeInterpolatedBodyShape(sweepShapes.first, firstState, amount),
136
+ writeInterpolatedBodyShape(sweepShapes.second, secondState, amount),
137
+ scratch,
128
138
  );
129
- if (collision) return withCollisionMetadata(collision, amount, true);
139
+ if (collision) return withCollisionMetadata(collision, amount, true, scratch);
130
140
  }
131
141
 
132
142
  return null;
133
143
  }
134
144
 
145
+ export function detectVehicleCollision(first, second, {
146
+ sweepSteps = DEFAULT_SWEEP_STEPS,
147
+ scratch = null,
148
+ } = {}) {
149
+ return detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch);
150
+ }
151
+
135
152
  function normalizedDistance(car, trackLength) {
136
153
  if (!Number.isFinite(trackLength) || trackLength <= 0) return car.raceDistance;
137
154
  const distance = Number.isFinite(car.progress) ? car.progress : car.raceDistance;
@@ -147,34 +164,51 @@ function wrappedDistanceDelta(first, second, trackLength) {
147
164
  export function buildCollisionCandidatePairs(cars, {
148
165
  trackLength = null,
149
166
  distanceWindow = DEFAULT_DISTANCE_WINDOW,
167
+ scratch = null,
150
168
  } = {}) {
151
- const candidates = [];
152
- const candidateKeys = new Set();
153
- const withDistance = [];
154
- const withoutDistance = [];
155
- const carOrder = new Map(cars.map((car, index) => [car, index]));
156
-
157
- const addCandidate = (first, second) => {
158
- const firstIndex = carOrder.get(first);
159
- const secondIndex = carOrder.get(second);
160
- const key = firstIndex < secondIndex ? `${firstIndex}:${secondIndex}` : `${secondIndex}:${firstIndex}`;
161
- if (candidateKeys.has(key)) return;
162
- candidateKeys.add(key);
163
- candidates.push([first, second]);
169
+ const candidates = scratchArray(scratch, 'candidates');
170
+ const pairMarker = candidatePairMarker(scratch, cars.length);
171
+ const withDistance = scratchArray(scratch, 'withDistance');
172
+ const pairPool = scratchArray(scratch, 'pairPool', { clear: false });
173
+ const distanceEntryPool = scratchArray(scratch, 'distanceEntryPool', { clear: false });
174
+ const missingDistanceFlags = missingDistanceFlagScratch(scratch, cars.length);
175
+ const missingDistanceIndexes = scratchArray(scratch, 'missingDistanceIndexes');
176
+
177
+ const addCandidateByIndex = (firstIndex, secondIndex) => {
178
+ if (pairMarker.has(firstIndex, secondIndex)) return;
179
+ pairMarker.add(firstIndex, secondIndex);
180
+ const pair = pairPool[candidates.length] ?? [];
181
+ pair[0] = cars[firstIndex];
182
+ pair[1] = cars[secondIndex];
183
+ pair[COLLISION_PAIR_FIRST_INDEX] = firstIndex;
184
+ pair[COLLISION_PAIR_SECOND_INDEX] = secondIndex;
185
+ pairPool[candidates.length] = pair;
186
+ candidates.push(pair);
164
187
  };
165
188
 
166
- cars.forEach((car) => {
189
+ for (let index = 0; index < cars.length; index += 1) {
190
+ const car = cars[index];
191
+ missingDistanceFlags[index] = 0;
167
192
  const distance = normalizedDistance(car, trackLength);
168
- if (Number.isFinite(distance)) withDistance.push({ car, distance });
169
- else withoutDistance.push(car);
170
- });
193
+ if (Number.isFinite(distance)) {
194
+ const entry = distanceEntryPool[withDistance.length] ?? {};
195
+ entry.car = car;
196
+ entry.carIndex = index;
197
+ entry.distance = distance;
198
+ distanceEntryPool[withDistance.length] = entry;
199
+ withDistance.push(entry);
200
+ } else {
201
+ missingDistanceFlags[index] = 1;
202
+ missingDistanceIndexes.push(index);
203
+ }
204
+ }
171
205
 
172
206
  withDistance.sort((first, second) => first.distance - second.distance);
173
207
  for (let i = 0; i < withDistance.length; i += 1) {
174
208
  for (let j = i + 1; j < withDistance.length; j += 1) {
175
209
  const delta = withDistance[j].distance - withDistance[i].distance;
176
210
  if (delta > distanceWindow) break;
177
- addCandidate(withDistance[i].car, withDistance[j].car);
211
+ addCandidateByIndex(withDistance[i].carIndex, withDistance[j].carIndex);
178
212
  }
179
213
  }
180
214
 
@@ -184,17 +218,18 @@ export function buildCollisionCandidatePairs(cars, {
184
218
  const firstWrappedIndex = lowerBoundDistance(withDistance, threshold);
185
219
  for (let j = Math.max(firstWrappedIndex, i + 1); j < withDistance.length; j += 1) {
186
220
  if (wrappedDistanceDelta(withDistance[i].distance, withDistance[j].distance, trackLength) <= distanceWindow) {
187
- addCandidate(withDistance[i].car, withDistance[j].car);
221
+ addCandidateByIndex(withDistance[i].carIndex, withDistance[j].carIndex);
188
222
  }
189
223
  }
190
224
  }
191
225
  }
192
226
 
193
- const missingDistance = new Set(withoutDistance);
194
- for (let i = 0; i < cars.length; i += 1) {
195
- for (let j = i + 1; j < cars.length; j += 1) {
196
- if (missingDistance.has(cars[i]) || missingDistance.has(cars[j])) {
197
- addCandidate(cars[i], cars[j]);
227
+ if (missingDistanceIndexes.length > 0) {
228
+ for (let i = 0; i < cars.length; i += 1) {
229
+ for (let j = i + 1; j < cars.length; j += 1) {
230
+ if (missingDistanceFlags[i] || missingDistanceFlags[j]) {
231
+ addCandidateByIndex(i, j);
232
+ }
198
233
  }
199
234
  }
200
235
  }
@@ -202,6 +237,135 @@ export function buildCollisionCandidatePairs(cars, {
202
237
  return candidates;
203
238
  }
204
239
 
240
+ export function collisionCandidatePairFirstIndex(pair) {
241
+ return Number.isInteger(pair?.[COLLISION_PAIR_FIRST_INDEX])
242
+ ? pair[COLLISION_PAIR_FIRST_INDEX]
243
+ : -1;
244
+ }
245
+
246
+ export function collisionCandidatePairSecondIndex(pair) {
247
+ return Number.isInteger(pair?.[COLLISION_PAIR_SECOND_INDEX])
248
+ ? pair[COLLISION_PAIR_SECOND_INDEX]
249
+ : -1;
250
+ }
251
+
252
+ function scratchArray(scratch, key, { clear = true } = {}) {
253
+ const array = scratch ? (scratch[key] ??= []) : [];
254
+ if (clear) array.length = 0;
255
+ return array;
256
+ }
257
+
258
+ function candidatePairMarker(scratch, carCount) {
259
+ if (!scratch) return setBackedCandidatePairMarker(carCount);
260
+ const requiredLength = Math.max(1, carCount * carCount);
261
+ if (!(scratch.candidatePairMarks instanceof Uint32Array) || scratch.candidatePairMarks.length < requiredLength) {
262
+ scratch.candidatePairMarks = new Uint32Array(requiredLength);
263
+ }
264
+ scratch.candidatePairEpoch = ((scratch.candidatePairEpoch ?? 0) + 1) >>> 0;
265
+ if (scratch.candidatePairEpoch === 0) {
266
+ scratch.candidatePairMarks.fill(0);
267
+ scratch.candidatePairEpoch = 1;
268
+ }
269
+ const marks = scratch.candidatePairMarks;
270
+ const epoch = scratch.candidatePairEpoch;
271
+ return {
272
+ has(firstIndex, secondIndex) {
273
+ return marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] === epoch;
274
+ },
275
+ add(firstIndex, secondIndex) {
276
+ marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] = epoch;
277
+ },
278
+ };
279
+ }
280
+
281
+ function missingDistanceFlagScratch(scratch, carCount) {
282
+ if (!scratch) return new Uint8Array(Math.max(1, carCount));
283
+ if (!(scratch.missingDistanceFlags instanceof Uint8Array) || scratch.missingDistanceFlags.length < carCount) {
284
+ scratch.missingDistanceFlags = new Uint8Array(Math.max(1, carCount));
285
+ }
286
+ return scratch.missingDistanceFlags;
287
+ }
288
+
289
+ function setBackedCandidatePairMarker(carCount) {
290
+ const marks = new Uint32Array(Math.max(1, carCount * carCount));
291
+ return {
292
+ has(firstIndex, secondIndex) {
293
+ return marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] === 1;
294
+ },
295
+ add(firstIndex, secondIndex) {
296
+ marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] = 1;
297
+ },
298
+ };
299
+ }
300
+
301
+ function candidatePairMarkIndex(firstIndex, secondIndex, carCount) {
302
+ const low = firstIndex < secondIndex ? firstIndex : secondIndex;
303
+ const high = firstIndex < secondIndex ? secondIndex : firstIndex;
304
+ return low * carCount + high;
305
+ }
306
+
307
+ function ensureSweepShapes(scratch, firstBody, secondBody) {
308
+ const sweepShapes = scratch ? (scratch.sweepShapes ??= {}) : {};
309
+ sweepShapes.first ??= createSweepShape(firstBody);
310
+ sweepShapes.second ??= createSweepShape(secondBody);
311
+ return sweepShapes;
312
+ }
313
+
314
+ function ensureProjectionScratch(scratch) {
315
+ const projectionScratch = scratch ? (scratch.projectionScratch ??= {}) : {};
316
+ projectionScratch.first ??= { min: Infinity, max: -Infinity };
317
+ projectionScratch.second ??= { min: Infinity, max: -Infinity };
318
+ return projectionScratch;
319
+ }
320
+
321
+ function createSweepShape(body) {
322
+ return {
323
+ id: body.id,
324
+ type: body.type,
325
+ center: { x: body.center.x, y: body.center.y },
326
+ heading: body.heading,
327
+ length: body.length,
328
+ width: body.width,
329
+ halfLength: body.halfLength,
330
+ halfWidth: body.halfWidth,
331
+ forward: { x: body.forward.x, y: body.forward.y },
332
+ right: { x: body.right.x, y: body.right.y },
333
+ corners: Array.from({ length: 4 }, () => ({ x: 0, y: 0 })),
334
+ };
335
+ }
336
+
337
+ function writeInterpolatedBodyShape(target, geometryState, amount) {
338
+ const pose = geometryState.pose;
339
+ const headingDelta = normalizeAngle(pose.heading - pose.previousHeading);
340
+ const x = pose.previousX + (pose.x - pose.previousX) * amount;
341
+ const y = pose.previousY + (pose.y - pose.previousY) * amount;
342
+ const heading = normalizeAngle(pose.previousHeading + headingDelta * amount);
343
+ const axes = vehicleAxes(heading);
344
+ const halfLength = target.halfLength;
345
+ const halfWidth = target.halfWidth;
346
+
347
+ target.center.x = x;
348
+ target.center.y = y;
349
+ target.heading = heading;
350
+ target.forward.x = axes.forward.x;
351
+ target.forward.y = axes.forward.y;
352
+ target.right.x = axes.right.x;
353
+ target.right.y = axes.right.y;
354
+
355
+ writeOffsetCorner(target.corners[0], target.center, target.forward, target.right, halfLength, halfWidth);
356
+ writeOffsetCorner(target.corners[1], target.center, target.forward, target.right, halfLength, -halfWidth);
357
+ writeOffsetCorner(target.corners[2], target.center, target.forward, target.right, -halfLength, -halfWidth);
358
+ writeOffsetCorner(target.corners[3], target.center, target.forward, target.right, -halfLength, halfWidth);
359
+
360
+ return target;
361
+ }
362
+
363
+ function writeOffsetCorner(target, center, forward, right, longitudinal, lateral) {
364
+ target.x = center.x + forward.x * longitudinal + right.x * lateral;
365
+ target.y = center.y + forward.y * longitudinal + right.y * lateral;
366
+ return target;
367
+ }
368
+
205
369
  function lowerBoundDistance(entries, target) {
206
370
  let low = 0;
207
371
  let high = entries.length;
@@ -37,7 +37,7 @@ export function decideDriverControls({ car, orderIndex, race }) {
37
37
  }
38
38
 
39
39
  function shouldStabilizeSimulatorCar(car, race) {
40
- if (race.physicsMode !== 'simulator') return false;
40
+ if (race.physicsMode !== 'advanced') return false;
41
41
  const gripUsage = car.gripUsage ?? 0;
42
42
  const slipAngle = Math.abs(car.slipAngleRadians ?? 0);
43
43
  const edgeDistance = race.track.width / 2 - VEHICLE_LIMITS.carWidth * 2.25;
@@ -5,7 +5,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
5
5
  import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
6
6
 
7
7
  export function calculateTrackEdgeGuard(car, race) {
8
- const simulatorMode = race.physicsMode === 'simulator';
8
+ const simulatorMode = race.physicsMode === 'advanced';
9
9
  const trackLimit = race.track.width / 2;
10
10
  const halfCarWidth = VEHICLE_LIMITS.carWidth / 2;
11
11
  const wholeCarOutsideLimit = trackLimit + halfCarWidth;
@@ -10,7 +10,7 @@ import { calculateActualOverlapPenalty, calculatePlannedTrafficPenalty, planRaci
10
10
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
11
11
 
12
12
  export function decideRacingControls(car, orderIndex, race) {
13
- if (race.physicsMode === 'simulator') {
13
+ if (race.physicsMode === 'advanced') {
14
14
  return decideSimulatorRacingControls(car, orderIndex, race);
15
15
  }
16
16
  return decideArcadeRacingControls(car, orderIndex, race);
@@ -389,7 +389,7 @@ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeG
389
389
  if (turnDirection === 0) return 0;
390
390
 
391
391
  const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
392
- const simulatorMode = race.physicsMode === 'simulator';
392
+ const simulatorMode = race.physicsMode === 'advanced';
393
393
  const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
394
394
  (simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
395
395
  const apexOffset = turnDirection * safeEdge *
@@ -8,7 +8,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
8
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
9
9
 
10
10
  export function decideRejoinControls(car, race) {
11
- if (race.physicsMode === 'simulator') {
11
+ if (race.physicsMode === 'advanced') {
12
12
  return decideSimulatorRejoinControls(car, race);
13
13
  }
14
14
  return decideArcadeRejoinControls(car, race);
@@ -103,7 +103,7 @@ export function decideSimulatorRejoinControls(car, race) {
103
103
  }
104
104
 
105
105
  function decideRejoinControlsForMode(car, race, profile) {
106
- const simulatorMode = race.physicsMode === 'simulator';
106
+ const simulatorMode = race.physicsMode === 'advanced';
107
107
  const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
108
108
  const edgeMotion = analyzeTrackEdgeMotion(car, race);
109
109
  const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
@@ -78,9 +78,14 @@ export function canStartPitStop(sim, car) {
78
78
  const stop = car.pitStop;
79
79
  const pitStops = sim.rules.modules?.pitStops;
80
80
  if (!stop || !pitStops?.enabled) return false;
81
- const active = sim.cars.filter((candidate) => candidate !== car && isCarInActivePitStop(sim, candidate));
82
81
  const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
83
- if (active.length >= maxConcurrentPitLaneCars) return false;
82
+ let activeCount = 0;
83
+ for (let index = 0; index < sim.cars.length; index += 1) {
84
+ const candidate = sim.cars[index];
85
+ if (candidate === car || !isCarInActivePitStop(sim, candidate)) continue;
86
+ activeCount += 1;
87
+ if (activeCount >= maxConcurrentPitLaneCars) return false;
88
+ }
84
89
 
85
90
  if (!pitStops.doubleStacking && stop.teamId) {
86
91
  const box = getPitStopBox(sim, stop);
@@ -89,10 +94,13 @@ export function canStartPitStop(sim, car) {
89
94
 
90
95
  const minimumGap = Math.max(0, pitStops.minimumPitLaneGap ?? 0);
91
96
  const candidateDistance = car.raceDistance ?? 0;
92
- return active.every((candidate) => {
97
+ for (let index = 0; index < sim.cars.length; index += 1) {
98
+ const candidate = sim.cars[index];
99
+ if (candidate === car || !isCarInActivePitStop(sim, candidate)) continue;
93
100
  const gap = (candidate.raceDistance ?? 0) - candidateDistance;
94
- return gap < 0 || gap >= minimumGap;
95
- });
101
+ if (gap >= 0 && gap < minimumGap) return false;
102
+ }
103
+ return true;
96
104
  }
97
105
 
98
106
  export function shouldStartPitStop(sim, car) {
@@ -152,13 +160,17 @@ export function startPitStop(sim, car) {
152
160
  0,
153
161
  (hasServiceQueue ? queueDistanceAlongLane : stopDistanceAlongLane) - PIT_BOX_APPROACH_DISTANCE,
154
162
  );
155
- const route = createRoute([
156
- ...createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance),
157
- ...(pitLane.entry.roadCenterline ?? []).map((point) => routePoint(point)),
163
+ const routePoints = createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance);
164
+ const entryCenterline = pitLane.entry.roadCenterline ?? [];
165
+ for (let index = 0; index < entryCenterline.length; index += 1) {
166
+ routePoints.push(routePoint(entryCenterline[index]));
167
+ }
168
+ routePoints.push(
158
169
  routePoint(pitMainLanePointAt(pitLane, 0, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
159
170
  routePoint(pitMainLanePointAt(pitLane, boxApproachDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
160
171
  routePoint(serviceTarget, pitLane.mainLane.heading, { limiterActive: true }),
161
- ]);
172
+ );
173
+ const route = createRoute(routePoints);
162
174
  stop.status = 'entering';
163
175
  stop.phase = 'entry';
164
176
  stop.queueingForService = shouldStageForService;
@@ -1,7 +1,7 @@
1
- export function syncPitCarKinematics(car, { physicsMode = 'simulator' } = {}) {
1
+ export function syncPitCarKinematics(car, { physicsMode = 'arcade' } = {}) {
2
2
  const speed = Number.isFinite(car.speed) ? car.speed : 0;
3
3
  const heading = Number.isFinite(car.heading) ? car.heading : 0;
4
- if (physicsMode === 'simulator') {
4
+ if (physicsMode === 'advanced') {
5
5
  car.velocityX = Math.cos(heading) * speed;
6
6
  car.velocityY = Math.sin(heading) * speed;
7
7
  } else {
@@ -34,11 +34,13 @@ export function beginPitQueue(sim, car, box) {
34
34
  export function releasePitQueue(sim, car, box, { fromCurrent = false } = {}) {
35
35
  const stop = car.pitStop;
36
36
  if (!stop || !box?.center) return false;
37
- const route = createRoute([
38
- ...(fromCurrent ? [routePoint(car, car.heading, { limiterActive: true })] : []),
37
+ const routePoints = [];
38
+ if (fromCurrent) routePoints.push(routePoint(car, car.heading, { limiterActive: true }));
39
+ routePoints.push(
39
40
  routePoint(box.queuePoint, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
40
41
  routePoint(box.center, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
41
- ]);
42
+ );
43
+ const route = createRoute(routePoints);
42
44
  stop.status = 'entering';
43
45
  stop.phase = 'queue-release';
44
46
  stop.queueingForService = false;
@@ -6,7 +6,7 @@ import {
6
6
  distanceToNextLimiterSegment,
7
7
  nearestDistanceOnRoute,
8
8
  routeLimiterActiveAt,
9
- sampleRoute,
9
+ sampleRouteInto,
10
10
  } from './pitRouting.js';
11
11
  import { syncPitCarKinematics } from './pitKinematics.js';
12
12
  import { completePitPenaltyService } from './pitPenaltyService.js';
@@ -26,6 +26,11 @@ import {
26
26
  pointDistance,
27
27
  } from './pitServiceConstants.js';
28
28
 
29
+ function samplePitStopRoute(stop, route, distanceAlong) {
30
+ const target = stop.routeSample ?? (stop.routeSample = { x: 0, y: 0, heading: 0, limiterActive: false });
31
+ return sampleRouteInto(target, route, distanceAlong);
32
+ }
33
+
29
34
  export function applyPitRoutePosition(sim, car, delta) {
30
35
  const stop = car.pitStop;
31
36
  if (!stop?.route) return false;
@@ -33,7 +38,7 @@ export function applyPitRoutePosition(sim, car, delta) {
33
38
  const route = stop.route;
34
39
  if (stop.phase === 'queue-release') {
35
40
  const nextProgress = Math.min(route.length, (stop.routeProgress ?? 0) + PIT_QUEUE_RELEASE_SPEED * delta);
36
- const point = sampleRoute(route, nextProgress);
41
+ const point = samplePitStopRoute(stop, route, nextProgress);
37
42
  if (!point) return false;
38
43
  const previousHeading = car.heading;
39
44
  car.previousX = car.x;
@@ -63,7 +68,7 @@ export function applyPitRoutePosition(sim, car, delta) {
63
68
  return route.length - stop.routeProgress <= PIT_QUEUE_RELEASE_FINISH_DISTANCE;
64
69
  }
65
70
  stop.routeProgress = clamp(nearestDistanceOnRoute(route, car, stop.routeProgress ?? 0), 0, route.length);
66
- const routeEnd = route.points.at(-1);
71
+ const routeEnd = route.points[route.points.length - 1];
67
72
  const limiterActive = routeLimiterActiveAt(route, stop.routeProgress);
68
73
  let targetSpeed = limiterActive
69
74
  ? Math.min(speedLimit, VEHICLE_LIMITS.maxSpeed)
@@ -93,7 +98,7 @@ export function applyPitRoutePosition(sim, car, delta) {
93
98
  }
94
99
 
95
100
  const nextProgress = Math.min(route.length, stop.routeProgress + Math.max(0, targetSpeed) * delta);
96
- const point = sampleRoute(route, nextProgress);
101
+ const point = samplePitStopRoute(stop, route, nextProgress);
97
102
  if (!point) return false;
98
103
  const previousSpeed = car.speed;
99
104
  const previousHeading = car.heading;