@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
@@ -0,0 +1,48 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { simUnitsToMeters } from '../units.js';
3
+
4
+ const JUDGEMENT_WAVELENGTH_METERS = 180;
5
+ const DISTANCE_PHASE_RATE = Math.PI * 2 / JUDGEMENT_WAVELENGTH_METERS;
6
+
7
+ function finiteOr(value, fallback) {
8
+ return Number.isFinite(value) ? value : fallback;
9
+ }
10
+
11
+ function normalizedSkill(value, minimum, maximum, neutral) {
12
+ return clamp((finiteOr(value, neutral) - minimum) / (maximum - minimum), 0, 1);
13
+ }
14
+
15
+ // Input skills are DriverData's constructor values, not its 0..100 ratings.
16
+ // Ability controls available precision/margin; willingness to risk it is a
17
+ // separate personality input. This policy does not change physical grip.
18
+ export function racingDriverProfile(car = {}) {
19
+ const pace = normalizedSkill(car.pace, 0.92, 1.08, 1);
20
+ const racecraft = normalizedSkill(car.racecraft, 0.62, 0.94, 0.78);
21
+ const ability = pace * 0.6 + racecraft * 0.4;
22
+ const consistency = normalizedSkill(car.consistency, 0.59, 0.91, 0.75);
23
+ const aggression = clamp(finiteOr(car.aggression, finiteOr(car.personality?.baseAggression, 0.5)), 0, 1);
24
+ const riskTolerance = clamp(finiteOr(car.personality?.riskTolerance, 0.5), 0, 1);
25
+ const risk = riskTolerance * 0.6 + aggression * 0.4;
26
+
27
+ // A continuous distance phase gives a bounded, zero-mean judgement error.
28
+ // It never advances RNG or introduces abrupt per-frame/sector decisions.
29
+ const distanceMeters = simUnitsToMeters(finiteOr(car.raceDistance, finiteOr(car.progress, 0)));
30
+ const phase = distanceMeters * DISTANCE_PHASE_RATE +
31
+ finiteOr(car.index, 0) * 0.73 + finiteOr(car.driverNumber, 0) * 0.31;
32
+ const precisionError = (1 - ability * 0.7) * (1 - consistency * 0.7);
33
+ const lineErrorMeters = 0.2 * precisionError * Math.sin(phase);
34
+ const judgementError = (1 - ability * 0.65) * (1 - consistency * 0.7) * risk;
35
+ const anticipationVariation = 0.025 * judgementError * Math.sin(phase + 1.7);
36
+
37
+ return {
38
+ ability,
39
+ consistency,
40
+ risk,
41
+ lateralShare: 0.74 + 0.2 * ability + 0.02 * risk,
42
+ longitudinalShare: 0.86 + 0.09 * ability + 0.03 * risk,
43
+ anticipationSeconds: 0.16 + 0.12 * (1 - ability) +
44
+ 0.04 * (1 - consistency) - 0.04 * risk + anticipationVariation,
45
+ lineErrorMeters,
46
+ speedJudgementScale: 1 + 0.012 * judgementError * Math.sin(phase + 0.8),
47
+ };
48
+ }
@@ -2,7 +2,7 @@ import { clamp, TWO_PI } from '../simMath.js';
2
2
  import { kphToSimSpeed, metersToSimUnits, simSpeedToKph, simUnitsToMeters } from '../units.js';
3
3
  import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
4
4
  import { DEFEND_MIN_OFFSET, DEFEND_REAR_GAP, LANE_EDGE_CLEARANCE_TARGET, LANE_OFFSETS, OVERTAKE_COMMIT_FRAMES, OVERTAKE_GAP_AGGRESSION, OVERTAKE_GAP_BASE, OVERTAKE_LATERAL_MIN, OVERTAKE_LATERAL_REWARD, PASS_SIDE_AGGRESSION, PASS_SIDE_BASE, TRAFFIC_GAP_AHEAD, TRAFFIC_REAR_WINDOW, TRAFFIC_SIDE_GAP, TRAFFIC_SIDE_OVERLAP } from './driverControlConstants.js';
5
- import { findDefensiveThreat, findLaneTrafficAhead, findLaneTrafficBeside, scanNearbyTraffic } from './trafficScan.js';
5
+ import { findDefensiveThreat, findLaneTrafficAhead, findLaneTrafficBeside, scanNearbyTraffic, mainTrackCenterOffset, followingSpeedFromTraffic, laneMovementBounds } from './trafficScan.js';
6
6
 
7
7
  export function planRacingLine(car, orderIndex, race) {
8
8
  const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
@@ -10,23 +10,35 @@ export function planRacingLine(car, orderIndex, race) {
10
10
  const trackLimit = race.track.width / 2 - VEHICLE_LIMITS.carWidth * clamp(1.55 - aggression * 0.28, 1.26, 1.55);
11
11
  const preferred = Math.sin((car.index / Math.max(1, race.cars.length)) * TWO_PI) * metersToSimUnits(1.1);
12
12
  const currentOffset = clamp(car.desiredOffset ?? preferred, -trackLimit, trackLimit);
13
- const ahead = race.orderedCars[orderIndex - 1];
14
13
  const traffic = scanNearbyTraffic(car, race);
15
- const rearThreat = findDefensiveThreat(traffic);
16
- const attackPlan = planAttackCommitment(car, ahead, traffic, trackLimit, aggression, race);
14
+ const corridorLimit = race.track.width / 2 + VEHICLE_LIMITS.carWidth / 2;
15
+ let aheadEntry = null;
16
+ for (let index = 0; index < traffic.length; index += 1) {
17
+ const entry = traffic[index];
18
+ if (Math.abs(entry.signedOffset) > corridorLimit) continue;
19
+ if (entry.gap > 0 && (!aheadEntry || entry.gap < aheadEntry.gap)) aheadEntry = entry;
20
+ }
21
+ const ahead = aheadEntry?.car;
22
+ const aheadGap = aheadEntry?.gap ?? Infinity;
23
+ const rearThreat = findDefensiveThreat(traffic, corridorLimit, car);
24
+ const attackPlan = planAttackCommitment(car, ahead, traffic, trackLimit, aggression, aheadGap);
17
25
 
18
26
  let bestOffset = currentOffset;
19
27
  let bestScore = -Infinity;
20
28
 
21
- LANE_OFFSETS.forEach((rawOffset) => {
22
- const offset = clamp(rawOffset, -trackLimit, trackLimit);
29
+ for (let laneIndex = 0; laneIndex < LANE_OFFSETS.length; laneIndex += 1) {
30
+ const offset = clamp(LANE_OFFSETS[laneIndex], -trackLimit, trackLimit);
23
31
  const edgeClearance = trackLimit - Math.abs(offset);
24
32
  let score = 80;
25
33
  score -= Math.abs(offset - preferred) * 0.18;
26
34
  score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
27
35
  score -= Math.max(0, LANE_EDGE_CLEARANCE_TARGET - edgeClearance) * (0.92 - aggression * 0.42);
28
36
 
29
- traffic.forEach((entry) => {
37
+ for (let trafficIndex = 0; trafficIndex < traffic.length; trafficIndex += 1) {
38
+ const entry = traffic[trafficIndex];
39
+ // Separate pit/runoff traffic cannot be an overtaking opportunity or
40
+ // defensive target. Keep it in the scan for genuine overlapping lanes.
41
+ if (Math.abs(entry.signedOffset) > corridorLimit) continue;
30
42
  const lateral = Math.abs(entry.signedOffset - offset);
31
43
  if (entry.gap > 0 && entry.gap < TRAFFIC_GAP_AHEAD) {
32
44
  const overlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
@@ -38,24 +50,26 @@ export function planRacingLine(car, orderIndex, race) {
38
50
  const sideOverlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
39
51
  score -= sideOverlapRisk * (TRAFFIC_SIDE_OVERLAP + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
40
52
  }
41
- });
53
+ }
42
54
 
43
- if (ahead && car.gapAhead < TRAFFIC_GAP_AHEAD) {
55
+ if (ahead && aheadGap < TRAFFIC_GAP_AHEAD) {
44
56
  const passSide = attackPlan?.targetOffset ?? clamp(
45
- (ahead.trackState.signedOffset * -0.65) + (car.index % 2 === 0 ? -1 : 1) * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION),
57
+ (mainTrackCenterOffset(ahead) * -0.65) + (car.index % 2 === 0 ? -1 : 1) * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION),
46
58
  -trackLimit,
47
59
  trackLimit,
48
60
  );
49
61
  const attackPressure = attackPlan?.pressure ?? 0.45;
50
62
  score -= Math.abs(offset - passSide) * (0.12 + aggression * 0.16) * attackPressure;
51
- if (attackPlan && Math.sign(offset || passSide) === attackPlan.side && Math.abs(offset - ahead.trackState.signedOffset) > OVERTAKE_LATERAL_MIN) {
63
+ if (attackPlan && Math.sign(offset || passSide) === attackPlan.side && Math.abs(offset - mainTrackCenterOffset(ahead)) > OVERTAKE_LATERAL_MIN) {
52
64
  score += metersToSimUnits(4 + aggression * 5) * attackPressure;
53
65
  }
54
66
  }
55
67
 
56
68
  const defenseIntensity = clamp((aggression - 0.5) / 0.3, 0, 1) *
57
69
  clamp((Math.max(riskTolerance, aggression) - 0.42) / 0.4, 0, 1);
58
- if (rearThreat && defenseIntensity > 0.04) {
70
+ // A pass needs a stable lane; reactive defense must not steer across the
71
+ // committed attacking line while the car is still passing its target.
72
+ if (rearThreat && !attackPlan && defenseIntensity > 0.04) {
59
73
  const threatSide = Math.sign(rearThreat.signedOffset || currentOffset || (car.index % 2 === 0 ? 1 : -1));
60
74
  const defendOffset = clamp(
61
75
  threatSide * Math.max(Math.abs(rearThreat.signedOffset) + metersToSimUnits(0.8), DEFEND_MIN_OFFSET),
@@ -73,17 +87,21 @@ export function planRacingLine(car, orderIndex, race) {
73
87
  bestScore = score;
74
88
  bestOffset = offset;
75
89
  }
76
- });
90
+ }
77
91
 
78
92
  const laneChangeRate = metersToSimUnits(0.95 + car.racecraft * 0.32 + aggression * 0.78);
79
93
  car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
80
94
 
95
+ const movement = laneMovementBounds(traffic, mainTrackCenterOffset(car));
81
96
  return {
82
97
  offset: car.desiredOffset,
98
+ minimumOffset: movement.minimum,
99
+ maximumOffset: movement.maximum,
83
100
  sameLaneAhead: findLaneTrafficAhead(traffic, car.desiredOffset, TRAFFIC_GAP_AHEAD),
84
- actualLaneAhead: findLaneTrafficAhead(traffic, car.trackState?.signedOffset ?? car.desiredOffset, TRAFFIC_GAP_AHEAD),
101
+ actualLaneAhead: findLaneTrafficAhead(traffic, mainTrackCenterOffset(car), TRAFFIC_GAP_AHEAD),
85
102
  sideRisk: findLaneTrafficBeside(traffic, car.desiredOffset),
86
103
  attackCommitted: Boolean(attackPlan),
104
+ followingSpeed: followingSpeedFromTraffic(car, traffic),
87
105
  };
88
106
  }
89
107
 
@@ -105,20 +123,20 @@ export function calculateActualOverlapPenalty(car, entry) {
105
123
  return clamp(urgent, 0, 72);
106
124
  }
107
125
 
108
- export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression, race) {
126
+ export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression, aheadGap = car.gapAhead) {
109
127
  const existingFrames = Math.max(0, Math.floor(car.attackCommitmentFrames ?? 0));
110
128
  const relativeSpeed = ahead ? car.speed - ahead.speed : 0;
111
129
  const closeEnough = ahead &&
112
- car.gapAhead > 0 &&
113
- car.gapAhead < OVERTAKE_GAP_BASE + aggression * OVERTAKE_GAP_AGGRESSION + metersToSimUnits(28);
130
+ aheadGap > 0 &&
131
+ aheadGap < OVERTAKE_GAP_BASE + aggression * OVERTAKE_GAP_AGGRESSION + metersToSimUnits(28);
114
132
  const speedPressure = clamp(relativeSpeed / kphToSimSpeed(34), -0.4, 1);
115
133
 
116
134
  if (closeEnough && speedPressure > -0.15) {
117
- const aheadOffset = ahead.trackState?.signedOffset ?? 0;
135
+ const aheadOffset = mainTrackCenterOffset(ahead);
118
136
  const currentSide = Math.sign(car.attackSide ?? 0);
119
137
  const blockedCommitment = currentSide &&
120
138
  Math.sign(aheadOffset || currentSide) === currentSide &&
121
- Math.abs(aheadOffset - (car.trackState?.signedOffset ?? 0)) < TRAFFIC_SIDE_GAP &&
139
+ Math.abs(aheadOffset - mainTrackCenterOffset(car)) < TRAFFIC_SIDE_GAP &&
122
140
  Math.abs(aheadOffset) > trackLimit * 0.55;
123
141
 
124
142
  if (!existingFrames || !currentSide || blockedCommitment) {
@@ -140,7 +158,7 @@ export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression
140
158
  if (!side || !ahead) return null;
141
159
 
142
160
  const targetOffset = clamp(
143
- (ahead.trackState?.signedOffset ?? 0) + side * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION + metersToSimUnits(2.4)),
161
+ mainTrackCenterOffset(ahead) + side * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION + metersToSimUnits(2.4)),
144
162
  -trackLimit,
145
163
  trackLimit,
146
164
  );
@@ -164,3 +182,19 @@ export function chooseFreerAttackSide(traffic, preferredSide) {
164
182
  }, side === preferredSide ? 0.18 : 0);
165
183
  return sideScore(1) >= sideScore(-1) ? 1 : -1;
166
184
  }
185
+
186
+ export function calculateRacingTrafficPenalty(car, lanePlan) {
187
+ const actualLaneLateral = lanePlan.actualLaneAhead
188
+ ? Math.abs(lanePlan.actualLaneAhead.signedOffset - mainTrackCenterOffset(car))
189
+ : Infinity;
190
+ const plannedPassingOverlap = lanePlan.attackCommitted && lanePlan.sameLaneAhead
191
+ ? Math.abs(lanePlan.sameLaneAhead.signedOffset - mainTrackCenterOffset(car)) > OVERTAKE_LATERAL_MIN
192
+ : false;
193
+ return Math.max(
194
+ lanePlan.sameLaneAhead ? calculatePlannedTrafficPenalty(lanePlan.sameLaneAhead, plannedPassingOverlap) : 0,
195
+ lanePlan.actualLaneAhead && actualLaneLateral < OVERTAKE_LATERAL_MIN
196
+ ? calculateActualOverlapPenalty(car, lanePlan.actualLaneAhead)
197
+ : 0,
198
+ lanePlan.sideRisk ? clamp(simUnitsToMeters(TRAFFIC_SIDE_GAP - lanePlan.sideRisk.lateral) * 0.78, 0, 30) : 0,
199
+ );
200
+ }
@@ -1,116 +1,78 @@
1
1
  import { clamp } from '../simMath.js';
2
- import { kphToSimSpeed, simUnitsToMeters } from '../units.js';
2
+ import { kphToSimSpeed, simUnitsToMeters, metersToSimUnits } from '../units.js';
3
3
  import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
4
  import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
5
5
  import { angleToPoint } from './driverMath.js';
6
6
  import { createDriverInput } from './driverInput.js';
7
- import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
7
+ import { VEHICLE_LIMITS, isSimulatorPhysicsMode, tirePerformanceFactor } from '../vehicle/vehiclePhysics.js';
8
+ import { advancedLateralAccelerationLimit } from '../vehicle/advancedTireForces.js';
9
+ import { advancedSteeringToPoint, advancedSpeedControls } from './advancedPathControls.js';
8
10
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
9
11
 
10
12
  export function decideRejoinControls(car, race) {
11
- if (race.physicsMode === 'advanced') {
13
+ if (isSimulatorPhysicsMode(race.physicsMode)) {
12
14
  return decideSimulatorRejoinControls(car, race);
13
15
  }
14
16
  return decideArcadeRejoinControls(car, race);
15
17
  }
16
18
 
17
- export function decideArcadeRejoinControls(car, race) {
18
- return decideRejoinControlsForMode(car, race, {
19
- simulatorMode: false,
20
- inwardOffsetRatio: 0.34,
21
- surfaceTargetSpeeds: {
22
- track: 88,
23
- kerb: 72,
24
- gravel: 56,
25
- grass: 48,
26
- barrier: 34,
27
- },
28
- lowSpeedRecovery: {
29
- barrier: 0.34,
30
- default: 0.18,
31
- },
32
- throttleLimits: {
33
- barrier: 0.68,
34
- gravel: 0.46,
35
- grass: 0.42,
36
- default: 0.42,
37
- },
38
- brakeResponseKph: 30,
39
- onTrackBrakeLimit: 0.46,
40
- offTrackBrakeLimit: 0.74,
41
- steerGain: 1.65,
42
- outwardBrakeStartMps: 1.2,
43
- outwardBrakeGain: 0.08,
44
- misalignmentBrakeGain: 0.06,
45
- slideThrottleDamping: 0.36,
46
- offTrackForwardTargetScale: 0.42,
47
- lowSpeedForwardTargetScale: 0,
48
- lowSpeedOffTrackSteerLimit: 0.52,
49
- lowSpeedRecoveryThrottle: 0.32,
50
- lowSpeedOutwardBrakeStartMps: 1.2,
51
- lowSpeedCrawlOutwardToleranceMps: 0,
52
- lowSpeedHeadingOutwardThrottleLimit: 1,
53
- stabilizeLowSpeedOutward: false,
54
- stabilizeRejoinHoldOutsideRoad: false,
55
- unsettledScale: () => 1,
56
- });
57
- }
19
+ const ARCADE_REJOIN_PROFILE = {
20
+ inwardOffsetRatio: 0.34,
21
+ surfaceTargetSpeeds: {
22
+ track: 88,
23
+ kerb: 72,
24
+ gravel: 56,
25
+ grass: 48,
26
+ barrier: 34,
27
+ },
28
+ lowSpeedRecovery: {
29
+ barrier: 0.34,
30
+ default: 0.18,
31
+ },
32
+ throttleLimits: {
33
+ barrier: 0.68,
34
+ gravel: 0.46,
35
+ grass: 0.42,
36
+ default: 0.42,
37
+ },
38
+ brakeResponseKph: 30,
39
+ onTrackBrakeLimit: 0.46,
40
+ offTrackBrakeLimit: 0.74,
41
+ steerGain: 1.65,
42
+ outwardBrakeStartMps: 1.2,
43
+ outwardBrakeGain: 0.08,
44
+ misalignmentBrakeGain: 0.06,
45
+ slideThrottleDamping: 0.36,
46
+ offTrackForwardTargetScale: 0.42,
47
+ lowSpeedForwardTargetScale: 0,
48
+ lowSpeedOffTrackSteerLimit: 0.52,
49
+ lowSpeedRecoveryThrottle: 0.32,
50
+ };
58
51
 
59
52
  export function decideSimulatorRejoinControls(car, race) {
60
- return decideRejoinControlsForMode(car, race, {
61
- simulatorMode: true,
62
- inwardOffsetRatio: 0.24,
63
- surfaceTargetSpeeds: {
64
- track: 78,
65
- kerb: 58,
66
- gravel: 62,
67
- grass: 52,
68
- barrier: 22,
69
- },
70
- lowSpeedRecovery: {
71
- barrier: 0.42,
72
- default: 0.36,
73
- },
74
- throttleLimits: {
75
- barrier: 0.5,
76
- gravel: 0.82,
77
- grass: 0.76,
78
- default: 0.52,
79
- },
80
- brakeResponseKph: 24,
81
- onTrackBrakeLimit: 0.34,
82
- offTrackBrakeLimit: 0.24,
83
- steerGain: 0.94,
84
- outwardBrakeStartMps: 0.45,
85
- outwardBrakeGain: 0.12,
86
- misalignmentBrakeGain: 0.08,
87
- slideThrottleDamping: 0.62,
88
- offTrackForwardTargetScale: 0.32,
89
- lowSpeedForwardTargetScale: 0.22,
90
- lowSpeedOffTrackSteerLimit: 0.68,
91
- lowSpeedRecoveryThrottle: 0.68,
92
- lowSpeedOutwardBrakeStartMps: 0.24,
93
- lowSpeedCrawlOutwardToleranceMps: 0.42,
94
- lowSpeedHeadingOutwardThrottleLimit: 0.24,
95
- stabilizeLowSpeedOutward: true,
96
- stabilizeRejoinHoldOutsideRoad: true,
97
- unsettledScale: (entry) => clamp(
98
- 1 - Math.max(0, (entry.gripUsage ?? 0) - 0.55) * 0.75 - Math.abs(entry.slipAngleRadians ?? 0) * 1.65,
99
- 0.22,
100
- 1,
101
- ),
102
- });
53
+ const speed = simUnitsToMeters(car.speed);
54
+ if (Math.abs(car.slipAngleRadians ?? 0) > 0.5 && speed > 2) {
55
+ // Once the car is sliding sideways/backwards, path steering cannot recover
56
+ // its line. Arrest the slide before requesting a forward rejoin.
57
+ return createDriverInput().brake(1).controls();
58
+ }
59
+ const target = pointAt(race.track, car.progress + metersToSimUnits(clamp(10 + speed * 0.5, 10, 30)));
60
+ const { steering, curvature } = advancedSteeringToPoint(car, target);
61
+ const capacity = advancedLateralAccelerationLimit(car, speed, tirePerformanceFactor(car.tireEnergy ?? 100));
62
+ const surfaceSpeed = car.trackState.onTrack ? 22 : 12;
63
+ const targetSpeed = Math.min(surfaceSpeed, Math.sqrt(capacity * 0.55 / Math.max(Math.abs(curvature), 0.001)));
64
+ const { throttle, brake } = advancedSpeedControls(car, targetSpeed);
65
+ return createDriverInput().steer(steering).accelerate(throttle).brake(brake).controls();
103
66
  }
104
67
 
105
- function decideRejoinControlsForMode(car, race, profile) {
106
- const simulatorMode = race.physicsMode === 'advanced';
107
- const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
68
+ export function decideArcadeRejoinControls(car, race) {
69
+ const profile = ARCADE_REJOIN_PROFILE;
70
+ const lookahead = clamp(car.speed * 0.72 + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
108
71
  const edgeMotion = analyzeTrackEdgeMotion(car, race);
109
72
  const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
110
73
  const distanceFromRoadMeters = simUnitsToMeters(Math.max(0, car.trackState.crossTrackError - race.track.width / 2));
111
- const inRejoinHold = (car.rejoinRecoveryFrames ?? 0) > 0;
112
74
  const recoveringOutsideRoad = distanceFromRoadMeters > 0.25 &&
113
- (!car.trackState.onTrack || (profile.stabilizeRejoinHoldOutsideRoad && inRejoinHold));
75
+ !car.trackState.onTrack;
114
76
  const offTrackTargetScale = lowSpeedOffTrack
115
77
  ? profile.lowSpeedForwardTargetScale
116
78
  : profile.offTrackForwardTargetScale;
@@ -145,26 +107,17 @@ function decideRejoinControlsForMode(car, race, profile) {
145
107
  const lowSpeedRecovery = car.speed < kphToSimSpeed(18)
146
108
  ? car.trackState.surface === 'barrier' ? profile.lowSpeedRecovery.barrier : profile.lowSpeedRecovery.default
147
109
  : 0;
148
- const outwardBrakeStart = lowSpeedOffTrack
149
- ? profile.lowSpeedOutwardBrakeStartMps
150
- : profile.outwardBrakeStartMps;
151
- const canCrawlSteerRecovery = lowSpeedOffTrack &&
152
- car.speed < kphToSimSpeed(8) &&
153
- edgeMotion.outwardSpeedMps < profile.lowSpeedCrawlOutwardToleranceMps;
110
+ const outwardBrakeStart = profile.outwardBrakeStartMps;
154
111
  const needsOutwardStabilization = recoveringOutsideRoad &&
155
- (!lowSpeedOffTrack || profile.stabilizeLowSpeedOutward) &&
156
- !canCrawlSteerRecovery &&
112
+ !lowSpeedOffTrack &&
157
113
  edgeMotion.outwardSpeedMps > outwardBrakeStart &&
158
114
  distanceFromRoadMeters > 0.25;
159
- const recoveryThrottleFloorBase = profile.simulatorMode && !car.trackState.onTrack
160
- ? (car.trackState.surface === 'barrier' ? profile.lowSpeedRecovery.barrier : profile.lowSpeedRecovery.default)
161
- : lowSpeedRecovery;
162
115
  const lowSpeedRecoveryThrottle = lowSpeedOffTrack && car.trackState.surface !== 'barrier'
163
116
  ? profile.lowSpeedRecoveryThrottle
164
117
  : 0;
165
118
  const recoveryThrottleFloor = needsOutwardStabilization
166
119
  ? 0
167
- : Math.max(recoveryThrottleFloorBase, lowSpeedRecoveryThrottle);
120
+ : Math.max(lowSpeedRecovery, lowSpeedRecoveryThrottle);
168
121
  const surfaceThrottleLimit = car.trackState.surface === 'barrier'
169
122
  ? profile.throttleLimits.barrier
170
123
  : car.trackState.surface === 'gravel'
@@ -172,9 +125,6 @@ function decideRejoinControlsForMode(car, race, profile) {
172
125
  : car.trackState.surface === 'grass'
173
126
  ? profile.throttleLimits.grass
174
127
  : profile.throttleLimits.default;
175
- const throttleLimit = lowSpeedOffTrack && edgeMotion.headingOutward && distanceFromRoadMeters > 0.5
176
- ? Math.min(surfaceThrottleLimit, profile.lowSpeedHeadingOutwardThrottleLimit)
177
- : surfaceThrottleLimit;
178
128
  let brakeAmount = speedError < -kphToSimSpeed(5)
179
129
  ? clamp(Math.abs(speedError) / kphToSimSpeed(profile.brakeResponseKph), 0.04, car.trackState.onTrack ? profile.onTrackBrakeLimit : profile.offTrackBrakeLimit)
180
130
  : 0;
@@ -189,9 +139,6 @@ function decideRejoinControlsForMode(car, race, profile) {
189
139
  ),
190
140
  );
191
141
  }
192
- const unsettledScale = lowSpeedOffTrack
193
- ? Math.max(profile.unsettledScale(car), 0.82)
194
- : profile.unsettledScale(car);
195
142
  const slideThrottleScale = 1 - clamp(
196
143
  Math.max(0, edgeMotion.outwardSpeedMps - profile.outwardBrakeStartMps) * profile.slideThrottleDamping +
197
144
  Math.max(0, Math.abs(angleError) - 1.25) * 0.22,
@@ -206,8 +153,8 @@ function decideRejoinControlsForMode(car, race, profile) {
206
153
  return createDriverInput()
207
154
  .steer(steeringRequest)
208
155
  .accelerate(brakeAmount > 0.05 ? 0 : speedError > 0
209
- ? clamp(((speedError / kphToSimSpeed(24)) * alignment + recoveryThrottleFloor) * unsettledScale * slideThrottleScale, minimumAcceleration, throttleLimit)
210
- : recoveryThrottleFloor * unsettledScale * slideThrottleScale)
156
+ ? clamp(((speedError / kphToSimSpeed(24)) * alignment + recoveryThrottleFloor) * slideThrottleScale, minimumAcceleration, surfaceThrottleLimit)
157
+ : recoveryThrottleFloor * slideThrottleScale)
211
158
  .brake(brakeAmount)
212
159
  .controls();
213
160
  }
@@ -4,8 +4,14 @@ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
4
  import { SAFETY_CAR_LOOKAHEAD_MAX, SAFETY_CAR_LOOKAHEAD_MIN } from './driverControlConstants.js';
5
5
  import { angleToPoint } from './driverMath.js';
6
6
  import { createDriverInput } from './driverInput.js';
7
+ import { isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
8
+ import { advancedSteeringToPoint, advancedSpeedControls } from './advancedPathControls.js';
9
+ import { decideSimulatorRejoinControls } from './rejoinControls.js';
7
10
 
8
11
  export function decideSafetyCarControls(car, orderIndex, race) {
12
+ if (isSimulatorPhysicsMode(race.physicsMode) && !car.trackState.onTrack) {
13
+ return decideSimulatorRejoinControls(car, race);
14
+ }
9
15
  const queueSlot = race.safetyCar.progress - race.rules.safetyCarLeadDistance - orderIndex * race.rules.safetyCarGap;
10
16
  const lookahead = clamp(car.speed * 0.55 + metersToSimUnits(20), SAFETY_CAR_LOOKAHEAD_MIN, SAFETY_CAR_LOOKAHEAD_MAX);
11
17
  const targetBase = pointAt(race.track, car.progress + lookahead);
@@ -27,6 +33,12 @@ export function decideSafetyCarControls(car, orderIndex, race) {
27
33
  );
28
34
  const speedError = desiredSpeed - car.speed;
29
35
 
36
+ if (isSimulatorPhysicsMode(race.physicsMode)) {
37
+ const { steering } = advancedSteeringToPoint(car, target);
38
+ const { throttle, brake } = advancedSpeedControls(car, simUnitsToMeters(desiredSpeed));
39
+ return createDriverInput().steer(steering).accelerate(throttle).brake(brake).controls();
40
+ }
41
+
30
42
  return createDriverInput()
31
43
  .steer(angleToPoint(car, target) * 1.18 + headingError * 0.32 + centerlineCorrection)
32
44
  .accelerate(speedError > kphToSimSpeed(1) ? clamp(speedError / kphToSimSpeed(16), 0, 0.5) : 0)
@@ -1,26 +1,50 @@
1
- import { metersToSimUnits } from '../units.js';
2
- import { DEFEND_REAR_GAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP, TRAFFIC_REAR_WINDOW } from './driverControlConstants.js';
1
+ import { metersToSimUnits, simUnitsToMeters, TOP_SPEED_SIM_UNITS_PER_SECOND } from '../units.js';
2
+ import { DEFEND_REAR_GAP, TRAFFIC_SIDE_GAP, TRAFFIC_GAP_AHEAD } from './driverControlConstants.js';
3
3
  import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
4
+ import { canCollide } from '../participants/participantInteractions.js';
5
+ import { signedLateralOffsetToPoint } from '../track/trackMath.js';
6
+
7
+ const TRAFFIC_REAR_SCAN_WINDOW = metersToSimUnits(39);
8
+ const TRAFFIC_FORWARD_SCAN_WINDOW = metersToSimUnits(134);
9
+ const TRAFFIC_BESIDE_WINDOW = metersToSimUnits(28);
10
+
11
+ // Entries are reused across calls: drivers consume the scan synchronously
12
+ // within a single control decision, so a module-level pool avoids re-allocating
13
+ // the candidate list for every car on every step.
14
+ const TRAFFIC_SCRATCH = [];
15
+ const TRAFFIC_ENTRY_POOL = [];
4
16
 
5
17
  export function scanNearbyTraffic(car, race) {
6
- return race.cars
7
- .filter((other) => other !== car)
8
- .map((other) => ({
9
- car: other,
10
- gap: other.raceDistance - car.raceDistance,
11
- signedOffset: other.trackState?.signedOffset ?? 0,
12
- speed: other.speed,
13
- }))
14
- .filter((entry) => entry.gap > -metersToSimUnits(39) && entry.gap < metersToSimUnits(134));
15
- }
16
-
17
- export function findDefensiveThreat(traffic) {
18
+ const traffic = TRAFFIC_SCRATCH;
19
+ traffic.length = 0;
20
+ const cars = race.cars;
21
+ for (let index = 0; index < cars.length; index += 1) {
22
+ const other = cars[index];
23
+ if (other === car || !canCollide(car, other)) continue;
24
+ const gap = physicalTrafficGap(car, other, race.track?.length);
25
+ if (gap <= -TRAFFIC_REAR_SCAN_WINDOW || gap >= TRAFFIC_FORWARD_SCAN_WINDOW) continue;
26
+ const entry = TRAFFIC_ENTRY_POOL[traffic.length] ??= { car: null, gap: 0, signedOffset: 0, speed: 0 };
27
+ entry.car = other;
28
+ entry.gap = gap;
29
+ entry.signedOffset = mainTrackCenterOffset(other);
30
+ entry.speed = other.speed;
31
+ traffic.push(entry);
32
+ }
33
+ return traffic;
34
+ }
35
+
36
+ export function findDefensiveThreat(traffic, corridorLimit = Infinity, car = null) {
18
37
  let closest = null;
19
38
 
20
- traffic.forEach((entry) => {
21
- if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) return;
39
+ for (let index = 0; index < traffic.length; index += 1) {
40
+ const entry = traffic[index];
41
+ if (Math.abs(entry.signedOffset) > corridorLimit) continue;
42
+ // A physically rearward car with greater race distance is lapping us.
43
+ // Yield ordinary road space instead of treating it as a position threat.
44
+ if (car && entry.car.raceDistance > car.raceDistance) continue;
45
+ if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) continue;
22
46
  if (!closest || entry.gap > closest.gap) closest = entry;
23
- });
47
+ }
24
48
 
25
49
  return closest;
26
50
  }
@@ -28,24 +52,109 @@ export function findDefensiveThreat(traffic) {
28
52
  export function findLaneTrafficAhead(traffic, offset, maxDistance) {
29
53
  let closest = null;
30
54
 
31
- traffic.forEach((entry) => {
32
- if (entry.gap <= 0 || entry.gap > maxDistance) return;
33
- if (Math.abs(entry.signedOffset - offset) > TRAFFIC_SIDE_GAP) return;
55
+ for (let index = 0; index < traffic.length; index += 1) {
56
+ const entry = traffic[index];
57
+ if (entry.gap <= 0 || entry.gap > maxDistance) continue;
58
+ if (Math.abs(entry.signedOffset - offset) > TRAFFIC_SIDE_GAP) continue;
34
59
  if (!closest || entry.gap < closest.gap) closest = entry;
35
- });
60
+ }
36
61
 
37
62
  return closest;
38
63
  }
39
64
 
40
65
  export function findLaneTrafficBeside(traffic, offset) {
41
66
  let closest = null;
67
+ let closestLateral = 0;
68
+ let closestRisk = -Infinity;
42
69
 
43
- traffic.forEach((entry) => {
70
+ for (let index = 0; index < traffic.length; index += 1) {
71
+ const entry = traffic[index];
44
72
  const lateral = Math.abs(entry.signedOffset - offset);
45
- if (Math.abs(entry.gap) > metersToSimUnits(28) || lateral > TRAFFIC_SIDE_GAP) return;
46
- const risk = (metersToSimUnits(28) - Math.abs(entry.gap)) + (TRAFFIC_SIDE_GAP - lateral);
47
- if (!closest || risk > closest.risk) closest = { ...entry, lateral, risk };
48
- });
73
+ if (Math.abs(entry.gap) > TRAFFIC_BESIDE_WINDOW || lateral > TRAFFIC_SIDE_GAP) continue;
74
+ const risk = (TRAFFIC_BESIDE_WINDOW - Math.abs(entry.gap)) + (TRAFFIC_SIDE_GAP - lateral);
75
+ if (!closest || risk > closestRisk) {
76
+ closest = entry;
77
+ closestLateral = lateral;
78
+ closestRisk = risk;
79
+ }
80
+ }
49
81
 
50
- return closest;
82
+ return closest ? { ...closest, lateral: closestLateral, risk: closestRisk } : null;
83
+ }
84
+
85
+ // Race classification distance can differ by whole laps while the cars occupy
86
+ // the same piece of road. Keep traffic in the local, signed circuit frame.
87
+ export function physicalTrafficGap(car, other, trackLength) {
88
+ if (!(trackLength > 0)) return other.raceDistance - car.raceDistance;
89
+ let gap = ((other.progress ?? other.raceDistance) - (car.progress ?? car.raceDistance)) % trackLength;
90
+ if (gap > trackLength / 2) gap -= trackLength;
91
+ else if (gap < -trackLength / 2) gap += trackLength;
92
+ return gap;
93
+ }
94
+
95
+ export function mainTrackCenterOffset(car) {
96
+ // Pit surfaces retain the chassis offset in the main-track frame. Ordinary
97
+ // signedOffset describes an outer wheel, so project the center instead.
98
+ const state = car.trackState;
99
+ if (state?.mainTrackSignedOffset != null) return state.mainTrackSignedOffset;
100
+ const projected = state ? signedLateralOffsetToPoint(state, car) : NaN;
101
+ return Number.isFinite(projected) ? projected : state?.signedOffset ?? 0;
102
+ }
103
+
104
+ export function followingSpeedLimit(car, cars, trackLength) {
105
+ const centerOffset = mainTrackCenterOffset(car);
106
+ let leader = null;
107
+ let gap = TRAFFIC_GAP_AHEAD;
108
+ for (let index = 0; index < cars.length; index += 1) {
109
+ const other = cars[index];
110
+ if (other === car || !canCollide(car, other)) continue;
111
+ const distance = physicalTrafficGap(car, other, trackLength);
112
+ if (distance <= 0 || distance > gap) continue;
113
+ const lateral = Math.abs(mainTrackCenterOffset(other) - centerOffset);
114
+ if (lateral >= VEHICLE_LIMITS.carWidth + metersToSimUnits(0.4)) continue;
115
+ leader = other;
116
+ gap = distance;
117
+ }
118
+ return followingTargetSpeed(car, leader, gap);
119
+ }
120
+
121
+ // The lane planner already has a filtered scan. Reuse it for pacing instead
122
+ // of rescanning the entire field for every driver decision.
123
+ export function followingSpeedFromTraffic(car, traffic) {
124
+ const centerOffset = mainTrackCenterOffset(car);
125
+ let leader = null;
126
+ let gap = TRAFFIC_GAP_AHEAD;
127
+ for (let index = 0; index < traffic.length; index += 1) {
128
+ const entry = traffic[index];
129
+ if (entry.gap <= 0 || entry.gap > gap) continue;
130
+ if (Math.abs(entry.signedOffset - centerOffset) >= VEHICLE_LIMITS.carWidth + metersToSimUnits(0.4)) continue;
131
+ leader = entry.car;
132
+ gap = entry.gap;
133
+ }
134
+ return followingTargetSpeed(car, leader, gap);
135
+ }
136
+
137
+ function followingTargetSpeed(car, leader, gap) {
138
+ if (!leader) return simUnitsToMeters(TOP_SPEED_SIM_UNITS_PER_SECOND);
139
+ const speed = simUnitsToMeters(car.speed);
140
+ const followingGap = simUnitsToMeters(VEHICLE_LIMITS.carLength) + 2 + speed * 0.35;
141
+ return Math.max(0, simUnitsToMeters(leader.speed) + (simUnitsToMeters(gap) - followingGap) * 1.2);
142
+ }
143
+
144
+ // Lane intentions and corner offsets compose differently in each mode. Return
145
+ // the available lateral corridor so the final commanded path stays clear of
146
+ // cars alongside, rather than checking only an intermediate lane intention.
147
+ const LANE_MOVEMENT_BOUNDS = { minimum: -Infinity, maximum: Infinity };
148
+ export function laneMovementBounds(traffic, currentOffset) {
149
+ const bounds = LANE_MOVEMENT_BOUNDS;
150
+ bounds.minimum = -Infinity;
151
+ bounds.maximum = Infinity;
152
+ for (let index = 0; index < traffic.length; index += 1) {
153
+ const entry = traffic[index];
154
+ if (Math.abs(entry.gap) > VEHICLE_LIMITS.carLength) continue;
155
+ const neighbor = entry.signedOffset - currentOffset;
156
+ if (neighbor > 0) bounds.maximum = Math.min(bounds.maximum, Math.max(currentOffset, entry.signedOffset - VEHICLE_LIMITS.carWidth));
157
+ else if (neighbor < 0) bounds.minimum = Math.max(bounds.minimum, Math.min(currentOffset, entry.signedOffset + VEHICLE_LIMITS.carWidth));
158
+ }
159
+ return bounds;
51
160
  }