@inventure71/paddockjs 1.0.0 → 2.0.1

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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,167 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { kphToSimSpeed, metersToSimUnits } from '../units.js';
3
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
4
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
5
+ import {
6
+ distanceToNextLimiterSegment,
7
+ nearestDistanceOnRoute,
8
+ routeLimiterActiveAt,
9
+ sampleRoute,
10
+ } from './pitRouting.js';
11
+ import { syncPitCarKinematics } from './pitKinematics.js';
12
+ import { completePitPenaltyService } from './pitPenaltyService.js';
13
+ import { completePitService } from './pitTireService.js';
14
+ import { getPitBoxRaceDistance, getPitStopBox } from './pitOccupancy.js';
15
+ import {
16
+ PIT_BOX_STOP_SPEED,
17
+ PIT_ENTRY_BOX_CAPTURE_DISTANCE,
18
+ PIT_ENTRY_CONNECTOR_OVERSPEED_KPH,
19
+ PIT_LIMITER_APPROACH_SPEED_SLOPE,
20
+ PIT_LIMITER_BRAKE_DISTANCE,
21
+ PIT_QUEUE_CAPTURE_DISTANCE,
22
+ PIT_QUEUE_CAPTURE_SPEED,
23
+ PIT_QUEUE_RELEASE_FINISH_DISTANCE,
24
+ PIT_QUEUE_RELEASE_SPEED,
25
+ PIT_ROUTE_FINISH_DISTANCE,
26
+ pointDistance,
27
+ } from './pitServiceConstants.js';
28
+
29
+ export function applyPitRoutePosition(sim, car, delta) {
30
+ const stop = car.pitStop;
31
+ if (!stop?.route) return false;
32
+ const speedLimit = kphToSimSpeed(sim.rules.modules?.pitStops?.pitLaneSpeedLimitKph ?? 80);
33
+ const route = stop.route;
34
+ if (stop.phase === 'queue-release') {
35
+ const nextProgress = Math.min(route.length, (stop.routeProgress ?? 0) + PIT_QUEUE_RELEASE_SPEED * delta);
36
+ const point = sampleRoute(route, nextProgress);
37
+ if (!point) return false;
38
+ const previousHeading = car.heading;
39
+ car.previousX = car.x;
40
+ car.previousY = car.y;
41
+ car.previousHeading = car.heading;
42
+ car.x = point.x;
43
+ car.y = point.y;
44
+ car.heading = point.heading ?? sim.track.pitLane.mainLane.heading;
45
+ car.speed = PIT_QUEUE_RELEASE_SPEED;
46
+ car.throttle = 0.32;
47
+ car.brake = 0;
48
+ car.yawRate = normalizeAngle(car.heading - previousHeading) / Math.max(delta, 1e-6);
49
+ car.steeringAngle = clamp(
50
+ Math.atan((car.yawRate * VEHICLE_LIMITS.wheelbase) / Math.max(car.speed, 1)),
51
+ -VEHICLE_LIMITS.maxSteer,
52
+ VEHICLE_LIMITS.maxSteer,
53
+ );
54
+ car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
55
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
56
+ stop.routeProgress = nextProgress;
57
+ const routeAmount = route.length > 0 ? stop.routeProgress / route.length : 1;
58
+ car.raceDistance = stop.routeStartRaceDistance +
59
+ (stop.routeEndRaceDistance - stop.routeStartRaceDistance) * routeAmount;
60
+ applyWheelSurfaceState(car, sim.track);
61
+ car.progress = car.trackState.distance;
62
+ car.lap = sim.computeLap(car.raceDistance);
63
+ return route.length - stop.routeProgress <= PIT_QUEUE_RELEASE_FINISH_DISTANCE;
64
+ }
65
+ stop.routeProgress = clamp(nearestDistanceOnRoute(route, car, stop.routeProgress ?? 0), 0, route.length);
66
+ const routeEnd = route.points.at(-1);
67
+ const limiterActive = routeLimiterActiveAt(route, stop.routeProgress);
68
+ let targetSpeed = limiterActive
69
+ ? Math.min(speedLimit, VEHICLE_LIMITS.maxSpeed)
70
+ : Math.min(kphToSimSpeed(150), VEHICLE_LIMITS.maxSpeed);
71
+ if (!limiterActive && stop.status === 'entering') {
72
+ targetSpeed = Math.min(targetSpeed, speedLimit + kphToSimSpeed(PIT_ENTRY_CONNECTOR_OVERSPEED_KPH));
73
+ const distanceToLimiter = distanceToNextLimiterSegment(route, stop.routeProgress);
74
+ if (distanceToLimiter < PIT_LIMITER_BRAKE_DISTANCE) {
75
+ targetSpeed = Math.min(
76
+ targetSpeed,
77
+ speedLimit + Math.max(0, distanceToLimiter * PIT_LIMITER_APPROACH_SPEED_SLOPE),
78
+ );
79
+ }
80
+ }
81
+ if (stop.status === 'entering') {
82
+ const remainingBefore = Math.max(0, route.length - stop.routeProgress);
83
+ const captureDistance = stop.queueingForService ? PIT_QUEUE_CAPTURE_DISTANCE : PIT_ENTRY_BOX_CAPTURE_DISTANCE;
84
+ const brakeZone = stop.queueingForService ? metersToSimUnits(25) : metersToSimUnits(80);
85
+ if (remainingBefore < brakeZone) {
86
+ const brakingSlope = stop.queueingForService ? kphToSimSpeed(62) / metersToSimUnits(100) : kphToSimSpeed(24) / metersToSimUnits(100);
87
+ const captureSpeedFloor = stop.queueingForService ? kphToSimSpeed(42) : kphToSimSpeed(28);
88
+ const approachSpeed = remainingBefore > captureDistance
89
+ ? Math.max(captureSpeedFloor, remainingBefore * brakingSlope)
90
+ : remainingBefore * brakingSlope;
91
+ targetSpeed = Math.min(targetSpeed, clamp(approachSpeed, 0, speedLimit * 0.78));
92
+ }
93
+ }
94
+
95
+ const nextProgress = Math.min(route.length, stop.routeProgress + Math.max(0, targetSpeed) * delta);
96
+ const point = sampleRoute(route, nextProgress);
97
+ if (!point) return false;
98
+ const previousSpeed = car.speed;
99
+ const previousHeading = car.heading;
100
+ car.previousX = car.x;
101
+ car.previousY = car.y;
102
+ car.previousHeading = car.heading;
103
+ car.x = point.x;
104
+ car.y = point.y;
105
+ car.heading = point.heading ?? car.heading;
106
+ car.speed = targetSpeed;
107
+ car.throttle = limiterActive ? 0.28 : 0.46;
108
+ car.brake = targetSpeed < previousSpeed ? 0.4 : 0;
109
+ car.yawRate = normalizeAngle(car.heading - previousHeading) / Math.max(delta, 1e-6);
110
+ car.steeringAngle = clamp(
111
+ Math.atan((car.yawRate * VEHICLE_LIMITS.wheelbase) / Math.max(car.speed, 1)),
112
+ -VEHICLE_LIMITS.maxSteer,
113
+ VEHICLE_LIMITS.maxSteer,
114
+ );
115
+ car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
116
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
117
+ applyWheelSurfaceState(car, sim.track);
118
+ stop.routeProgress = nextProgress;
119
+ const routeAmount = route.length > 0 ? stop.routeProgress / route.length : 1;
120
+ car.raceDistance = stop.routeStartRaceDistance +
121
+ (stop.routeEndRaceDistance - stop.routeStartRaceDistance) * routeAmount;
122
+ car.progress = car.trackState.distance;
123
+ car.lap = sim.computeLap(car.raceDistance);
124
+ const remainingAfter = Math.max(0, route.length - stop.routeProgress);
125
+ if (stop.status === 'entering') {
126
+ const captureDistance = stop.queueingForService ? PIT_QUEUE_CAPTURE_DISTANCE : PIT_ENTRY_BOX_CAPTURE_DISTANCE;
127
+ const captureSpeed = stop.queueingForService ? PIT_QUEUE_CAPTURE_SPEED : PIT_BOX_STOP_SPEED;
128
+ return remainingAfter <= captureDistance &&
129
+ pointDistance(car, routeEnd) <= captureDistance &&
130
+ car.speed <= captureSpeed;
131
+ }
132
+ return remainingAfter <= PIT_ROUTE_FINISH_DISTANCE;
133
+ }
134
+
135
+ export function advancePitService(sim, car, delta) {
136
+ const stop = car.pitStop;
137
+ const box = getPitStopBox(sim, stop);
138
+ if (!box) return true;
139
+ if (stop.phase === 'penalty') {
140
+ stop.penaltyServiceRemaining = Math.max(0, (stop.penaltyServiceRemaining ?? 0) - delta);
141
+ stop.serviceRemaining = stop.penaltyServiceRemaining;
142
+ } else {
143
+ stop.serviceRemaining = Math.max(0, (stop.serviceRemaining ?? 0) - delta);
144
+ }
145
+ car.previousX = car.x;
146
+ car.previousY = car.y;
147
+ car.previousHeading = car.heading;
148
+ car.x = box.center.x;
149
+ car.y = box.center.y;
150
+ car.heading = sim.track.pitLane.mainLane.heading;
151
+ car.speed = 0;
152
+ car.throttle = 0;
153
+ car.brake = 1;
154
+ car.steeringAngle = 0;
155
+ car.yawRate = 0;
156
+ car.turnRadius = Infinity;
157
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
158
+ applyWheelSurfaceState(car, sim.track);
159
+ car.progress = car.trackState.distance;
160
+ car.raceDistance = getPitBoxRaceDistance(sim, stop, box);
161
+ if (stop.phase === 'penalty' && stop.penaltyServiceRemaining <= 0) {
162
+ completePitPenaltyService(sim, car);
163
+ } else if (stop.serviceRemaining <= 0) {
164
+ completePitService(sim, car);
165
+ }
166
+ return true;
167
+ }
@@ -0,0 +1,319 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { metersToSimUnits } from '../units.js';
3
+ import { nearestTrackState, offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
+
5
+ const PIT_ENTRY_APPROACH_DISTANCE = metersToSimUnits(250);
6
+
7
+ function clonePointLike(point) {
8
+ if (!point) return point;
9
+ return { ...point };
10
+ }
11
+
12
+ function clonePointArray(points) {
13
+ return Array.isArray(points) ? points.map(clonePointLike) : points;
14
+ }
15
+
16
+ export function clonePitLaneModel(pitLane) {
17
+ if (!pitLane) return pitLane;
18
+ return {
19
+ ...pitLane,
20
+ entry: pitLane.entry ? {
21
+ ...pitLane.entry,
22
+ trackPoint: clonePointLike(pitLane.entry.trackPoint),
23
+ edgePoint: clonePointLike(pitLane.entry.edgePoint),
24
+ trackConnectPoint: clonePointLike(pitLane.entry.trackConnectPoint),
25
+ lanePoint: clonePointLike(pitLane.entry.lanePoint),
26
+ roadCenterline: clonePointArray(pitLane.entry.roadCenterline),
27
+ connector: clonePointArray(pitLane.entry.connector),
28
+ } : pitLane.entry,
29
+ exit: pitLane.exit ? {
30
+ ...pitLane.exit,
31
+ trackPoint: clonePointLike(pitLane.exit.trackPoint),
32
+ edgePoint: clonePointLike(pitLane.exit.edgePoint),
33
+ trackConnectPoint: clonePointLike(pitLane.exit.trackConnectPoint),
34
+ lanePoint: clonePointLike(pitLane.exit.lanePoint),
35
+ roadCenterline: clonePointArray(pitLane.exit.roadCenterline),
36
+ connector: clonePointArray(pitLane.exit.connector),
37
+ } : pitLane.exit,
38
+ mainLane: pitLane.mainLane ? {
39
+ ...pitLane.mainLane,
40
+ start: clonePointLike(pitLane.mainLane.start),
41
+ end: clonePointLike(pitLane.mainLane.end),
42
+ points: clonePointArray(pitLane.mainLane.points),
43
+ } : pitLane.mainLane,
44
+ workingLane: pitLane.workingLane ? {
45
+ ...pitLane.workingLane,
46
+ start: clonePointLike(pitLane.workingLane.start),
47
+ end: clonePointLike(pitLane.workingLane.end),
48
+ points: clonePointArray(pitLane.workingLane.points),
49
+ } : pitLane.workingLane,
50
+ fastLane: pitLane.fastLane ? { ...pitLane.fastLane } : pitLane.fastLane,
51
+ serviceNormal: clonePointLike(pitLane.serviceNormal),
52
+ boxes: Array.isArray(pitLane.boxes)
53
+ ? pitLane.boxes.map((box) => ({
54
+ ...box,
55
+ laneTarget: clonePointLike(box.laneTarget),
56
+ center: clonePointLike(box.center),
57
+ corners: clonePointArray(box.corners),
58
+ }))
59
+ : pitLane.boxes,
60
+ serviceAreas: Array.isArray(pitLane.serviceAreas)
61
+ ? pitLane.serviceAreas.map((area) => ({
62
+ ...area,
63
+ laneTarget: clonePointLike(area.laneTarget),
64
+ center: clonePointLike(area.center),
65
+ queuePoint: clonePointLike(area.queuePoint),
66
+ corners: clonePointArray(area.corners),
67
+ queueCorners: clonePointArray(area.queueCorners),
68
+ garageBoxIds: [...(area.garageBoxIds ?? [])],
69
+ pitCrew: area.pitCrew ? { ...area.pitCrew } : area.pitCrew,
70
+ }))
71
+ : pitLane.serviceAreas,
72
+ teams: Array.isArray(pitLane.teams)
73
+ ? pitLane.teams.map((team) => ({ ...team, boxIds: [...(team.boxIds ?? [])], pitCrew: team.pitCrew ? { ...team.pitCrew } : team.pitCrew }))
74
+ : pitLane.teams,
75
+ };
76
+ }
77
+
78
+ export function routePoint(point, heading = point?.heading, options = {}) {
79
+ if (!point) return null;
80
+ return {
81
+ x: point.x,
82
+ y: point.y,
83
+ heading,
84
+ limiterActive: Boolean(options.limiterActive ?? point.limiterActive),
85
+ };
86
+ }
87
+
88
+ export function offsetPitLanePoint(pitLane, point, lateralOffset) {
89
+ return {
90
+ ...point,
91
+ x: point.x + pitLane.serviceNormal.x * lateralOffset,
92
+ y: point.y + pitLane.serviceNormal.y * lateralOffset,
93
+ heading: pitLane.mainLane.heading,
94
+ };
95
+ }
96
+
97
+ export function pitMainLanePointAt(pitLane, distanceAlongLane, lateralOffset = 0) {
98
+ const amount = pitLane.mainLane.length > 0
99
+ ? clamp(distanceAlongLane / pitLane.mainLane.length, 0, 1)
100
+ : 0;
101
+ const point = {
102
+ x: pitLane.mainLane.start.x + (pitLane.mainLane.end.x - pitLane.mainLane.start.x) * amount,
103
+ y: pitLane.mainLane.start.y + (pitLane.mainLane.end.y - pitLane.mainLane.start.y) * amount,
104
+ heading: pitLane.mainLane.heading,
105
+ };
106
+ return offsetPitLanePoint(pitLane, point, lateralOffset);
107
+ }
108
+
109
+ export function pitDriveLaneOffset(pitLane) {
110
+ return pitLane.fastLane?.offset ?? 0;
111
+ }
112
+
113
+ function sameRoutePoint(first, second) {
114
+ return first && second && Math.hypot(first.x - second.x, first.y - second.y) < 0.001;
115
+ }
116
+
117
+ export function shiftPreviousRenderPose(car, dx, dy, dheading = 0) {
118
+ car.previousX = Number.isFinite(car.previousX) ? car.previousX + dx : car.x + dx;
119
+ car.previousY = Number.isFinite(car.previousY) ? car.previousY + dy : car.y + dy;
120
+ car.previousHeading = normalizeAngle(
121
+ (Number.isFinite(car.previousHeading) ? car.previousHeading : car.heading) + dheading,
122
+ );
123
+ }
124
+
125
+ export function pointDistance(first, second) {
126
+ if (!first || !second) return Infinity;
127
+ return Math.hypot(first.x - second.x, first.y - second.y);
128
+ }
129
+
130
+ export function createRoute(points) {
131
+ const routePoints = points
132
+ .filter(Boolean)
133
+ .map((point) => routePoint(point))
134
+ .filter(Boolean)
135
+ .reduce((deduped, point) => {
136
+ if (!sameRoutePoint(deduped.at(-1), point)) {
137
+ deduped.push(point);
138
+ } else if (point.limiterActive) {
139
+ deduped.at(-1).limiterActive = true;
140
+ }
141
+ return deduped;
142
+ }, []);
143
+ const segments = [];
144
+ let totalLength = 0;
145
+
146
+ for (let index = 0; index < routePoints.length - 1; index += 1) {
147
+ const start = routePoints[index];
148
+ const end = routePoints[index + 1];
149
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
150
+ if (length <= 0.001) continue;
151
+ const heading = Math.atan2(end.y - start.y, end.x - start.x);
152
+ segments.push({
153
+ start,
154
+ end,
155
+ length,
156
+ heading,
157
+ startDistance: totalLength,
158
+ endDistance: totalLength + length,
159
+ limiterActive: Boolean(start.limiterActive && end.limiterActive),
160
+ });
161
+ totalLength += length;
162
+ }
163
+
164
+ let nextLimiterStartDistance = Infinity;
165
+ for (let index = segments.length - 1; index >= 0; index -= 1) {
166
+ const segment = segments[index];
167
+ if (segment.limiterActive) nextLimiterStartDistance = segment.startDistance;
168
+ segment.nextLimiterStartDistance = nextLimiterStartDistance;
169
+ }
170
+
171
+ return {
172
+ points: routePoints,
173
+ segments,
174
+ length: totalLength,
175
+ segmentCursor: 0,
176
+ };
177
+ }
178
+
179
+ function findRouteSegment(route, distanceAlong, endPadding = 0) {
180
+ if (!route?.segments?.length) return null;
181
+ const clampedDistance = clamp(distanceAlong, 0, route.length);
182
+ const segments = route.segments;
183
+ let index = clamp(Math.floor(route.segmentCursor ?? 0), 0, segments.length - 1);
184
+ let segment = segments[index];
185
+
186
+ if (
187
+ clampedDistance >= segment.startDistance - 0.001 &&
188
+ clampedDistance <= segment.endDistance - endPadding
189
+ ) {
190
+ return segment;
191
+ }
192
+
193
+ if (clampedDistance > segment.endDistance - endPadding) {
194
+ while (index < segments.length - 1 && clampedDistance > segments[index].endDistance - endPadding) {
195
+ index += 1;
196
+ }
197
+ } else {
198
+ while (index > 0 && clampedDistance < segments[index].startDistance - 0.001) {
199
+ index -= 1;
200
+ }
201
+ }
202
+
203
+ segment = segments[index];
204
+ if (
205
+ clampedDistance < segment.startDistance - 0.001 ||
206
+ clampedDistance > segment.endDistance - endPadding
207
+ ) {
208
+ segment = segments.find((candidate) => (
209
+ clampedDistance >= candidate.startDistance - 0.001 &&
210
+ clampedDistance <= candidate.endDistance - endPadding
211
+ )) ?? segments.at(-1);
212
+ index = segments.indexOf(segment);
213
+ }
214
+
215
+ route.segmentCursor = Math.max(0, index);
216
+ return segment;
217
+ }
218
+
219
+ export function sampleRoute(route, distanceAlong) {
220
+ if (!route?.segments?.length) return route?.points?.[0] ?? null;
221
+ const clampedDistance = clamp(distanceAlong, 0, route.length);
222
+ const segment = findRouteSegment(route, clampedDistance) ?? route.segments.at(-1);
223
+ const amount = clamp((clampedDistance - segment.startDistance) / segment.length, 0, 1);
224
+
225
+ return {
226
+ x: segment.start.x + (segment.end.x - segment.start.x) * amount,
227
+ y: segment.start.y + (segment.end.y - segment.start.y) * amount,
228
+ heading: segment.heading,
229
+ limiterActive: segment.limiterActive,
230
+ };
231
+ }
232
+
233
+ export function routeLimiterActiveAt(route, distanceAlong) {
234
+ if (!route?.segments?.length) return false;
235
+ const clampedDistance = clamp(distanceAlong, 0, route.length);
236
+ const segment = findRouteSegment(route, clampedDistance, 0.001) ?? route.segments.at(-1);
237
+ return Boolean(segment?.limiterActive);
238
+ }
239
+
240
+ export function distanceToNextLimiterSegment(route, distanceAlong) {
241
+ if (!route?.segments?.length) return Infinity;
242
+ const clampedDistance = clamp(distanceAlong, 0, route.length);
243
+ const segment = findRouteSegment(route, clampedDistance, 0.001);
244
+ if (!segment) return Infinity;
245
+ if (segment.limiterActive && segment.endDistance > clampedDistance + 0.001) return 0;
246
+ return Math.max(0, segment.nextLimiterStartDistance - clampedDistance);
247
+ }
248
+
249
+ function easeInOut(value) {
250
+ const amount = clamp(value, 0, 1);
251
+ return amount * amount * (3 - 2 * amount);
252
+ }
253
+
254
+ function projectPositionToSegment(position, segment) {
255
+ const dx = segment.end.x - segment.start.x;
256
+ const dy = segment.end.y - segment.start.y;
257
+ const lengthSquared = dx * dx + dy * dy;
258
+ const amount = lengthSquared > 0
259
+ ? clamp(((position.x - segment.start.x) * dx + (position.y - segment.start.y) * dy) / lengthSquared, 0, 1)
260
+ : 0;
261
+ const x = segment.start.x + dx * amount;
262
+ const y = segment.start.y + dy * amount;
263
+ const distanceSquared = (position.x - x) ** 2 + (position.y - y) ** 2;
264
+
265
+ return {
266
+ distanceAlong: segment.startDistance + segment.length * amount,
267
+ distanceSquared,
268
+ };
269
+ }
270
+
271
+ export function nearestDistanceOnRoute(route, position, previousDistance = 0) {
272
+ if (!route?.segments?.length) return 0;
273
+ let best = null;
274
+ const backtrackAllowance = metersToSimUnits(90);
275
+
276
+ route.segments.forEach((segment) => {
277
+ if (segment.endDistance < previousDistance - backtrackAllowance) return;
278
+ const projected = projectPositionToSegment(position, segment);
279
+ if (!best || projected.distanceSquared < best.distanceSquared) best = projected;
280
+ });
281
+
282
+ return clamp(best?.distanceAlong ?? previousDistance, 0, route.length);
283
+ }
284
+
285
+ export function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
286
+ const currentDistance = car.raceDistance ?? entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
287
+ const remaining = Math.max(0, entryRaceDistance - currentDistance);
288
+ const currentState = nearestTrackState(track, car, car.progress, { indexMode: 'legacy' });
289
+ const startOffset = currentState.inPitLane ? 0 : currentState.signedOffset ?? 0;
290
+ const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance, {
291
+ indexMode: 'legacy',
292
+ });
293
+ const targetOffset = entryConnectState.signedOffset ?? 0;
294
+ const points = [routePoint(car, car.heading)];
295
+ const straightDistance = remaining > metersToSimUnits(14)
296
+ ? Math.min(remaining * 0.22, metersToSimUnits(42))
297
+ : 0;
298
+ if (straightDistance > metersToSimUnits(1)) {
299
+ points.push(routePoint({
300
+ x: car.x + Math.cos(car.heading) * straightDistance,
301
+ y: car.y + Math.sin(car.heading) * straightDistance,
302
+ }, car.heading));
303
+ }
304
+
305
+ const transitionDistance = Math.max(0, remaining - straightDistance);
306
+ const steps = Math.max(4, Math.ceil(Math.max(transitionDistance, PIT_ENTRY_APPROACH_DISTANCE * 0.45) / metersToSimUnits(74)));
307
+ for (let index = 1; index <= steps; index += 1) {
308
+ const amount = index / steps;
309
+ const distance = currentDistance + straightDistance + transitionDistance * amount;
310
+ const trackPoint = pointAt(track, distance);
311
+ const lateralAmount = easeInOut(amount);
312
+ const offset = startOffset + (targetOffset - startOffset) * lateralAmount;
313
+ const point = offsetTrackPoint(trackPoint, offset);
314
+ points.push(routePoint(point, trackPoint.heading));
315
+ }
316
+
317
+ points.push(routePoint(pitLane.entry.trackConnectPoint, pitLane.entry.trackPoint?.heading));
318
+ return points;
319
+ }
@@ -0,0 +1,43 @@
1
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
2
+ import { normalizePitCrewStats } from './pitState.js';
3
+ import { beginPitPenaltyService } from './pitPenaltyService.js';
4
+ import { beginTireService } from './pitTireService.js';
5
+ import { syncPitCarKinematics } from './pitKinematics.js';
6
+ import { getPitBoxRaceDistance } from './pitOccupancy.js';
7
+
8
+ export { normalizePitCrewStats };
9
+ export {
10
+ getPitBoxRaceDistance,
11
+ getPitStopBox,
12
+ isPitServiceAreaOccupied,
13
+ isPitServiceBusy,
14
+ isPitServiceQueueOccupied,
15
+ } from './pitOccupancy.js';
16
+ export { beginPitQueue, releasePitQueue } from './pitQueue.js';
17
+ export { calculatePitServiceProfile } from './pitServiceProfile.js';
18
+ export { beginPitPenaltyService, completePitPenaltyService, getPitServicePenalties } from './pitPenaltyService.js';
19
+ export { beginTireService, completePitService, finishPitExit } from './pitTireService.js';
20
+ export { advancePitService, applyPitRoutePosition } from './pitRouteMovement.js';
21
+
22
+ export function beginPitService(sim, car, box) {
23
+ const stop = car.pitStop;
24
+ stop.status = 'servicing';
25
+ stop.route = null;
26
+ stop.routeProgress = 0;
27
+ car.x = box.center.x;
28
+ car.y = box.center.y;
29
+ car.heading = sim.track.pitLane.mainLane.heading;
30
+ car.speed = 0;
31
+ car.throttle = 0;
32
+ car.brake = 1;
33
+ car.steeringAngle = 0;
34
+ car.yawRate = 0;
35
+ car.turnRadius = Infinity;
36
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
37
+ applyWheelSurfaceState(car, sim.track);
38
+ car.progress = car.trackState.distance;
39
+ car.raceDistance = getPitBoxRaceDistance(sim, stop, box);
40
+
41
+ if (beginPitPenaltyService(sim, car)) return;
42
+ beginTireService(sim, car);
43
+ }
@@ -0,0 +1,21 @@
1
+ import { kphToSimSpeed, metersToSimUnits } from '../units.js';
2
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
3
+
4
+ export const PIT_ROUTE_FINISH_DISTANCE = metersToSimUnits(8.5);
5
+ export const PIT_QUEUE_RELEASE_FINISH_DISTANCE = metersToSimUnits(4);
6
+ export const PIT_QUEUE_CAPTURE_DISTANCE = metersToSimUnits(2.5);
7
+ export const PIT_ENTRY_BOX_CAPTURE_DISTANCE = metersToSimUnits(20);
8
+ export const PIT_BOX_STOP_SPEED = kphToSimSpeed(35);
9
+ export const PIT_QUEUE_CAPTURE_SPEED = kphToSimSpeed(140);
10
+ export const PIT_QUEUE_RELEASE_SPEED = kphToSimSpeed(30);
11
+ export const PIT_EXIT_RELEASE_SPEED_KPH = 95;
12
+ export const PIT_BOX_APPROACH_DISTANCE = metersToSimUnits(34);
13
+ export const PIT_LIMITER_BRAKE_DISTANCE = metersToSimUnits(295);
14
+ export const PIT_LIMITER_APPROACH_SPEED_SLOPE = 0.045;
15
+ export const PIT_ENTRY_CONNECTOR_OVERSPEED_KPH = 75;
16
+ export const PIT_SERVICE_CLEAR_DISTANCE = VEHICLE_LIMITS.carLength * 0.9;
17
+
18
+ export function pointDistance(first, second) {
19
+ if (!first || !second) return Infinity;
20
+ return Math.hypot(first.x - second.x, first.y - second.y);
21
+ }
@@ -0,0 +1,45 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { normalizePitCrewStats } from './pitState.js';
3
+ import { getPitStopBox } from './pitOccupancy.js';
4
+
5
+ export function calculatePitServiceProfile(sim, car) {
6
+ const pitStops = sim.rules.modules?.pitStops ?? {};
7
+ const variability = pitStops.variability ?? {};
8
+ const baseSeconds = Math.max(0, Number(pitStops.defaultStopSeconds) || 2.8);
9
+ const box = getPitStopBox(sim, car?.pitStop);
10
+ const team = sim.track.pitLane?.teams?.find((entry) => entry.id === car?.pitStop?.teamId);
11
+ const pitCrew = normalizePitCrewStats(
12
+ box?.pitCrew ?? team?.pitCrew ?? car?.team?.pitCrew ?? car?.team?.pitCrewStats,
13
+ );
14
+ const profile = {
15
+ baseSeconds,
16
+ seconds: baseSeconds,
17
+ perfect: Boolean(variability.perfect),
18
+ variabilityEnabled: Boolean(variability.enabled),
19
+ teamId: car?.pitStop?.teamId ?? box?.teamId ?? null,
20
+ pitCrew,
21
+ speedDeltaSeconds: 0,
22
+ consistencyDeltaSeconds: 0,
23
+ issueDelaySeconds: 0,
24
+ issue: null,
25
+ };
26
+
27
+ if (!variability.enabled || variability.perfect) return profile;
28
+
29
+ const speedImpact = Math.max(0, Number(variability.speedImpactSeconds) || 0);
30
+ const jitterImpact = Math.max(0, Number(variability.consistencyJitterSeconds) || 0);
31
+ const issueChance = clamp(Number(variability.issueChance) || 0, 0, 1);
32
+ const issueMaxDelay = Math.max(0, Number(variability.issueMaxDelaySeconds) || 0);
33
+ profile.speedDeltaSeconds = (0.5 - pitCrew.speed) * speedImpact;
34
+ profile.consistencyDeltaSeconds = (sim.random() - 0.5) * (1 - pitCrew.consistency) * jitterImpact;
35
+ const effectiveIssueChance = issueChance * (1 - pitCrew.reliability);
36
+ if (sim.random() < effectiveIssueChance) {
37
+ profile.issueDelaySeconds = 0.35 + sim.random() * issueMaxDelay * (1 - pitCrew.reliability);
38
+ profile.issue = 'slow-stop';
39
+ }
40
+ profile.seconds = Math.max(
41
+ Math.min(1.6, baseSeconds),
42
+ baseSeconds + profile.speedDeltaSeconds + profile.consistencyDeltaSeconds + profile.issueDelaySeconds,
43
+ );
44
+ return profile;
45
+ }
@@ -0,0 +1,61 @@
1
+ import { finiteOrNull } from '../vehicle/vehicleSnapshots.js';
2
+
3
+ export function pitLaneStatusSnapshot(raceControl, pitLane, pitStops) {
4
+ const enabled = Boolean(pitLane?.enabled && pitStops?.enabled);
5
+ const redFlag = Boolean(raceControl.redFlag);
6
+ const open = enabled && Boolean(raceControl.pitLaneOpen) && !redFlag;
7
+ return {
8
+ enabled,
9
+ open,
10
+ reason: !enabled ? 'unavailable' : redFlag ? 'red-flag' : open ? 'open' : 'closed',
11
+ color: open ? 'green' : redFlag ? 'yellow' : 'red',
12
+ light: open ? '#22c55e' : redFlag ? '#facc15' : '#ef4444',
13
+ };
14
+ }
15
+
16
+ export function serializePitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE) {
17
+ if (!pitStop) return null;
18
+ return {
19
+ status: pitStop.status,
20
+ intent: normalizePitIntent(pitStop.intent) ?? PIT_INTENT_NONE,
21
+ phase: pitStop.phase ?? null,
22
+ boxIndex: pitStop.boxIndex,
23
+ boxId: pitStop.boxId,
24
+ garageBoxIndex: pitStop.garageBoxIndex ?? null,
25
+ garageBoxId: pitStop.garageBoxId ?? null,
26
+ teamId: pitStop.teamId ?? null,
27
+ teamColor: pitStop.teamColor ?? null,
28
+ stopsCompleted: pitStop.stopsCompleted ?? 0,
29
+ queueingForService: Boolean(pitStop.queueingForService),
30
+ plannedRaceDistance: finiteOrNull(pitStop.plannedRaceDistance),
31
+ entryRaceDistance: finiteOrNull(pitStop.entryRaceDistance),
32
+ serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
33
+ penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
34
+ penaltyServiceTotalSeconds: finiteOrNull(pitStop.penaltyServiceTotal),
35
+ servingPenaltyIds: [...(pitStop.servingPenaltyIds ?? [])],
36
+ targetTire: pitStop.targetTire ?? null,
37
+ serviceProfile: pitStop.serviceProfile ? { ...pitStop.serviceProfile } : null,
38
+ };
39
+ }
40
+
41
+ export function serializeRenderPitStop(pitStop) {
42
+ if (!pitStop) return null;
43
+ return {
44
+ phase: pitStop.phase ?? null,
45
+ serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
46
+ penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
47
+ };
48
+ }
49
+
50
+ export function serializeObservationPitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE) {
51
+ if (!pitStop) return null;
52
+ return {
53
+ status: pitStop.status,
54
+ intent: normalizePitIntent(pitStop.intent) ?? PIT_INTENT_NONE,
55
+ phase: pitStop.phase ?? null,
56
+ targetTire: pitStop.targetTire ?? null,
57
+ serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
58
+ penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
59
+ stopsCompleted: pitStop.stopsCompleted ?? 0,
60
+ };
61
+ }