@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
@@ -12,7 +12,7 @@ function getSectorLength(track) {
12
12
  return track.length / TELEMETRY_SECTOR_COUNT;
13
13
  }
14
14
 
15
- function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
15
+ function writeLapTelemetryPosition(target, track, raceDistance, totalLaps = Infinity) {
16
16
  const positiveDistance = Math.max(0, raceDistance ?? 0);
17
17
  const lapIndex = Math.min(
18
18
  Math.floor(positiveDistance / track.length),
@@ -25,41 +25,40 @@ function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
25
25
  const sectorIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
26
26
  const sectorStart = sectorIndex * sectorLength;
27
27
 
28
- return {
29
- completedLaps: Math.floor(positiveDistance / track.length),
30
- currentLap: Math.max(1, lapIndex + 1),
31
- currentSector: sectorIndex + 1,
32
- currentSectorProgress: clamp((lapProgress - sectorStart) / sectorLength, 0, 1),
33
- };
28
+ target.completedLaps = Math.floor(positiveDistance / track.length);
29
+ target.currentLap = Math.max(1, lapIndex + 1);
30
+ target.currentSector = sectorIndex + 1;
31
+ target.currentSectorProgress = clamp((lapProgress - sectorStart) / sectorLength, 0, 1);
32
+ return target;
34
33
  }
35
34
 
36
- function createSectorProgress(position, currentSectors = null) {
35
+ function createSectorProgress(position, currentSectors = null, { preserveCompletedSectors = false } = {}) {
37
36
  const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
38
- return createEmptySectorTimes().map((_, index) => {
39
- if (index < activeIndex && Number.isFinite(currentSectors?.[index])) return 1;
40
- if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
41
- return 0;
42
- });
37
+ const progress = createEmptySectorTimes();
38
+ writeSectorProgress(progress, position, currentSectors, { preserveCompletedSectors, activeIndex });
39
+ return progress;
43
40
  }
44
41
 
45
42
  export function createLapTelemetry(track, currentTime = 0, raceDistance = 0, totalLaps = Infinity) {
46
- const position = getLapTelemetryPosition(track, raceDistance, totalLaps);
47
- return {
48
- ...position,
43
+ const telemetry = {
49
44
  currentLapStartedAt: currentTime,
50
45
  currentSectorStartedAt: currentTime,
51
46
  lastUpdatedAt: currentTime,
52
47
  currentLapTime: 0,
53
48
  currentSectorElapsed: 0,
54
49
  currentSectors: createEmptySectorTimes(),
55
- sectorProgress: createSectorProgress(position),
50
+ sectorProgress: createEmptySectorTimes(),
56
51
  liveSectors: createEmptySectorTimes(),
57
52
  lastLapTime: null,
58
53
  bestLapTime: null,
59
54
  lastSectors: createEmptySectorTimes(),
60
55
  bestSectors: createEmptySectorTimes(),
61
56
  sectorPerformance: createEmptySectorPerformance(),
57
+ sectorPerformanceRevision: 0,
62
58
  };
59
+ writeLapTelemetryPosition(telemetry, track, raceDistance, totalLaps);
60
+ telemetry.sectorProgress = createSectorProgress(telemetry);
61
+ return telemetry;
63
62
  }
64
63
 
65
64
  export function resetLapTelemetry(car, currentTime, track, totalLaps) {
@@ -99,12 +98,25 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
99
98
  const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
100
99
  const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
101
100
 
102
- if (!Number.isFinite(previousDistance) || !Number.isFinite(currentDistance) || travelled < -1e-3 || travelled > track.length / 2) {
101
+ if (
102
+ !Number.isFinite(previousDistance) ||
103
+ !Number.isFinite(currentDistance) ||
104
+ Math.abs(travelled) > track.length / 2
105
+ ) {
103
106
  resetLapTelemetry(car, currentTime, track, totalLaps);
104
- return;
107
+ return true;
108
+ }
109
+
110
+ if (travelled < -1e-3) {
111
+ syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
112
+ preserveCompletedSectors: true,
113
+ });
114
+ telemetry.lastUpdatedAt = currentTime;
115
+ return false;
105
116
  }
106
117
 
107
118
  const sectorLength = getSectorLength(track);
119
+ let crossedBoundary = false;
108
120
  if (travelled > 1e-6) {
109
121
  const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
110
122
  const lastBoundary = Math.floor(currentDistance / sectorLength);
@@ -112,6 +124,7 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
112
124
  for (let boundary = firstBoundary; boundary <= lastBoundary; boundary += 1) {
113
125
  const boundaryDistance = boundary * sectorLength;
114
126
  if (boundaryDistance <= previousDistance + 1e-3 || boundaryDistance > currentDistance + 1e-3) continue;
127
+ crossedBoundary = true;
115
128
 
116
129
  const boundaryRatio = clamp((boundaryDistance - previousDistance) / travelled, 0, 1);
117
130
  const crossingTime = previousUpdateTime + boundaryRatio * elapsedTime;
@@ -127,8 +140,8 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
127
140
  telemetry.bestLapTime = Number.isFinite(telemetry.bestLapTime)
128
141
  ? Math.min(telemetry.bestLapTime, lapTime)
129
142
  : lapTime;
130
- telemetry.lastSectors = serializeSectorTimes(telemetry.currentSectors);
131
- telemetry.currentSectors = createEmptySectorTimes();
143
+ telemetry.lastSectors = copySectorTimes(telemetry.lastSectors, telemetry.currentSectors);
144
+ clearSectorTimes(telemetry.currentSectors);
132
145
  telemetry.currentLapStartedAt = crossingTime;
133
146
  telemetry.completedLaps = Math.max(telemetry.completedLaps + 1, Math.min(Math.floor(boundaryDistance / track.length), totalLaps));
134
147
  }
@@ -137,8 +150,18 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
137
150
  }
138
151
  }
139
152
 
153
+ if (!crossedBoundary) {
154
+ syncInProgressLapTelemetry(telemetry, currentTime, currentRaceDistance, track, totalLaps);
155
+ telemetry.lastUpdatedAt = currentTime;
156
+ return false;
157
+ }
158
+
140
159
  syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps);
141
160
  telemetry.lastUpdatedAt = currentTime;
161
+ if (crossedBoundary) {
162
+ telemetry.sectorPerformanceRevision = (telemetry.sectorPerformanceRevision ?? 0) + 1;
163
+ }
164
+ return crossedBoundary;
142
165
  }
143
166
 
144
167
  function updateBestSector(telemetry, sectorIndex, sectorTime) {
@@ -155,28 +178,103 @@ function clearFutureSectorTelemetry(telemetry) {
155
178
  }
156
179
  }
157
180
 
158
- function syncLiveSectorTelemetry(telemetry) {
181
+ function syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors = false } = {}) {
159
182
  const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
183
+ const liveSectors = ensureSectorTimesArray(telemetry.liveSectors);
184
+ clearSectorTimes(liveSectors);
185
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
186
+ const time = telemetry.currentSectors[index];
187
+ if (!preserveCompletedSectors && index >= activeIndex) continue;
188
+ if (!Number.isFinite(time)) continue;
189
+ liveSectors[index] = time;
190
+ }
191
+ if (!Number.isFinite(liveSectors[activeIndex])) {
192
+ liveSectors[activeIndex] = telemetry.currentSectorElapsed;
193
+ }
194
+ telemetry.liveSectors = liveSectors;
195
+ }
160
196
 
161
- telemetry.liveSectors = createEmptySectorTimes();
162
- telemetry.currentSectors.forEach((time, index) => {
163
- if (index >= activeIndex) return;
164
- if (!Number.isFinite(time)) return;
165
- telemetry.liveSectors[index] = time;
197
+ function syncInProgressLapTelemetry(telemetry, currentTime, currentRaceDistance, track, totalLaps) {
198
+ const positiveDistance = Math.max(0, currentRaceDistance ?? 0);
199
+ const maxLapIndex = Math.max(0, totalLaps - 1);
200
+ const lapIndex = Math.min(Math.floor(positiveDistance / track.length), maxLapIndex);
201
+ const lapProgress = positiveDistance >= track.length * totalLaps
202
+ ? track.length
203
+ : positiveDistance - lapIndex * track.length;
204
+ const sectorLength = getSectorLength(track);
205
+ const activeIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
206
+ const sectorStart = activeIndex * sectorLength;
207
+ const sectorProgress = ensureSectorTimesArray(telemetry.sectorProgress);
208
+ const liveSectors = ensureSectorTimesArray(telemetry.liveSectors);
209
+
210
+ telemetry.completedLaps = Math.floor(positiveDistance / track.length);
211
+ telemetry.currentLap = Math.max(1, lapIndex + 1);
212
+ telemetry.currentSector = activeIndex + 1;
213
+ telemetry.currentSectorProgress = clamp((lapProgress - sectorStart) / sectorLength, 0, 1);
214
+ telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
215
+ telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
216
+ for (let index = activeIndex; index < TELEMETRY_SECTOR_COUNT; index += 1) {
217
+ telemetry.currentSectors[index] = null;
218
+ liveSectors[index] = null;
219
+ sectorProgress[index] = 0;
220
+ }
221
+ sectorProgress[activeIndex] = telemetry.currentSectorProgress;
222
+ liveSectors[activeIndex] = telemetry.currentSectorElapsed;
223
+ telemetry.sectorProgress = sectorProgress;
224
+ telemetry.liveSectors = liveSectors;
225
+ }
226
+
227
+ function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
228
+ preserveCompletedSectors = false,
229
+ } = {}) {
230
+ writeLapTelemetryPosition(telemetry, track, currentRaceDistance, totalLaps);
231
+ if (!preserveCompletedSectors) clearFutureSectorTelemetry(telemetry);
232
+ const sectorProgress = ensureSectorTimesArray(telemetry.sectorProgress);
233
+ writeSectorProgress(sectorProgress, telemetry, telemetry.currentSectors, {
234
+ preserveCompletedSectors,
166
235
  });
167
- telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
168
- }
169
-
170
- function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps) {
171
- const position = getLapTelemetryPosition(track, currentRaceDistance, totalLaps);
172
- telemetry.completedLaps = position.completedLaps;
173
- telemetry.currentLap = position.currentLap;
174
- telemetry.currentSector = position.currentSector;
175
- telemetry.currentSectorProgress = position.currentSectorProgress;
176
- clearFutureSectorTelemetry(telemetry);
177
- telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors);
178
- telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
236
+ telemetry.sectorProgress = sectorProgress;
237
+ telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(telemetry.completedLaps, totalLaps));
179
238
  telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
180
239
  telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
181
- syncLiveSectorTelemetry(telemetry);
240
+ syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors });
241
+ }
242
+
243
+ function ensureSectorTimesArray(values) {
244
+ return Array.isArray(values) && values.length === TELEMETRY_SECTOR_COUNT
245
+ ? values
246
+ : createEmptySectorTimes();
247
+ }
248
+
249
+ function clearSectorTimes(values) {
250
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
251
+ values[index] = null;
252
+ }
253
+ }
254
+
255
+ function copySectorTimes(target, source) {
256
+ const nextTarget = ensureSectorTimesArray(target);
257
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
258
+ nextTarget[index] = finiteOrNull(source?.[index]);
259
+ }
260
+ return nextTarget;
261
+ }
262
+
263
+ function writeSectorProgress(target, position, currentSectors = null, { preserveCompletedSectors = false, activeIndex = null } = {}) {
264
+ const resolvedActiveIndex = activeIndex == null
265
+ ? clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1)
266
+ : activeIndex;
267
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
268
+ if (preserveCompletedSectors && Number.isFinite(currentSectors?.[index])) {
269
+ target[index] = 1;
270
+ continue;
271
+ }
272
+ if (index < resolvedActiveIndex && Number.isFinite(currentSectors?.[index])) {
273
+ target[index] = 1;
274
+ continue;
275
+ }
276
+ target[index] = index === resolvedActiveIndex
277
+ ? clamp(position.currentSectorProgress ?? 0, 0, 1)
278
+ : 0;
279
+ }
182
280
  }
@@ -5,7 +5,11 @@ export {
5
5
  updateLapTelemetry,
6
6
  } from './lapTelemetry.js';
7
7
  export { estimateGapAheadSeconds, estimateTimingLineGapSeconds, wholeLapGap } from './gapEstimation.js';
8
- export { createEmptySectorPerformance, createEmptySectorTimes, updateSectorPerformance } from './sectorPerformance.js';
8
+ export {
9
+ createEmptySectorPerformance,
10
+ createEmptySectorTimes,
11
+ updateSectorPerformance,
12
+ } from './sectorPerformance.js';
9
13
  export { recordTimingSample, resetTimingHistory, trimTimingHistory } from './timingHistory.js';
10
14
  export {
11
15
  createTimingLines,
@@ -1,5 +1,9 @@
1
1
  import { TELEMETRY_SECTOR_COUNT } from './timingConstants.js';
2
2
 
3
+ const OVERALL_BEST_0 = '_sectorPerformanceOverallBest0';
4
+ const OVERALL_BEST_1 = '_sectorPerformanceOverallBest1';
5
+ const OVERALL_BEST_2 = '_sectorPerformanceOverallBest2';
6
+
3
7
  export function createEmptySectorTimes() {
4
8
  return Array.from({ length: TELEMETRY_SECTOR_COUNT }, () => null);
5
9
  }
@@ -20,32 +24,80 @@ export function serializeSectorPerformance(values) {
20
24
  return createEmptySectorTimes().map((_, index) => values?.[index] ?? null);
21
25
  }
22
26
 
23
- export function updateSectorPerformance(cars) {
24
- const overallBestSectors = createEmptySectorTimes();
25
- cars.forEach((car) => {
26
- car.lapTelemetry?.bestSectors?.forEach((value, index) => {
27
- if (!Number.isFinite(value)) return;
28
- const previousBest = overallBestSectors[index];
29
- if (!Number.isFinite(previousBest) || value < previousBest) overallBestSectors[index] = value;
30
- });
31
- });
32
- cars.forEach((car) => {
33
- const telemetry = car.lapTelemetry;
34
- if (!telemetry) return;
35
- telemetry.sectorPerformance = {
36
- current: telemetry.currentSectors.map((value, index) => classifySectorPerformance(
37
- value,
38
- telemetry.bestSectors[index],
39
- overallBestSectors[index],
40
- )),
41
- last: telemetry.lastSectors.map((value, index) => classifySectorPerformance(
42
- value,
43
- telemetry.bestSectors[index],
44
- overallBestSectors[index],
45
- )),
46
- best: telemetry.bestSectors.map((value, index) => classifySectorPerformance(value, value, overallBestSectors[index])),
47
- };
48
- });
27
+ export function updateSectorPerformance(cars, stats = null) {
28
+ let overallBest0 = null;
29
+ let overallBest1 = null;
30
+ let overallBest2 = null;
31
+ let changedCars = 0;
32
+
33
+ for (let index = 0; index < cars.length; index += 1) {
34
+ const telemetry = cars[index]?.lapTelemetry;
35
+ if (!telemetry) continue;
36
+ const best0 = finiteOrNull(telemetry.bestSectors?.[0]);
37
+ const best1 = finiteOrNull(telemetry.bestSectors?.[1]);
38
+ const best2 = finiteOrNull(telemetry.bestSectors?.[2]);
39
+ const revision = telemetry.sectorPerformanceRevision ?? 0;
40
+ if (telemetry._sectorPerfAppliedRevision !== revision) changedCars += 1;
41
+ if (Number.isFinite(best0) && (!Number.isFinite(overallBest0) || best0 < overallBest0)) overallBest0 = best0;
42
+ if (Number.isFinite(best1) && (!Number.isFinite(overallBest1) || best1 < overallBest1)) overallBest1 = best1;
43
+ if (Number.isFinite(best2) && (!Number.isFinite(overallBest2) || best2 < overallBest2)) overallBest2 = best2;
44
+ }
45
+
46
+ const overallBestChanged =
47
+ overallBest0 !== cars[OVERALL_BEST_0] ||
48
+ overallBest1 !== cars[OVERALL_BEST_1] ||
49
+ overallBest2 !== cars[OVERALL_BEST_2];
50
+
51
+ if (stats) {
52
+ stats.sectorPerformanceChangedCars = (stats.sectorPerformanceChangedCars ?? 0) + changedCars;
53
+ }
54
+
55
+ if (!overallBestChanged && changedCars === 0) {
56
+ if (stats) stats.sectorPerformanceSkippedRebuilds = (stats.sectorPerformanceSkippedRebuilds ?? 0) + 1;
57
+ return false;
58
+ }
59
+
60
+ cars[OVERALL_BEST_0] = overallBest0;
61
+ cars[OVERALL_BEST_1] = overallBest1;
62
+ cars[OVERALL_BEST_2] = overallBest2;
63
+
64
+ let updatedCars = 0;
65
+ const overallBestSectors = [overallBest0, overallBest1, overallBest2];
66
+ for (let index = 0; index < cars.length; index += 1) {
67
+ const telemetry = cars[index]?.lapTelemetry;
68
+ if (!telemetry) continue;
69
+ const revision = telemetry.sectorPerformanceRevision ?? 0;
70
+ if (!overallBestChanged && telemetry._sectorPerfAppliedRevision === revision) continue;
71
+ const sectorPerformance = ensureSectorPerformanceShape(telemetry.sectorPerformance);
72
+ writeSectorPerformance(
73
+ sectorPerformance.current,
74
+ telemetry.currentSectors,
75
+ telemetry.bestSectors,
76
+ overallBestSectors,
77
+ );
78
+ writeSectorPerformance(
79
+ sectorPerformance.last,
80
+ telemetry.lastSectors,
81
+ telemetry.bestSectors,
82
+ overallBestSectors,
83
+ );
84
+ writeSectorPerformance(
85
+ sectorPerformance.best,
86
+ telemetry.bestSectors,
87
+ telemetry.bestSectors,
88
+ overallBestSectors,
89
+ );
90
+ telemetry.sectorPerformance = sectorPerformance;
91
+ telemetry._sectorPerfAppliedRevision = revision;
92
+ updatedCars += 1;
93
+ }
94
+
95
+ if (stats) {
96
+ stats.sectorPerformanceRebuilds = (stats.sectorPerformanceRebuilds ?? 0) + 1;
97
+ stats.sectorPerformanceUpdatedCars = (stats.sectorPerformanceUpdatedCars ?? 0) + updatedCars;
98
+ if (overallBestChanged) stats.sectorPerformanceOverallBestChanges = (stats.sectorPerformanceOverallBestChanges ?? 0) + 1;
99
+ }
100
+ return true;
49
101
  }
50
102
 
51
103
  export function finiteOrNull(value) {
@@ -58,3 +110,29 @@ function classifySectorPerformance(value, personalBest, overallBest) {
58
110
  if (Number.isFinite(personalBest) && Math.abs(value - personalBest) <= 1e-6) return 'personal-best';
59
111
  return 'slower';
60
112
  }
113
+
114
+ function ensureSectorPerformanceShape(sectorPerformance) {
115
+ if (!sectorPerformance || typeof sectorPerformance !== 'object') {
116
+ return createEmptySectorPerformance();
117
+ }
118
+ sectorPerformance.current = ensureSectorArray(sectorPerformance.current);
119
+ sectorPerformance.last = ensureSectorArray(sectorPerformance.last);
120
+ sectorPerformance.best = ensureSectorArray(sectorPerformance.best);
121
+ return sectorPerformance;
122
+ }
123
+
124
+ function ensureSectorArray(values) {
125
+ return Array.isArray(values) && values.length === TELEMETRY_SECTOR_COUNT
126
+ ? values
127
+ : createEmptySectorTimes();
128
+ }
129
+
130
+ function writeSectorPerformance(target, sectorValues, bestSectors, overallBestSectors) {
131
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
132
+ target[index] = classifySectorPerformance(
133
+ sectorValues?.[index],
134
+ bestSectors?.[index],
135
+ overallBestSectors[index],
136
+ );
137
+ }
138
+ }