@inventure71/paddockjs 1.0.0 → 2.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,120 @@
1
+ export const PARTICIPANT_INTERACTION_PROFILES = Object.freeze({
2
+ normal: Object.freeze({
3
+ profile: 'normal',
4
+ collidable: true,
5
+ detectableByRays: true,
6
+ detectableAsNearby: true,
7
+ blocksPitLane: true,
8
+ affectsRaceOrder: true,
9
+ }),
10
+ 'isolated-training': Object.freeze({
11
+ profile: 'isolated-training',
12
+ collidable: false,
13
+ detectableByRays: false,
14
+ detectableAsNearby: false,
15
+ blocksPitLane: false,
16
+ affectsRaceOrder: true,
17
+ }),
18
+ 'batch-training': Object.freeze({
19
+ profile: 'batch-training',
20
+ collidable: false,
21
+ detectableByRays: false,
22
+ detectableAsNearby: false,
23
+ blocksPitLane: false,
24
+ affectsRaceOrder: false,
25
+ }),
26
+ 'phantom-race': Object.freeze({
27
+ profile: 'phantom-race',
28
+ collidable: false,
29
+ detectableByRays: true,
30
+ detectableAsNearby: true,
31
+ blocksPitLane: false,
32
+ affectsRaceOrder: true,
33
+ }),
34
+ 'time-trial-overlay': Object.freeze({
35
+ profile: 'time-trial-overlay',
36
+ collidable: false,
37
+ detectableByRays: false,
38
+ detectableAsNearby: false,
39
+ blocksPitLane: false,
40
+ affectsRaceOrder: false,
41
+ }),
42
+ });
43
+
44
+ const INTERACTION_FLAGS = Object.freeze([
45
+ 'collidable',
46
+ 'detectableByRays',
47
+ 'detectableAsNearby',
48
+ 'blocksPitLane',
49
+ 'affectsRaceOrder',
50
+ ]);
51
+
52
+ export function normalizeParticipantInteractions(options = {}) {
53
+ const defaultProfile = resolveProfileName(options.defaultProfile);
54
+ const defaultInteraction = { ...PARTICIPANT_INTERACTION_PROFILES[defaultProfile] };
55
+ const drivers = {};
56
+ Object.entries(options.drivers ?? {}).forEach(([driverId, override]) => {
57
+ drivers[driverId] = resolveInteraction(override, defaultProfile);
58
+ });
59
+ return {
60
+ defaultProfile,
61
+ defaultInteraction,
62
+ drivers,
63
+ };
64
+ }
65
+
66
+ export function attachParticipantInteractions(cars = [], options = {}) {
67
+ const normalized = normalizeParticipantInteractions(options);
68
+ cars.forEach((car) => {
69
+ car.interaction = normalized.drivers[car.id] ?? { ...normalized.defaultInteraction };
70
+ });
71
+ return normalized;
72
+ }
73
+
74
+ export function resolveInteraction(override = {}, fallbackProfile = 'normal') {
75
+ const profile = resolveProfileName(override?.profile ?? fallbackProfile);
76
+ const resolved = { ...PARTICIPANT_INTERACTION_PROFILES[profile] };
77
+ INTERACTION_FLAGS.forEach((flag) => {
78
+ if (override?.[flag] != null) resolved[flag] = Boolean(override[flag]);
79
+ });
80
+ return resolved;
81
+ }
82
+
83
+ export function canCollide(first, second) {
84
+ return isCollidable(first) && isCollidable(second);
85
+ }
86
+
87
+ export function isCollidable(car) {
88
+ if (car?.destroyed || car?.outOfRace) return false;
89
+ return car?.interaction?.collidable !== false;
90
+ }
91
+
92
+ export function isRayDetectable(car) {
93
+ if (car?.destroyed || car?.outOfRace) return false;
94
+ return car?.interaction?.detectableByRays !== false;
95
+ }
96
+
97
+ export function isNearbyDetectable(car) {
98
+ if (car?.destroyed || car?.outOfRace) return false;
99
+ return car?.interaction?.detectableAsNearby !== false;
100
+ }
101
+
102
+ export function blocksPitLane(car) {
103
+ if (car?.destroyed || car?.outOfRace) return false;
104
+ return car?.interaction?.blocksPitLane !== false;
105
+ }
106
+
107
+ export function affectsRaceOrder(car) {
108
+ if (car?.destroyed || car?.outOfRace) return false;
109
+ return car?.interaction?.affectsRaceOrder !== false;
110
+ }
111
+
112
+ export function serializeParticipantInteraction(interaction) {
113
+ const normalized = resolveInteraction(interaction ?? {}, interaction?.profile ?? 'normal');
114
+ return { ...normalized };
115
+ }
116
+
117
+ function resolveProfileName(profile) {
118
+ const key = String(profile ?? 'normal');
119
+ return PARTICIPANT_INTERACTION_PROFILES[key] ? key : 'normal';
120
+ }
@@ -0,0 +1,234 @@
1
+ import { pitLaneStatusSnapshot } from './pitSnapshots.js';
2
+ import {
3
+ firstDifferentCompound,
4
+ normalizePitIntent,
5
+ shouldStartPitStopForSimulation,
6
+ updateAutomaticPitIntentForSimulation,
7
+ } from './pitIntent.js';
8
+ import {
9
+ advancePitService,
10
+ applyPitRoutePosition,
11
+ beginPitQueue,
12
+ beginPitService,
13
+ finishPitExit,
14
+ getPitBoxRaceDistance,
15
+ getPitStopBox,
16
+ isPitServiceBusy,
17
+ isPitServiceQueueOccupied,
18
+ releasePitQueue,
19
+ } from './pitService.js';
20
+ import {
21
+ createPitApproachPoints,
22
+ createRoute,
23
+ pitDriveLaneOffset,
24
+ pitMainLanePointAt,
25
+ routePoint,
26
+ } from './pitRouting.js';
27
+ import { metersToSimUnits } from '../units.js';
28
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
29
+
30
+ const PIT_ENTRY_APPROACH_DISTANCE = metersToSimUnits(250);
31
+ const PIT_BOX_APPROACH_DISTANCE = metersToSimUnits(34);
32
+ const PIT_SERVICE_QUEUE_FALLBACK_GAP = metersToSimUnits(48);
33
+ const PIT_ENTRY_LATE_THRESHOLD_DISTANCE = metersToSimUnits(90);
34
+
35
+ export function isCarInActivePitStop(sim, car) {
36
+ return Boolean(car?.pitStop && car.pitStop.status !== 'pending' && car.pitStop.status !== 'completed');
37
+ }
38
+
39
+ export function schedulePitStopAtNextEntry(sim, car, stop) {
40
+ const pitLane = sim.track.pitLane;
41
+ if (!stop || !pitLane?.entry || !Number.isFinite(sim.track.length) || sim.track.length <= 0) return false;
42
+ const raceDistance = car.raceDistance ?? 0;
43
+ const entryOffset = pitLane.entry.distanceFromStart ?? 0;
44
+ let lapBase = Math.floor((raceDistance - entryOffset) / sim.track.length) * sim.track.length;
45
+ let entryRaceDistance = lapBase + entryOffset;
46
+
47
+ while (raceDistance > entryRaceDistance + PIT_ENTRY_LATE_THRESHOLD_DISTANCE) {
48
+ lapBase += sim.track.length;
49
+ entryRaceDistance = lapBase + entryOffset;
50
+ }
51
+
52
+ stop.lapBase = lapBase;
53
+ stop.entryRaceDistance = entryRaceDistance;
54
+ stop.plannedRaceDistance = entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
55
+ return true;
56
+ }
57
+
58
+ export function rearmCompletedPitStop(sim, car, stop) {
59
+ if (!stop || stop.status !== 'completed') return false;
60
+ stop.status = 'pending';
61
+ stop.phase = null;
62
+ stop.route = null;
63
+ stop.routeProgress = 0;
64
+ stop.routeStartRaceDistance = null;
65
+ stop.routeEndRaceDistance = null;
66
+ stop.serviceRemaining = 0;
67
+ stop.penaltyServiceRemaining = 0;
68
+ stop.penaltyServiceTotal = 0;
69
+ stop.servingPenaltyIds = [];
70
+ stop.queueingForService = false;
71
+ stop.serviceProfile = null;
72
+ stop.targetTire = stop.targetTire ?? firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
73
+ return schedulePitStopAtNextEntry(sim, car, stop);
74
+ }
75
+
76
+ export function canStartPitStop(sim, car) {
77
+ const stop = car.pitStop;
78
+ const pitStops = sim.rules.modules?.pitStops;
79
+ if (!stop || !pitStops?.enabled) return false;
80
+ const active = sim.cars.filter((candidate) => candidate !== car && isCarInActivePitStop(sim, candidate));
81
+ const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
82
+ if (active.length >= maxConcurrentPitLaneCars) return false;
83
+
84
+ if (!pitStops.doubleStacking && stop.teamId) {
85
+ const box = getPitStopBox(sim, stop);
86
+ if (isPitServiceQueueOccupied(sim, car, box)) return false;
87
+ }
88
+
89
+ const minimumGap = Math.max(0, pitStops.minimumPitLaneGap ?? 0);
90
+ const candidateDistance = car.raceDistance ?? 0;
91
+ return active.every((candidate) => {
92
+ const gap = (candidate.raceDistance ?? 0) - candidateDistance;
93
+ return gap < 0 || gap >= minimumGap;
94
+ });
95
+ }
96
+
97
+ export function shouldStartPitStop(sim, car) {
98
+ return shouldStartPitStopForSimulation(sim, car);
99
+ }
100
+
101
+ export function updateAutomaticPitIntent(sim, car) {
102
+ updateAutomaticPitIntentForSimulation(sim, car);
103
+ }
104
+
105
+ export function isPitLaneOpenForStops(sim) {
106
+ return pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops).open;
107
+ }
108
+
109
+ export function pitEntryLateThresholdDistance() {
110
+ return PIT_ENTRY_LATE_THRESHOLD_DISTANCE;
111
+ }
112
+
113
+ export function startPitStop(sim, car) {
114
+ const stop = car.pitStop;
115
+ const box = getPitStopBox(sim, stop);
116
+ const pitLane = sim.track.pitLane;
117
+ if (!stop || !box || !pitLane?.enabled) return false;
118
+
119
+ car.gridLocked = false;
120
+ car.drsActive = false;
121
+ car.drsEligible = false;
122
+ car.drsZoneId = null;
123
+ car.drsZoneEnabled = false;
124
+ car.canAttack = false;
125
+ stop.targetTire = stop.targetTire ?? firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
126
+
127
+ const currentState = applyWheelSurfaceState(car, sim.track).representativeState;
128
+ sim.events.unshift({
129
+ type: 'pit-entry',
130
+ at: sim.time,
131
+ carId: car.id,
132
+ boxId: box.id,
133
+ teamId: box.teamId ?? null,
134
+ });
135
+
136
+ if (currentState.surface === 'pit-box' && currentState.pitBoxId === box.id) {
137
+ beginPitService(sim, car, box);
138
+ return true;
139
+ }
140
+
141
+ const hasServiceQueue = Boolean(box.queuePoint);
142
+ const shouldStageForService = hasServiceQueue;
143
+ const queueDistanceAlongLane = box.queueDistanceAlongLane ??
144
+ Math.max(0, box.distanceAlongLane - PIT_SERVICE_QUEUE_FALLBACK_GAP);
145
+ const stopDistanceAlongLane = shouldStageForService
146
+ ? queueDistanceAlongLane
147
+ : box.distanceAlongLane;
148
+ const serviceTarget = shouldStageForService ? box.queuePoint : box.center;
149
+ const driveLaneOffset = pitDriveLaneOffset(pitLane);
150
+ const boxApproachDistance = Math.max(
151
+ 0,
152
+ (hasServiceQueue ? queueDistanceAlongLane : stopDistanceAlongLane) - PIT_BOX_APPROACH_DISTANCE,
153
+ );
154
+ const route = createRoute([
155
+ ...createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance),
156
+ ...(pitLane.entry.roadCenterline ?? []).map((point) => routePoint(point)),
157
+ routePoint(pitMainLanePointAt(pitLane, 0, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
158
+ routePoint(pitMainLanePointAt(pitLane, boxApproachDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
159
+ routePoint(serviceTarget, pitLane.mainLane.heading, { limiterActive: true }),
160
+ ]);
161
+ stop.status = 'entering';
162
+ stop.phase = 'entry';
163
+ stop.queueingForService = shouldStageForService;
164
+ stop.route = route;
165
+ stop.routeProgress = 0;
166
+ stop.routeStartRaceDistance = car.raceDistance ?? stop.plannedRaceDistance;
167
+ stop.routeEndRaceDistance = getPitBoxRaceDistance(sim, stop, {
168
+ ...box,
169
+ distanceAlongLane: stopDistanceAlongLane,
170
+ });
171
+ return true;
172
+ }
173
+
174
+ export function advancePitStopCar(sim, car, delta) {
175
+ const pitStops = sim.rules.modules?.pitStops;
176
+ const stop = car.pitStop;
177
+ if (!pitStops?.enabled || !stop) return false;
178
+ if (stop.status === 'pending' || stop.status === 'completed') updateAutomaticPitIntent(sim, car);
179
+ if (stop.status === 'completed') return false;
180
+ if (stop.status === 'pending' && !shouldStartPitStop(sim, car)) return false;
181
+ if (stop.status === 'pending') startPitStop(sim, car);
182
+
183
+ if (stop.status === 'entering') {
184
+ const finishedRoute = applyPitRoutePosition(sim, car, delta);
185
+ const box = getPitStopBox(sim, stop);
186
+ if (finishedRoute && box) {
187
+ if (stop.queueingForService) {
188
+ if (!isPitServiceBusy(sim, car, box) && !isPitServiceQueueOccupied(sim, car, box)) {
189
+ releasePitQueue(sim, car, box, { fromCurrent: true });
190
+ } else {
191
+ beginPitQueue(sim, car, box);
192
+ }
193
+ }
194
+ else beginPitService(sim, car, box);
195
+ }
196
+ car.contactCooldown = Math.max(0, car.contactCooldown - delta);
197
+ return true;
198
+ }
199
+
200
+ if (stop.status === 'queued') {
201
+ const box = getPitStopBox(sim, stop);
202
+ if (!box) return true;
203
+ car.previousX = car.x;
204
+ car.previousY = car.y;
205
+ car.previousHeading = car.heading;
206
+ car.x = box.queuePoint.x;
207
+ car.y = box.queuePoint.y;
208
+ car.heading = sim.track.pitLane.mainLane.heading;
209
+ car.speed = 0;
210
+ car.throttle = 0;
211
+ car.brake = 1;
212
+ applyWheelSurfaceState(car, sim.track);
213
+ car.progress = car.trackState.distance;
214
+ car.raceDistance = getPitBoxRaceDistance(sim, stop, {
215
+ ...box,
216
+ distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
217
+ });
218
+ if (!isPitServiceBusy(sim, car, box)) releasePitQueue(sim, car, box);
219
+ return true;
220
+ }
221
+
222
+ if (stop.status === 'servicing') {
223
+ return advancePitService(sim, car, delta);
224
+ }
225
+
226
+ if (stop.status === 'exiting') {
227
+ const finishedRoute = applyPitRoutePosition(sim, car, delta);
228
+ if (finishedRoute) finishPitExit(sim, car);
229
+ car.contactCooldown = Math.max(0, car.contactCooldown - delta);
230
+ return true;
231
+ }
232
+
233
+ return false;
234
+ }
@@ -0,0 +1,92 @@
1
+ export const PIT_INTENT_NONE = 0;
2
+ export const PIT_INTENT_IF_FREE = 1;
3
+ export const PIT_INTENT_COMMITTED = 2;
4
+
5
+ export function firstDifferentCompound(currentTire, compounds) {
6
+ const available = Array.isArray(compounds) && compounds.length ? compounds : ['S', 'M', 'H'];
7
+ return available.find((compound) => compound !== currentTire) ?? currentTire;
8
+ }
9
+
10
+ export function normalizePitCompound(value, compounds) {
11
+ if (value == null || value === '') return null;
12
+ const available = Array.isArray(compounds) && compounds.length ? compounds : ['S', 'M', 'H'];
13
+ return available.includes(value) ? value : null;
14
+ }
15
+
16
+ export function normalizePitIntent(value) {
17
+ const number = Number(value);
18
+ if (!Number.isInteger(number) || number < 0 || number > 2) return null;
19
+ return number;
20
+ }
21
+
22
+ export function setPitIntentForSimulation(sim, id, intent, targetCompound = undefined) {
23
+ const request = intent && typeof intent === 'object'
24
+ ? intent
25
+ : { intent, targetCompound };
26
+ const nextIntent = normalizePitIntent(request.intent ?? request.pitIntent);
27
+ if (nextIntent == null) return false;
28
+ const car = sim.cars.find((item) => item.id === id);
29
+ const stop = car?.pitStop;
30
+ const pitStops = sim.rules.modules?.pitStops;
31
+ if (!car || !stop || !pitStops?.enabled || !sim.track.pitLane?.enabled) return false;
32
+ if (sim.isCarInActivePitStop(car)) return false;
33
+
34
+ const compoundRequest = request.targetCompound ??
35
+ request.compound ??
36
+ request.pitCompound ??
37
+ request.pitTargetCompound ??
38
+ request.targetTire;
39
+ const hasCompoundRequest = compoundRequest != null && compoundRequest !== '';
40
+ if (hasCompoundRequest) {
41
+ const targetTire = normalizePitCompound(compoundRequest, sim.rules.modules?.tireStrategy?.compounds);
42
+ if (!targetTire) return false;
43
+ stop.targetTire = targetTire;
44
+ } else if (nextIntent !== 0 && (!stop.targetTire || stop.status === 'completed')) {
45
+ stop.targetTire = firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
46
+ } else if (nextIntent === 0) {
47
+ stop.targetTire = firstDifferentCompound(car.tire, sim.rules.modules?.tireStrategy?.compounds);
48
+ }
49
+
50
+ stop.intent = nextIntent;
51
+ if (stop.status === 'pending' && nextIntent !== 0) {
52
+ sim.schedulePitStopAtNextEntry(car, stop);
53
+ } else if (stop.status === 'completed' && nextIntent !== 0) {
54
+ sim.rearmCompletedPitStop(car, stop);
55
+ }
56
+ return true;
57
+ }
58
+
59
+ export function shouldStartPitStopForSimulation(sim, car) {
60
+ const stop = car.pitStop;
61
+ if (!stop || stop.status !== 'pending' || car.finished) return false;
62
+ if (!sim.isPitLaneOpenForStops()) return false;
63
+ const intent = normalizePitIntent(stop.intent) ?? 0;
64
+ if (intent === 0) return false;
65
+ const raceDistance = car.raceDistance ?? 0;
66
+ if (raceDistance > stop.entryRaceDistance + sim.pitEntryLateThresholdDistance()) {
67
+ sim.schedulePitStopAtNextEntry(car, stop);
68
+ return false;
69
+ }
70
+ return raceDistance >= stop.plannedRaceDistance &&
71
+ (intent === 2 || sim.canStartPitStop(car));
72
+ }
73
+
74
+ export function updateAutomaticPitIntentForSimulation(sim, car) {
75
+ const stop = car.pitStop;
76
+ const pitStops = sim.rules.modules?.pitStops;
77
+ if (!stop || !['pending', 'completed'].includes(stop.status) || !pitStops?.enabled || car.finished) return;
78
+ if (car.automaticPitIntentEnabled === false) return;
79
+ const tireEnergy = Number(car.tireEnergy ?? 100);
80
+ const requestThreshold = Number(pitStops.tirePitRequestThresholdPercent ?? 50);
81
+ const commitThreshold = Number(pitStops.tirePitCommitThresholdPercent ?? 30);
82
+ if (!Number.isFinite(tireEnergy)) return;
83
+
84
+ let nextIntent = 0;
85
+ if (tireEnergy < commitThreshold) nextIntent = 2;
86
+ else if (tireEnergy < requestThreshold) nextIntent = 1;
87
+ if (nextIntent === 0 || nextIntent <= (normalizePitIntent(stop.intent) ?? 0)) return;
88
+
89
+ stop.intent = nextIntent;
90
+ if (stop.status === 'completed') sim.rearmCompletedPitStop(car, stop);
91
+ else sim.schedulePitStopAtNextEntry(car, stop);
92
+ }
@@ -0,0 +1,47 @@
1
+ import { blocksPitLane } from '../participants/participantInteractions.js';
2
+ import { PIT_SERVICE_CLEAR_DISTANCE, pointDistance } from './pitServiceConstants.js';
3
+
4
+ export function getPitStopBox(sim, stop) {
5
+ return sim.track.pitLane?.serviceAreas?.find((box) => box.id === stop?.boxId) ??
6
+ sim.track.pitLane?.boxes?.find((box) => box.id === stop?.boxId) ??
7
+ null;
8
+ }
9
+
10
+ export function isPitServiceAreaOccupied(sim, candidate, box, clearDistance) {
11
+ if (!blocksPitLane(candidate)) return false;
12
+ const status = candidate?.pitStop?.status;
13
+ const phase = candidate?.pitStop?.phase;
14
+ if (!status || !box) return false;
15
+ if (status === 'servicing') return true;
16
+ if (status === 'entering' && phase === 'queue-release') return true;
17
+ if (status === 'exiting') return pointDistance(candidate, box.center) < clearDistance;
18
+ return false;
19
+ }
20
+
21
+ export function isPitServiceBusy(sim, car, box, clearDistance = PIT_SERVICE_CLEAR_DISTANCE) {
22
+ return sim.cars.some((candidate) => (
23
+ candidate !== car &&
24
+ blocksPitLane(candidate) &&
25
+ candidate.pitStop?.boxId === box?.id &&
26
+ isPitServiceAreaOccupied(sim, candidate, box, clearDistance)
27
+ ));
28
+ }
29
+
30
+ export function isPitServiceQueueOccupied(sim, car, box) {
31
+ return sim.cars.some((candidate) => (
32
+ candidate !== car &&
33
+ blocksPitLane(candidate) &&
34
+ candidate.pitStop?.boxId === box?.id &&
35
+ (
36
+ candidate.pitStop?.status === 'queued' ||
37
+ Boolean(candidate.pitStop?.queueingForService)
38
+ )
39
+ ));
40
+ }
41
+
42
+ export function getPitBoxRaceDistance(sim, stop, box) {
43
+ const pitLane = sim.track.pitLane;
44
+ const amount = pitLane?.mainLane?.length > 0 ? box.distanceAlongLane / pitLane.mainLane.length : 0;
45
+ return stop.lapBase + pitLane.entry.distanceFromStart +
46
+ (pitLane.exit.distanceFromStart - pitLane.entry.distanceFromStart) * amount;
47
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ getPitServicePenaltySeconds,
3
+ isPenaltyPitServiceable,
4
+ servePitPenaltyRecord,
5
+ } from '../rules/penaltyLedger.js';
6
+ import { beginTireService } from './pitTireService.js';
7
+
8
+ export function beginPitPenaltyService(sim, car) {
9
+ const stop = car.pitStop;
10
+ const penalties = getPitServicePenalties(sim, car.id);
11
+ if (!stop || !penalties.length) return false;
12
+
13
+ const totalSeconds = penalties.reduce((total, penalty) => (
14
+ total + getPitServicePenaltySeconds(penalty)
15
+ ), 0);
16
+ stop.servingPenaltyIds = penalties.map((penalty) => penalty.id);
17
+ stop.penaltyServiceTotal = totalSeconds;
18
+ stop.penaltyServiceRemaining = totalSeconds;
19
+
20
+ if (totalSeconds <= 0) {
21
+ completePitPenaltyService(sim, car);
22
+ return true;
23
+ }
24
+
25
+ stop.phase = 'penalty';
26
+ stop.serviceRemaining = totalSeconds;
27
+ sim.events.unshift({
28
+ type: 'pit-penalty-service-start',
29
+ at: sim.time,
30
+ carId: car.id,
31
+ penaltyIds: [...stop.servingPenaltyIds],
32
+ penaltyServiceSeconds: totalSeconds,
33
+ });
34
+ return true;
35
+ }
36
+
37
+ export function completePitPenaltyService(sim, car) {
38
+ const stop = car.pitStop;
39
+ if (!stop) return;
40
+ const servedIds = [...(stop.servingPenaltyIds ?? [])];
41
+ servedIds.forEach((penaltyId) => {
42
+ const penalty = sim.penalties.find((entry) => entry.id === penaltyId);
43
+ const beforeStatus = penalty?.status;
44
+ const result = servePitPenaltyRecord(penalty, sim.time);
45
+ if (result && beforeStatus !== result.status) {
46
+ sim.events.unshift({
47
+ type: 'penalty-served',
48
+ at: sim.time,
49
+ penaltyId: result.id,
50
+ driverId: result.driverId,
51
+ serviceType: result.serviceType,
52
+ serviceContext: 'pit-stop',
53
+ });
54
+ }
55
+ });
56
+ stop.penaltyServiceRemaining = 0;
57
+ stop.serviceRemaining = 0;
58
+ sim.events.unshift({
59
+ type: 'pit-penalty-service-complete',
60
+ at: sim.time,
61
+ carId: car.id,
62
+ penaltyIds: servedIds,
63
+ });
64
+ beginTireService(sim, car);
65
+ }
66
+
67
+ export function getPitServicePenalties(sim, driverId) {
68
+ return sim.penalties.filter((penalty) => (
69
+ penalty.driverId === driverId && isPenaltyPitServiceable(penalty)
70
+ ));
71
+ }
@@ -0,0 +1,55 @@
1
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
2
+ import { createRoute, routePoint } from './pitRouting.js';
3
+ import { PIT_QUEUE_RELEASE_SPEED } from './pitServiceConstants.js';
4
+ import { getPitBoxRaceDistance } from './pitOccupancy.js';
5
+
6
+ export function beginPitQueue(sim, car, box) {
7
+ const stop = car.pitStop;
8
+ if (!stop || !box?.queuePoint) return false;
9
+ stop.status = 'queued';
10
+ stop.phase = 'queue';
11
+ stop.route = null;
12
+ stop.routeProgress = 0;
13
+ stop.queueingForService = false;
14
+ car.x = box.queuePoint.x;
15
+ car.y = box.queuePoint.y;
16
+ car.heading = sim.track.pitLane.mainLane.heading;
17
+ car.speed = 0;
18
+ car.throttle = 0;
19
+ car.brake = 1;
20
+ applyWheelSurfaceState(car, sim.track);
21
+ car.progress = car.trackState.distance;
22
+ car.raceDistance = getPitBoxRaceDistance(sim, stop, {
23
+ ...box,
24
+ distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
25
+ });
26
+ return true;
27
+ }
28
+
29
+ export function releasePitQueue(sim, car, box, { fromCurrent = false } = {}) {
30
+ const stop = car.pitStop;
31
+ if (!stop || !box?.center) return false;
32
+ const route = createRoute([
33
+ ...(fromCurrent ? [routePoint(car, car.heading, { limiterActive: true })] : []),
34
+ routePoint(box.queuePoint, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
35
+ routePoint(box.center, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
36
+ ]);
37
+ stop.status = 'entering';
38
+ stop.phase = 'queue-release';
39
+ stop.queueingForService = false;
40
+ stop.route = route;
41
+ stop.routeProgress = 0;
42
+ stop.routeStartRaceDistance = fromCurrent
43
+ ? car.raceDistance ?? getPitBoxRaceDistance(sim, stop, {
44
+ ...box,
45
+ distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
46
+ })
47
+ : getPitBoxRaceDistance(sim, stop, {
48
+ ...box,
49
+ distanceAlongLane: box.queueDistanceAlongLane ?? box.distanceAlongLane,
50
+ });
51
+ stop.routeEndRaceDistance = getPitBoxRaceDistance(sim, stop, box);
52
+ return true;
53
+ }
54
+
55
+ export { PIT_QUEUE_RELEASE_SPEED };