@inventure71/paddockjs 4.1.0 → 4.3.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 (257) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/INSTALL_AND_UPDATE.md +31 -5
  3. package/README.md +34 -3
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +111 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +32 -16
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/getting-started.md +3 -0
  16. package/docs/grid_walk.md +80 -0
  17. package/docs/index.md +5 -2
  18. package/docs/keyboard_driving_fix.md +118 -0
  19. package/docs/limit_driving_2026-10-14.md +88 -0
  20. package/docs/loading.md +141 -0
  21. package/docs/maintainability_audit_2026-09-08.md +146 -0
  22. package/docs/racing_ai_improvements.md +145 -0
  23. package/docs/rules.md +43 -13
  24. package/docs/sense_contract.md +4 -1
  25. package/docs/system_specs.md +40 -12
  26. package/docs/testing-reliability-guide.md +67 -0
  27. package/docs/v11_improvements.md +108 -0
  28. package/package.json +23 -15
  29. package/src/api/MountedSimulatorComponents.js +101 -0
  30. package/src/api/PaddockSimulatorController.js +56 -81
  31. package/src/app/BrowserExpertAdapter.js +0 -6
  32. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  33. package/src/app/ExternalTrackSnapshot.js +50 -0
  34. package/src/app/F1SimulatorApp.js +242 -297
  35. package/src/app/HostLifecycleEmitter.js +93 -0
  36. package/src/app/banners/raceDataBannerController.js +9 -4
  37. package/src/app/camera/cameraConstants.js +1 -1
  38. package/src/app/camera/cameraController.js +10 -13
  39. package/src/app/camera/cameraGeometry.js +1 -1
  40. package/src/app/controls/SimulatorViewControls.js +45 -0
  41. package/src/app/controls/workbenchBindings.js +64 -0
  42. package/src/app/controls/workbenchDisclosure.js +41 -0
  43. package/src/app/domBindings.js +21 -13
  44. package/src/app/gridWalk/GridWalkController.js +180 -0
  45. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  46. package/src/app/gridWalk/gridWalkControls.js +24 -0
  47. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  48. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  49. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  50. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  51. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  52. package/src/app/gridWalk/gridWalkSession.js +50 -0
  53. package/src/app/layoutSupport.js +2 -0
  54. package/src/app/raceOverlayClearanceSupport.js +21 -7
  55. package/src/app/readouts/carOverviewRenderer.js +9 -4
  56. package/src/app/readouts/readoutFormatters.js +1 -8
  57. package/src/app/readouts/telemetryRenderer.js +1 -3
  58. package/src/app/readouts/timingTowerRenderer.js +48 -50
  59. package/src/app/rendering/appAssets.js +8 -10
  60. package/src/app/rendering/carRenderer.js +27 -79
  61. package/src/app/rendering/displayUtils.js +0 -6
  62. package/src/app/rendering/expertSensorRenderer.js +1 -1
  63. package/src/app/rendering/replayGhostRenderer.js +4 -1
  64. package/src/app/rendering/trackRenderer.js +2 -2
  65. package/src/app/runtime/frameLoop.js +18 -17
  66. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  67. package/src/app/runtime/runtimeTiming.js +4 -1
  68. package/src/app/runtime/runtimeVisibility.js +7 -2
  69. package/src/config/cssValues.js +1 -1
  70. package/src/config/defaultOptions.js +12 -173
  71. package/src/config/restartOptions.js +1 -0
  72. package/src/config/runtimeSeed.js +10 -0
  73. package/src/config/simulatorDefaults.js +106 -0
  74. package/src/config/telemetryModules.js +20 -0
  75. package/src/config/theme/cssTokens.js +85 -0
  76. package/src/config/theme/themeCss.js +72 -0
  77. package/src/config/theme/themeInputs.js +155 -0
  78. package/src/config/theme/themeResolver.js +172 -0
  79. package/src/config/theme/themeSelectors.js +69 -0
  80. package/src/config/theme/themeTokens.js +257 -0
  81. package/src/config/theme/themeUtils.js +15 -0
  82. package/src/config/themeOptions.js +14 -830
  83. package/src/config/timingGapMode.js +0 -2
  84. package/src/config/uiOptions.js +53 -0
  85. package/src/data/championship.js +16 -158
  86. package/src/data/championshipDefaults.js +87 -0
  87. package/src/data/driverData.js +10 -20
  88. package/src/data/entryIdentity.js +67 -0
  89. package/src/data/entryPresentation.js +23 -0
  90. package/src/data/entryTypes.d.ts +160 -0
  91. package/src/data/index.d.ts +12 -195
  92. package/src/data/ratingScale.js +14 -0
  93. package/src/data/teamTypes.d.ts +23 -0
  94. package/src/data/trackTypes.d.ts +101 -0
  95. package/src/data/vehicleData.js +12 -23
  96. package/src/environment/actions.js +3 -2
  97. package/src/environment/controllerLoop.js +57 -44
  98. package/src/environment/controllerScheduler.js +30 -0
  99. package/src/environment/driverObservations.js +260 -0
  100. package/src/environment/driverReset.js +51 -0
  101. package/src/environment/episode.js +0 -1
  102. package/src/environment/evaluation.js +6 -90
  103. package/src/environment/evaluationTracker.js +100 -0
  104. package/src/environment/events.js +23 -0
  105. package/src/environment/index.d.ts +35 -91
  106. package/src/environment/metrics.js +3 -17
  107. package/src/environment/observationSchema.js +104 -0
  108. package/src/environment/observationVector.js +16 -115
  109. package/src/environment/observations.js +16 -316
  110. package/src/environment/options.js +5 -24
  111. package/src/environment/renderFrames.js +21 -0
  112. package/src/environment/resetOptions.js +41 -0
  113. package/src/environment/results.js +125 -0
  114. package/src/environment/rewards.js +10 -16
  115. package/src/environment/runtime.js +17 -300
  116. package/src/environment/scenarioOptions.js +90 -0
  117. package/src/environment/scenarioPlacements.js +70 -0
  118. package/src/environment/scenarioPresets.js +78 -0
  119. package/src/environment/scenarios.js +5 -236
  120. package/src/environment/scratch.js +18 -0
  121. package/src/environment/sensorOptions.js +20 -0
  122. package/src/environment/sensors/index.js +2 -556
  123. package/src/environment/sensors/indexedRayBands.js +0 -18
  124. package/src/environment/sensors/nearbyCars.js +18 -15
  125. package/src/environment/sensors/opponentRadar.js +3 -2
  126. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  127. package/src/environment/sensors/rayBandTrace.js +1 -37
  128. package/src/environment/sensors/rayBatch.js +81 -0
  129. package/src/environment/sensors/rayConfig.js +26 -0
  130. package/src/environment/sensors/rayGeometry.js +0 -8
  131. package/src/environment/sensors/rayGuards.js +0 -6
  132. package/src/environment/sensors/rayOutputs.js +75 -0
  133. package/src/environment/sensors/rayQuery.js +110 -0
  134. package/src/environment/sensors/rayScratch.js +146 -0
  135. package/src/environment/sensors/sensorTargets.js +12 -4
  136. package/src/environment/sensors/surfaceRays.js +6 -61
  137. package/src/environment/sensors/trackRays.js +7 -80
  138. package/src/environment/simulationSetup.js +30 -0
  139. package/src/environment/specs.js +15 -109
  140. package/src/index.d.ts +43 -202
  141. package/src/index.js +28 -6
  142. package/src/placeholder/index.js +1 -8
  143. package/src/rendering/displayLifecycle.js +6 -0
  144. package/src/rendering/proceduralTrackAsset.js +1 -1
  145. package/src/rendering/renderSnapshot.js +3 -0
  146. package/src/rendering/track/finishLineRenderer.js +1 -1
  147. package/src/rendering/track/gridRenderer.js +1 -1
  148. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  149. package/src/rendering/track/pitLaneRenderer.js +1 -1
  150. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  151. package/src/rendering/track/trackRenderConstants.js +0 -1
  152. package/src/rendering/track/trackRenderGeometry.js +1 -7
  153. package/src/simulation/collisionGeometry.js +12 -20
  154. package/src/simulation/driver/advancedPathControls.js +53 -0
  155. package/src/simulation/driver/advancedRacePace.js +112 -0
  156. package/src/simulation/driver/advancedRacingControls.js +61 -0
  157. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  158. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  159. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  160. package/src/simulation/driver/arcadeRacePace.js +83 -0
  161. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  162. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  163. package/src/simulation/driver/driverControlConstants.js +0 -2
  164. package/src/simulation/driver/driverController.js +16 -5
  165. package/src/simulation/driver/edgeRecovery.js +2 -3
  166. package/src/simulation/driver/racingControls.js +10 -396
  167. package/src/simulation/driver/racingDriverProfile.js +48 -0
  168. package/src/simulation/driver/racingLinePlan.js +54 -20
  169. package/src/simulation/driver/rejoinControls.js +59 -112
  170. package/src/simulation/driver/safetyCarControls.js +12 -0
  171. package/src/simulation/driver/trafficScan.js +136 -27
  172. package/src/simulation/participants/participantInteractions.js +24 -15
  173. package/src/simulation/pit/pitCarPlacement.js +29 -0
  174. package/src/simulation/pit/pitFlow.js +8 -30
  175. package/src/simulation/pit/pitIntent.js +4 -3
  176. package/src/simulation/pit/pitOccupancy.js +2 -2
  177. package/src/simulation/pit/pitQueue.js +2 -18
  178. package/src/simulation/pit/pitRouteMovement.js +1 -36
  179. package/src/simulation/pit/pitRouting.js +5 -81
  180. package/src/simulation/pit/pitService.js +3 -32
  181. package/src/simulation/pit/pitServiceConstants.js +1 -0
  182. package/src/simulation/pit/pitServiceProgress.js +34 -0
  183. package/src/simulation/pit/pitSnapshots.js +8 -4
  184. package/src/simulation/pit/pitState.js +1 -5
  185. package/src/simulation/race/classification.js +1 -8
  186. package/src/simulation/race/raceFinish.js +3 -8
  187. package/src/simulation/race/raceOrder.js +2 -13
  188. package/src/simulation/race/raceProgress.js +6 -20
  189. package/src/simulation/race/raceSetup.js +1 -1
  190. package/src/simulation/race/raceStep.js +20 -11
  191. package/src/simulation/race/redFlag.js +2 -4
  192. package/src/simulation/race/safetyCar.js +5 -9
  193. package/src/simulation/race/stalledDnf.js +2 -4
  194. package/src/simulation/raceSimulation.js +6 -38
  195. package/src/simulation/replay/replayGhosts.js +44 -32
  196. package/src/simulation/rules/collisionSteward.js +0 -14
  197. package/src/simulation/rules/penaltyActions.js +37 -0
  198. package/src/simulation/rules/penaltyLedger.js +8 -1
  199. package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
  200. package/src/simulation/rules/rulesReview.js +20 -16
  201. package/src/simulation/rules/trackLimitsSteward.js +2 -2
  202. package/src/simulation/simMath.js +4 -0
  203. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  204. package/src/simulation/timing/drsState.js +8 -0
  205. package/src/simulation/timing/drsTiming.js +2 -4
  206. package/src/simulation/timing/lapTelemetry.js +7 -4
  207. package/src/simulation/timing/sectorPerformance.js +14 -6
  208. package/src/simulation/timing/timingLines.js +12 -6
  209. package/src/simulation/track/pitLaneAccess.js +2 -2
  210. package/src/simulation/track/pitLaneLayout.js +4 -135
  211. package/src/simulation/track/pitLaneModel.js +70 -0
  212. package/src/simulation/track/pitLaneState.js +0 -1
  213. package/src/simulation/track/pitServiceLayout.js +121 -0
  214. package/src/simulation/track/proceduralTrack.js +77 -0
  215. package/src/simulation/track/spatialQueries.js +76 -31
  216. package/src/simulation/track/trackBuilder.js +54 -0
  217. package/src/simulation/track/trackConstants.js +0 -22
  218. package/src/simulation/track/trackMath.js +4 -29
  219. package/src/simulation/track/trackModel.js +12 -240
  220. package/src/simulation/track/trackModelCache.js +105 -0
  221. package/src/simulation/track/trackNearestQueries.js +1218 -0
  222. package/src/simulation/track/trackQueryConstants.js +3 -0
  223. package/src/simulation/track/trackQueryConstruction.js +368 -0
  224. package/src/simulation/track/trackQueryIndex.js +12 -1933
  225. package/src/simulation/track/trackQuerySegments.js +174 -0
  226. package/src/simulation/track/trackQueryStats.js +12 -0
  227. package/src/simulation/track/trackRayQueries.js +165 -0
  228. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  229. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  230. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  231. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  232. package/src/simulation/vehicle/contactResolution.js +23 -9
  233. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  234. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  235. package/src/simulation/vehicle/runoffResponse.js +2 -4
  236. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  237. package/src/simulation/vehicle/vehicleGeometry.js +85 -103
  238. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  239. package/src/simulation/vehicle/vehiclePhysics.js +42 -196
  240. package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
  241. package/src/simulation/vehicle/vehicleState.js +5 -3
  242. package/src/simulation/vehicle/wheelSurface.js +13 -432
  243. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  244. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  245. package/src/styles.css +173 -19
  246. package/src/ui/bannerTemplates.js +4 -2
  247. package/src/ui/cameraControlsTemplate.js +4 -2
  248. package/src/ui/gridWalkTemplate.js +29 -0
  249. package/src/ui/htmlEscaping.js +8 -0
  250. package/src/ui/raceCanvasTemplate.js +3 -2
  251. package/src/ui/shellTemplate.js +3 -0
  252. package/src/ui/telemetryDrawerTemplate.js +4 -2
  253. package/src/ui/telemetryTemplates.js +3 -22
  254. package/src/ui/templateUtils.js +2 -8
  255. package/src/ui/viewControlsTemplate.js +21 -0
  256. package/examples/train-basic-policy.mjs +0 -276
  257. package/examples/trainingData.mjs +0 -133
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from '../timing/drsState.js';
1
2
  import { pointAt } from '../track/trackModel.js';
2
3
  import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
3
4
  import {
@@ -30,10 +31,7 @@ export function refreshLocalRaceStateForSimulation(sim) {
30
31
  stabilityState: car.destroyed ? 'destroyed' : car.stabilityState,
31
32
  });
32
33
  car.canAttack = false;
33
- car.drsEligible = false;
34
- car.drsActive = false;
35
- car.drsZoneId = null;
36
- car.drsZoneEnabled = false;
34
+ clearCurrentDrsState(car);
37
35
  return;
38
36
  }
39
37
  const centerState = takeValidRunoffCenterState(car);
@@ -98,10 +96,7 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true, refre
98
96
  stabilityState: car.destroyed ? 'destroyed' : car.stabilityState,
99
97
  });
100
98
  car.canAttack = false;
101
- car.drsEligible = false;
102
- car.drsActive = false;
103
- car.drsZoneId = null;
104
- car.drsZoneEnabled = false;
99
+ clearCurrentDrsState(car);
105
100
  recordTimingStateForCar(car, sim.time, sim.track, previousRaceDistanceForTiming, sim.runtimeBenchmarkStats);
106
101
  return;
107
102
  }
@@ -149,10 +144,7 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true, refre
149
144
  car.gapAheadSeconds = Infinity;
150
145
  car.intervalAheadSeconds = Infinity;
151
146
  car.leaderGapSeconds = Infinity;
152
- car.drsEligible = false;
153
- car.drsActive = false;
154
- car.drsZoneId = null;
155
- car.drsZoneEnabled = false;
147
+ clearCurrentDrsState(car);
156
148
  car.canAttack = false;
157
149
  });
158
150
  } else if (sim?.runtimeBenchmarkStats) {
@@ -175,10 +167,7 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true, refre
175
167
  car.gapAheadSeconds = Infinity;
176
168
  car.intervalAheadSeconds = Infinity;
177
169
  car.leaderGapSeconds = Infinity;
178
- car.drsEligible = false;
179
- car.drsActive = false;
180
- car.drsZoneId = null;
181
- car.drsZoneEnabled = false;
170
+ clearCurrentDrsState(car);
182
171
  car.canAttack = false;
183
172
  return;
184
173
  }
@@ -210,10 +199,7 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true, refre
210
199
  car.canAttack = !sim.safetyCar.deployed && !car.finished && !activePitStop;
211
200
  car.aggression = sim.computeAggression(car, index, aggressionFieldDepth);
212
201
  if (activePitStop) {
213
- car.drsEligible = false;
214
- car.drsActive = false;
215
- car.drsZoneId = null;
216
- car.drsZoneEnabled = false;
202
+ clearCurrentDrsState(car);
217
203
  } else if (updateDrs) sim.updateDrsLatch(car, drsReference, Boolean(drsReference));
218
204
  });
219
205
  sim.evaluateRaceFinish(ordered);
@@ -13,7 +13,7 @@ import {
13
13
  } from '../timing/raceTiming.js';
14
14
  import { normalizePitIntent, PIT_INTENT_NONE } from '../pit/pitIntent.js';
15
15
  import { assignPitLaneTeams, initializePitStops } from '../pit/pitState.js';
16
- import { clonePitLaneModel } from '../pit/pitRouting.js';
16
+ import { clonePitLaneModel } from '../track/pitLaneModel.js';
17
17
  import { createRaceControlState, createSafetyCarState } from './raceControlState.js';
18
18
  import { normalizeTotalLaps } from './raceDistance.js';
19
19
  import { normalizeRaceRules } from '../rulesConfig.js';
@@ -1,7 +1,7 @@
1
- import { decideDriverControls } from '../driverController.js';
1
+ import { decideDriverControls } from '../driver/driverController.js';
2
2
  import { clamp } from '../simMath.js';
3
3
  import { updateReplayGhosts } from '../replay/replayGhosts.js';
4
- import { integrateVehiclePhysics } from '../vehicle/vehiclePhysics.js';
4
+ import { integrateVehiclePhysics, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
5
5
  import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
6
6
  import { applyRedFlagHoldForSimulation } from './redFlag.js';
7
7
  import { freezeRetiredCar, updateStalledDnfForSimulation } from './stalledDnf.js';
@@ -38,19 +38,28 @@ export function runRaceStep(simulation, dt) {
38
38
 
39
39
  const orderedCars = simulation.orderedCars();
40
40
  const raceContext = simulation.driverRaceContext(orderedCars);
41
- const orderedIndexById = new Map(orderedCars.map((car, index) => [car.id, index]));
42
- const driveCars = orderedCars.length === simulation.cars.length
43
- ? orderedCars
44
- : [
45
- ...orderedCars,
46
- ...simulation.cars.filter((car) => !orderedIndexById.has(car.id)),
47
- ];
41
+ for (let index = 0; index < simulation.cars.length; index += 1) {
42
+ simulation.cars[index]._driveOrderIndex = -1;
43
+ }
44
+ for (let index = 0; index < orderedCars.length; index += 1) {
45
+ orderedCars[index]._driveOrderIndex = index;
46
+ }
47
+ let driveCars = orderedCars;
48
+ if (orderedCars.length !== simulation.cars.length) {
49
+ driveCars = simulation._driveCarsScratch ??= [];
50
+ driveCars.length = 0;
51
+ driveCars.push(...orderedCars);
52
+ for (let index = 0; index < simulation.cars.length; index += 1) {
53
+ const car = simulation.cars[index];
54
+ if (car._driveOrderIndex === -1) driveCars.push(car);
55
+ }
56
+ }
48
57
  driveCars.forEach((car) => {
49
58
  if (car.destroyed || car.outOfRace) {
50
59
  freezeRetiredCar(car, { physicsMode: simulation.physicsMode });
51
60
  return;
52
61
  }
53
- const orderIndex = orderedIndexById.get(car.id);
62
+ const orderIndex = car._driveOrderIndex >= 0 ? car._driveOrderIndex : undefined;
54
63
  car.previousX = car.x;
55
64
  car.previousY = car.y;
56
65
  car.previousHeading = car.heading;
@@ -66,7 +75,7 @@ export function runRaceStep(simulation, dt) {
66
75
  throttle: controls.throttle ?? 0,
67
76
  brake: controls.brake ?? 0,
68
77
  };
69
- if (simulation.physicsMode === 'advanced') {
78
+ if (isSimulatorPhysicsMode(simulation.physicsMode)) {
70
79
  measureRuntimePhase(simulation, 'prePhysicsWheelSurface', () => {
71
80
  applyWheelSurfaceState(car, simulation.track);
72
81
  });
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from '../timing/drsState.js';
1
2
  import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
2
3
 
3
4
  export function applyRedFlagHoldForSimulation(sim) {
@@ -6,10 +7,7 @@ export function applyRedFlagHoldForSimulation(sim) {
6
7
  car.previousY = car.y;
7
8
  car.previousHeading = car.heading;
8
9
  freezeVehicleMotion(car, { physicsMode: sim.physicsMode });
9
- car.drsActive = false;
10
- car.drsEligible = false;
11
- car.drsZoneId = null;
12
- car.drsZoneEnabled = false;
10
+ clearCurrentDrsState(car);
13
11
  car.canAttack = false;
14
12
  });
15
13
  }
@@ -1,6 +1,8 @@
1
+ import { clearCurrentDrsState } from '../timing/drsState.js';
1
2
  import { pointAt } from '../track/trackModel.js';
2
3
  import { kphToSimSpeed } from '../units.js';
3
4
  import { freezeVehicleMotion, syncVehicleVelocityToHeading } from '../vehicle/vehicleKinematics.js';
5
+ import { isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
4
6
 
5
7
  export function setSafetyCarState(sim, deployed) {
6
8
  const next = Boolean(deployed);
@@ -20,10 +22,7 @@ export function setSafetyCarState(sim, deployed) {
20
22
  }
21
23
  sim.cars.forEach((car) => {
22
24
  car.desiredOffset = 0;
23
- car.drsActive = false;
24
- car.drsEligible = false;
25
- car.drsZoneId = null;
26
- car.drsZoneEnabled = false;
25
+ clearCurrentDrsState(car);
27
26
  });
28
27
  }
29
28
  sim.events.unshift({ type: next ? 'safety-car' : 'green-flag', at: sim.time });
@@ -62,10 +61,7 @@ export function setRedFlagState(sim, deployed) {
62
61
  sim.raceControl.frozenOrder = sim.orderedCars().map((car) => car.id);
63
62
  sim.cars.forEach((car) => {
64
63
  freezeVehicleMotion(car, { physicsMode: sim.physicsMode });
65
- car.drsActive = false;
66
- car.drsEligible = false;
67
- car.drsZoneId = null;
68
- car.drsZoneEnabled = false;
64
+ clearCurrentDrsState(car);
69
65
  car.canAttack = false;
70
66
  });
71
67
  } else {
@@ -82,7 +78,7 @@ export function setRedFlagState(sim, deployed) {
82
78
  });
83
79
  } else if (!car.finished) {
84
80
  car.speed = Math.max(car.speed, kphToSimSpeed(60));
85
- if (sim.physicsMode === 'advanced') syncVehicleVelocityToHeading(car);
81
+ if (isSimulatorPhysicsMode(sim.physicsMode)) syncVehicleVelocityToHeading(car);
86
82
  car.brake = 0;
87
83
  car.throttle = Math.max(car.throttle ?? 0, 0.35);
88
84
  }
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from '../timing/drsState.js';
1
2
  import { markCarDnf } from './retirements.js';
2
3
  import { simSpeedToKph } from '../units.js';
3
4
  import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
@@ -66,10 +67,7 @@ export function freezeRetiredCar(car, { physicsMode = 'arcade' } = {}) {
66
67
  stabilityState: 'destroyed',
67
68
  });
68
69
  car.canAttack = false;
69
- car.drsActive = false;
70
- car.drsEligible = false;
71
- car.drsZoneId = null;
72
- car.drsZoneEnabled = false;
70
+ clearCurrentDrsState(car);
73
71
  }
74
72
 
75
73
  function resetStallTimer(car) {
@@ -1,11 +1,7 @@
1
1
  import {
2
2
  applyUnservedServicePenalty,
3
- cancelPenaltyRecord,
4
- createPenaltyEvent,
5
- createPenaltyRecord,
6
- servePenaltyRecord,
7
3
  } from './rules/penaltyLedger.js';
8
- import { createPenaltyCancelledEvent, createPenaltyServedEvent } from './rules/penaltyEvents.js';
4
+ import { recordPenaltyForSimulation, servePenaltyForSimulation, cancelPenaltyForSimulation } from './rules/penaltyActions.js';
9
5
  import { buildPenaltyStatsByDriver, getPenaltyStats } from './rules/penaltyStats.js';
10
6
  import {
11
7
  reviewCollisionForSimulation,
@@ -93,7 +89,6 @@ import { applyGridDropForSimulation } from './race/gridPenalties.js';
93
89
  import { initializeRaceSimulation, normalizePitIntentForRace } from './race/raceSetup.js';
94
90
  import { runRaceStep } from './race/raceStep.js';
95
91
  import {
96
- createVehicleSnapshotDependencies,
97
92
  getRaceWinnerSnapshot as getRaceWinnerSnapshotForSimulation,
98
93
  snapshotRace,
99
94
  snapshotRaceObservation,
@@ -209,7 +204,7 @@ export class F1RaceSimulation {
209
204
  }
210
205
 
211
206
  isCarInActivePitStop(car) {
212
- return isCarInActivePitStopForSimulation(this, car);
207
+ return isCarInActivePitStopForSimulation(car);
213
208
  }
214
209
 
215
210
  schedulePitStopAtNextEntry(car, stop) {
@@ -249,7 +244,7 @@ export class F1RaceSimulation {
249
244
  }
250
245
 
251
246
  isPitServiceAreaOccupied(candidate, box) {
252
- return isPitServiceAreaOccupiedForSimulation(this, candidate, box);
247
+ return isPitServiceAreaOccupiedForSimulation(candidate, box);
253
248
  }
254
249
 
255
250
  isPitServiceQueueOccupied(car, box) {
@@ -317,38 +312,15 @@ export class F1RaceSimulation {
317
312
  }
318
313
 
319
314
  recordPenalty(penalty) {
320
- const car = this.cars.find((item) => item.id === penalty.driverId);
321
- const entry = createPenaltyRecord({
322
- sequence: this.nextPenaltyId,
323
- time: this.time,
324
- lap: this.computeLap(car?.raceDistance ?? 0),
325
- penalty,
326
- });
327
- this.nextPenaltyId += 1;
328
- this.penalties.push(entry);
329
- if (entry.gridDrop > 0 && this.raceControl.mode === 'pre-start') {
330
- this.applyGridDrop(entry.driverId, entry.gridDrop);
331
- }
332
- this.events.unshift(createPenaltyEvent(entry));
333
- return entry;
315
+ return recordPenaltyForSimulation(this, penalty);
334
316
  }
335
317
 
336
318
  servePenalty(penaltyId) {
337
- const penalty = this.penalties.find((entry) => entry.id === penaltyId);
338
- const result = servePenaltyRecord(penalty, this.time);
339
- if (result) {
340
- this.events.unshift(createPenaltyServedEvent(result, this.time));
341
- }
342
- return result;
319
+ return servePenaltyForSimulation(this, penaltyId);
343
320
  }
344
321
 
345
322
  cancelPenalty(penaltyId) {
346
- const penalty = this.penalties.find((entry) => entry.id === penaltyId);
347
- const result = cancelPenaltyRecord(penalty, this.time);
348
- if (result) {
349
- this.events.unshift(createPenaltyCancelledEvent(result, this.time));
350
- }
351
- return result;
323
+ return cancelPenaltyForSimulation(this, penaltyId);
352
324
  }
353
325
 
354
326
  getDriverPenaltySeconds(driverId) {
@@ -389,10 +361,6 @@ export class F1RaceSimulation {
389
361
  reviewPitLaneSpeedingForSimulation(this);
390
362
  }
391
363
 
392
- vehicleSnapshotDependencies() {
393
- return createVehicleSnapshotDependencies(this);
394
- }
395
-
396
364
  snapshot() {
397
365
  return snapshotRace(this);
398
366
  }
@@ -11,25 +11,17 @@ export function normalizeReplayGhosts(replayGhosts = []) {
11
11
  }
12
12
 
13
13
  export function updateReplayGhosts(replayGhosts = [], timeSeconds = 0) {
14
- replayGhosts.forEach((ghost) => {
15
- const previous = {
16
- x: ghost.x,
17
- y: ghost.y,
18
- heading: ghost.heading,
19
- };
20
- const sample = interpolateTrajectory(ghost.trajectory, timeSeconds);
21
- Object.assign(ghost, {
22
- previousX: Number.isFinite(previous.x) ? previous.x : sample.x,
23
- previousY: Number.isFinite(previous.y) ? previous.y : sample.y,
24
- previousHeading: Number.isFinite(previous.heading) ? previous.heading : sample.heading,
25
- x: sample.x,
26
- y: sample.y,
27
- heading: sample.heading,
28
- speedKph: sample.speedKph,
29
- progressMeters: sample.progressMeters,
30
- timeSeconds,
31
- });
32
- });
14
+ for (let index = 0; index < replayGhosts.length; index += 1) {
15
+ const ghost = replayGhosts[index];
16
+ const previousX = ghost.x;
17
+ const previousY = ghost.y;
18
+ const previousHeading = ghost.heading;
19
+ interpolateTrajectoryInto(ghost, ghost.trajectory, timeSeconds);
20
+ ghost.previousX = Number.isFinite(previousX) ? previousX : ghost.x;
21
+ ghost.previousY = Number.isFinite(previousY) ? previousY : ghost.y;
22
+ ghost.previousHeading = Number.isFinite(previousHeading) ? previousHeading : ghost.heading;
23
+ ghost.timeSeconds = timeSeconds;
24
+ }
33
25
  }
34
26
 
35
27
  export function serializeReplayGhosts(replayGhosts = []) {
@@ -57,7 +49,7 @@ function normalizeReplayGhost(ghost, index) {
57
49
  const trajectory = normalizeTrajectory(ghost.trajectory);
58
50
  if (trajectory.length === 0) return null;
59
51
  const id = String(ghost.id ?? `replay-ghost-${index + 1}`);
60
- const initial = interpolateTrajectory(trajectory, 0);
52
+ const initial = interpolateTrajectoryInto({}, trajectory, 0);
61
53
  return {
62
54
  id,
63
55
  label: String(ghost.label ?? id),
@@ -103,23 +95,43 @@ function normalizeTrajectory(trajectory) {
103
95
  .sort((a, b) => a.timeSeconds - b.timeSeconds);
104
96
  }
105
97
 
106
- function interpolateTrajectory(trajectory, timeSeconds) {
107
- if (trajectory.length === 1 || timeSeconds <= trajectory[0].timeSeconds) return { ...trajectory[0] };
98
+ function interpolateTrajectoryInto(target, trajectory, timeSeconds) {
99
+ if (trajectory.length === 1 || timeSeconds <= trajectory[0].timeSeconds) {
100
+ return writeTrajectorySample(target, trajectory[0]);
101
+ }
108
102
  const last = trajectory[trajectory.length - 1];
109
- if (timeSeconds >= last.timeSeconds) return { ...last };
110
- const nextIndex = trajectory.findIndex((sample) => sample.timeSeconds >= timeSeconds);
103
+ if (timeSeconds >= last.timeSeconds) return writeTrajectorySample(target, last);
104
+ const nextIndex = lowerBoundTrajectoryTime(trajectory, timeSeconds);
111
105
  const previous = trajectory[Math.max(0, nextIndex - 1)];
112
106
  const next = trajectory[nextIndex];
113
107
  const span = next.timeSeconds - previous.timeSeconds || 1;
114
108
  const amount = clamp((timeSeconds - previous.timeSeconds) / span, 0, 1);
115
- return {
116
- timeSeconds,
117
- x: lerp(previous.x, next.x, amount),
118
- y: lerp(previous.y, next.y, amount),
119
- heading: previous.heading + normalizeAngle(next.heading - previous.heading) * amount,
120
- speedKph: lerp(previous.speedKph, next.speedKph, amount),
121
- progressMeters: lerp(previous.progressMeters, next.progressMeters, amount),
122
- };
109
+ target.x = lerp(previous.x, next.x, amount);
110
+ target.y = lerp(previous.y, next.y, amount);
111
+ target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
112
+ target.speedKph = lerp(previous.speedKph, next.speedKph, amount);
113
+ target.progressMeters = lerp(previous.progressMeters, next.progressMeters, amount);
114
+ return target;
115
+ }
116
+
117
+ function lowerBoundTrajectoryTime(trajectory, timeSeconds) {
118
+ let low = 0;
119
+ let high = trajectory.length;
120
+ while (low < high) {
121
+ const middle = Math.floor((low + high) / 2);
122
+ if (trajectory[middle].timeSeconds < timeSeconds) low = middle + 1;
123
+ else high = middle;
124
+ }
125
+ return low;
126
+ }
127
+
128
+ function writeTrajectorySample(target, sample) {
129
+ target.x = sample.x;
130
+ target.y = sample.y;
131
+ target.heading = sample.heading;
132
+ target.speedKph = sample.speedKph;
133
+ target.progressMeters = sample.progressMeters;
134
+ return target;
123
135
  }
124
136
 
125
137
  function finiteNumber(value) {
@@ -1,19 +1,5 @@
1
1
  import { simSpeedToKph } from '../units.js';
2
2
 
3
- export function calculateCollisionPenalties({ first, second, collision = null, rule }) {
4
- const penalties = [];
5
- emitCollisionPenalties({
6
- first,
7
- second,
8
- collision,
9
- rule,
10
- emit: (penalty) => {
11
- penalties.push(penalty);
12
- },
13
- });
14
- return penalties;
15
- }
16
-
17
3
  export function emitCollisionPenalties({ first, second, collision = null, rule, emit, emitContext = null }) {
18
4
  if (!rule || typeof emit !== 'function') return 0;
19
5
  const severity = collision?.depth ?? 0;
@@ -0,0 +1,37 @@
1
+ import { cancelPenaltyRecord, createPenaltyEvent, createPenaltyRecord, servePenaltyRecord } from './penaltyLedger.js';
2
+ import { createPenaltyCancelledEvent, createPenaltyServedEvent } from './penaltyEvents.js';
3
+
4
+ export function recordPenaltyForSimulation(sim, penalty) {
5
+ const car = sim.cars.find((item) => item.id === penalty.driverId);
6
+ const entry = createPenaltyRecord({
7
+ sequence: sim.nextPenaltyId,
8
+ time: sim.time,
9
+ lap: sim.computeLap(car?.raceDistance ?? 0),
10
+ penalty,
11
+ });
12
+ sim.nextPenaltyId += 1;
13
+ sim.penalties.push(entry);
14
+ if (entry.gridDrop > 0 && sim.raceControl.mode === 'pre-start') {
15
+ sim.applyGridDrop(entry.driverId, entry.gridDrop);
16
+ }
17
+ sim.events.unshift(createPenaltyEvent(entry));
18
+ return entry;
19
+ }
20
+
21
+ export function servePenaltyForSimulation(sim, penaltyId) {
22
+ const penalty = sim.penalties.find((entry) => entry.id === penaltyId);
23
+ const result = servePenaltyRecord(penalty, sim.time);
24
+ if (result) {
25
+ sim.events.unshift(createPenaltyServedEvent(result, sim.time));
26
+ }
27
+ return result;
28
+ }
29
+
30
+ export function cancelPenaltyForSimulation(sim, penaltyId) {
31
+ const penalty = sim.penalties.find((entry) => entry.id === penaltyId);
32
+ const result = cancelPenaltyRecord(penalty, sim.time);
33
+ if (result) {
34
+ sim.events.unshift(createPenaltyCancelledEvent(result, sim.time));
35
+ }
36
+ return result;
37
+ }
@@ -48,7 +48,14 @@ export function createPenaltyEvent(entry) {
48
48
  }
49
49
 
50
50
  export function serializePenalty(penalty) {
51
- return { ...penalty };
51
+ return {
52
+ ...penalty,
53
+ ...(Array.isArray(penalty.consequences) ? {
54
+ consequences: penalty.consequences.map((consequence) => ({ ...consequence })),
55
+ } : {}),
56
+ ...(Array.isArray(penalty.usedCompounds) ? { usedCompounds: [...penalty.usedCompounds] } : {}),
57
+ ...(Array.isArray(penalty.wheelOffsets) ? { wheelOffsets: [...penalty.wheelOffsets] } : {}),
58
+ };
52
59
  }
53
60
 
54
61
  export function servePenaltyRecord(penalty, time) {
@@ -20,7 +20,7 @@ export function calculatePitLaneSpeedingReview({ car, rule, stewardState }) {
20
20
 
21
21
  if (!isViolation) {
22
22
  return {
23
- nextState: { ...current, active: false },
23
+ nextState: current.active === false ? current : { ...current, active: false },
24
24
  event: null,
25
25
  penalty: null,
26
26
  };
@@ -5,6 +5,7 @@ import { calculatePitLaneSpeedingReview } from './pitLaneSpeedingSteward.js';
5
5
  import { getPenaltyRule } from '../rulesConfig.js';
6
6
  import { simSpeedToKph } from '../units.js';
7
7
  import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
+ import { progressDelta } from '../race/raceDistance.js';
8
9
 
9
10
  function velocityComponentX(car) {
10
11
  return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
@@ -18,13 +19,6 @@ function velocityComponentY(car) {
18
19
  : Math.sin(car.heading) * car.speed;
19
20
  }
20
21
 
21
- function progressDelta(a, b, trackLength) {
22
- let delta = a - b;
23
- if (delta < -trackLength / 2) delta += trackLength;
24
- if (delta > trackLength / 2) delta -= trackLength;
25
- return delta;
26
- }
27
-
28
22
  function prepareCollisionStewardContext(scratch = null) {
29
23
  const target = scratch
30
24
  ? (scratch.collisionStewardContext ??= { sharedFaultDriverIds: [] })
@@ -129,7 +123,9 @@ export function reviewTrackLimitsForSimulation(sim) {
129
123
  sim.cars.forEach((car) => {
130
124
  if (isLegallyInsidePitLaneForTrackLimits(car)) {
131
125
  const current = sim.stewardState.trackLimits[car.id];
132
- sim.stewardState.trackLimits[car.id] = { ...(current ?? { violations: 0 }), active: false };
126
+ if (!current || current.active !== false) {
127
+ sim.stewardState.trackLimits[car.id] = { ...(current ?? { violations: 0 }), active: false };
128
+ }
133
129
  return;
134
130
  }
135
131
 
@@ -146,18 +142,26 @@ export function reviewTrackLimitsForSimulation(sim) {
146
142
  });
147
143
  }
148
144
 
145
+ const PIT_SPEED_REVIEW_SCRATCH = {
146
+ car: { id: null, speedKph: 0, trackState: null, pitLanePart: null },
147
+ rule: null,
148
+ stewardState: null,
149
+ };
150
+
149
151
  export function reviewPitLaneSpeedingForSimulation(sim) {
150
152
  const rule = getPenaltyRule(sim.rules, 'pitLaneSpeeding');
151
153
  sim.cars.forEach((car) => {
152
154
  const currentState = sim.stewardState.pitLaneSpeeding[car.id];
153
- const review = calculatePitLaneSpeedingReview({
154
- car: {
155
- ...car,
156
- speedKph: simSpeedToKph(car.speed),
157
- },
158
- rule,
159
- stewardState: currentState,
160
- });
155
+ // The steward only reads the speed and pit-lane location, so feed it a
156
+ // pooled view instead of spreading the whole car every step.
157
+ const reviewCar = PIT_SPEED_REVIEW_SCRATCH.car;
158
+ reviewCar.id = car.id;
159
+ reviewCar.speedKph = simSpeedToKph(car.speed);
160
+ reviewCar.trackState = car.trackState;
161
+ reviewCar.pitLanePart = car.pitLanePart;
162
+ PIT_SPEED_REVIEW_SCRATCH.rule = rule;
163
+ PIT_SPEED_REVIEW_SCRATCH.stewardState = currentState;
164
+ const review = calculatePitLaneSpeedingReview(PIT_SPEED_REVIEW_SCRATCH);
161
165
  sim.stewardState.pitLaneSpeeding[car.id] = review.nextState;
162
166
  if (review.event) sim.events.unshift({ ...review.event, at: sim.time });
163
167
  if (review.penalty) sim.recordPenalty(review.penalty);
@@ -22,7 +22,7 @@ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
22
22
  const wheelStates = wheelSurfaceState.wheels;
23
23
  if (wheelStates.some((wheelState) => wheelState.inPitLane)) {
24
24
  return {
25
- nextState: { ...current, active: false },
25
+ nextState: current.active === false ? current : { ...current, active: false },
26
26
  event: null,
27
27
  penalty: null,
28
28
  };
@@ -34,7 +34,7 @@ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
34
34
 
35
35
  if (!isViolation) {
36
36
  return {
37
- nextState: { ...current, active: false },
37
+ nextState: current.active === false ? current : { ...current, active: false },
38
38
  event: null,
39
39
  penalty: null,
40
40
  };
@@ -32,3 +32,7 @@ export function seededRange(random, min, max) {
32
32
  export function wrapDistance(value, length) {
33
33
  return ((value % length) + length) % length;
34
34
  }
35
+
36
+ export function finiteOrNull(value) {
37
+ return Number.isFinite(value) ? value : null;
38
+ }
@@ -38,12 +38,8 @@ export function createVehicleSnapshotDependencies(sim) {
38
38
  normalizePitIntent,
39
39
  PIT_INTENT_NONE,
40
40
  serializeLapTelemetry,
41
- serializeObservationPitStop: (pitStop) => serializeObservationPitStop(
42
- pitStop,
43
- normalizePitIntent,
44
- PIT_INTENT_NONE,
45
- ),
46
- serializePitStop: (pitStop) => serializePitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE),
41
+ serializeObservationPitStop,
42
+ serializePitStop,
47
43
  serializeRenderPitStop,
48
44
  simSpeedToKph,
49
45
  simUnitsToMeters,
@@ -220,11 +216,34 @@ function carsForSnapshot(sim, ordered) {
220
216
  return [...ranked, ...unranked];
221
217
  }
222
218
 
219
+ const RENDER_CAR_ORDER_SCRATCH = new WeakMap();
220
+
223
221
  function renderCarsForSnapshotInto(targetCars, sim, ordered, dependencies) {
224
- const entries = carsForSnapshot(sim, ordered);
225
- targetCars.length = entries.length;
226
- entries.forEach(({ car }, index) => {
227
- targetCars[index] = serializeRenderCarInto(targetCars[index] ?? {}, car, dependencies);
228
- });
222
+ let scratch = RENDER_CAR_ORDER_SCRATCH.get(targetCars);
223
+ if (!scratch) {
224
+ scratch = { orderedIds: new Set(), unranked: [] };
225
+ RENDER_CAR_ORDER_SCRATCH.set(targetCars, scratch);
226
+ }
227
+ const { orderedIds, unranked } = scratch;
228
+ orderedIds.clear();
229
+ unranked.length = 0;
230
+ let count = 0;
231
+ for (const car of ordered) {
232
+ orderedIds.add(car.id);
233
+ targetCars[count] = serializeRenderCarInto(targetCars[count] ?? {}, car, dependencies);
234
+ count += 1;
235
+ }
236
+ for (const car of sim.cars) {
237
+ if (!orderedIds.has(car.id)) unranked.push(car);
238
+ }
239
+ unranked.sort((a, b) => a.index - b.index);
240
+ for (const car of unranked) {
241
+ targetCars[count] = serializeRenderCarInto(targetCars[count] ?? {}, car, dependencies);
242
+ count += 1;
243
+ }
244
+ targetCars.length = count;
245
+ // Scratch owns ordering only; do not retain live participants between frames.
246
+ orderedIds.clear();
247
+ unranked.length = 0;
229
248
  return targetCars;
230
249
  }
@@ -0,0 +1,8 @@
1
+ // Suppress the current latch without erasing passage history or next-zone hints.
2
+ // External placement uses the separate full reset in vehicleState.js.
3
+ export function clearCurrentDrsState(car) {
4
+ car.drsActive = false;
5
+ car.drsEligible = false;
6
+ car.drsZoneId = null;
7
+ car.drsZoneEnabled = false;
8
+ }
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from './drsState.js';
1
2
  import { wrapDistance } from '../simMath.js';
2
3
 
3
4
  function wrapProgress(value, length) {
@@ -106,10 +107,7 @@ function updateDrsLatchCore(
106
107
  runtimeBenchmarkStats = null,
107
108
  ) {
108
109
  if (safetyCarDeployed || car.finished) {
109
- car.drsEligible = false;
110
- car.drsActive = false;
111
- car.drsZoneId = null;
112
- car.drsZoneEnabled = false;
110
+ clearCurrentDrsState(car);
113
111
  car._drsZoneRef = null;
114
112
  return;
115
113
  }