@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,111 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../units.js';
3
+ import { pointAtInto } from '../track/trackModel.js';
4
+ import { VEHICLE_LIMITS, tirePerformanceFactor, arcadeTrackCornerSpeedLimit, arcadeSurfaceGrip } from '../vehicle/vehiclePhysics.js';
5
+ import { mainTrackCenterOffset } from './trafficScan.js';
6
+ import { createDriverInput } from './driverInput.js';
7
+ import { angleToPoint } from './driverMath.js';
8
+ import { calculateTrackEdgeGuard } from './edgeRecovery.js';
9
+ import { calculateRacingTrafficPenalty, planRacingLine } from './racingLinePlan.js';
10
+ import { arcadePreviewLookahead, arcadePreviewSpeed, arcadeRacingLookahead, calculateRacingLineOffset, maxLookaheadCurvature } from './arcadeRacePace.js';
11
+ import { arcadeRacingSpeedControls, arcadeLongitudinalLimits } from './arcadeRacingEnvelope.js';
12
+ import { racingDriverProfile } from './racingDriverProfile.js';
13
+
14
+ const TARGET_BASE_SCRATCH = { x: 0, y: 0, heading: 0, normalX: 0, normalY: 0, curvature: 0, distance: 0 };
15
+ const TARGET_POINT_SCRATCH = { x: 0, y: 0 };
16
+
17
+ export function decideArcadeRacingControls(car, orderIndex, race) {
18
+ const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
19
+ const profile = racingDriverProfile(car);
20
+ const edgeGuard = calculateTrackEdgeGuard(car, race);
21
+ const previewLookahead = arcadePreviewLookahead(car.speed);
22
+ const previewCurvature = maxLookaheadCurvature(race.track, car.progress, previewLookahead);
23
+ const lookahead = arcadeRacingLookahead(car.speed);
24
+ const targetBase = pointAtInto(race.track, car.progress + lookahead, TARGET_BASE_SCRATCH);
25
+ const curvature = Math.max(car.trackState.curvature, targetBase.curvature, previewCurvature);
26
+ const lanePlan = planRacingLine(car, orderIndex, race);
27
+ const cornerOffsetDamping = clamp(curvature / 0.0012, 0, 0.58);
28
+ const recoveryOffset = edgeGuard.side * edgeGuard.recoveryOffset;
29
+ const racingOffset = calculateRacingLineOffset(car, race, lookahead, curvature, edgeGuard);
30
+ const trafficPressure = lanePlan.attackCommitted || lanePlan.sameLaneAhead || lanePlan.actualLaneAhead || lanePlan.sideRisk ? 1 : 0;
31
+ const racingLineWeight = lanePlan.attackCommitted ? 0.18 : trafficPressure ? 0.52 : 1;
32
+ const cornerOffsetFactor = 1 - cornerOffsetDamping * (lanePlan.attackCommitted ? 0.28 : trafficPressure ? 0.58 : 1);
33
+ const plannedTargetOffset = clamp(
34
+ (lanePlan.offset + racingOffset * racingLineWeight + metersToSimUnits(profile.lineErrorMeters)) * cornerOffsetFactor * (1 - edgeGuard.pressure * 0.8) +
35
+ recoveryOffset * edgeGuard.pressure * 0.8,
36
+ -edgeGuard.recoveryOffset,
37
+ edgeGuard.recoveryOffset,
38
+ );
39
+ // A committed passing lane must unwind with the corner. Moving laterally
40
+ // while the current turn consumes most grip sends a safe speed target off
41
+ // the road. Edge recovery retains priority over holding the current line.
42
+ const cornerHold = trafficPressure && edgeGuard.pressure < 0.2
43
+ ? clamp((car.gripUsage ?? 0) / 0.7, 0, 1) ** 2 : 0;
44
+ const currentOffset = mainTrackCenterOffset(car);
45
+ const targetOffset = clamp(
46
+ currentOffset + (plannedTargetOffset - currentOffset) * (1 - cornerHold),
47
+ -edgeGuard.recoveryOffset,
48
+ edgeGuard.recoveryOffset,
49
+ );
50
+ TARGET_POINT_SCRATCH.x = targetBase.x + targetBase.normalX * targetOffset;
51
+ TARGET_POINT_SCRATCH.y = targetBase.y + targetBase.normalY * targetOffset;
52
+ const angleError = angleToPoint(car, TARGET_POINT_SCRATCH);
53
+ const headingError = normalizeAngle(targetBase.heading - car.heading);
54
+ const steeringLimit = car.trackState.surface === 'kerb' ? VEHICLE_LIMITS.maxSteer * 0.96 : VEHICLE_LIMITS.maxSteer;
55
+ // Invert the arcade bicycle yaw law for the curvature to the chosen lane point.
56
+ // Tire wear scales yaw response in the integrator, so it also scales this inverse.
57
+ const pursuitCurve = 2 * Math.sin(angleError) / Math.max(
58
+ Math.hypot(TARGET_POINT_SCRATCH.x - car.x, TARGET_POINT_SCRATCH.y - car.y),
59
+ metersToSimUnits(1),
60
+ );
61
+ const steeringRequest = clamp(
62
+ Math.atan(VEHICLE_LIMITS.wheelbase * pursuitCurve / tirePerformanceFactor(car.tireEnergy ?? 100)),
63
+ -steeringLimit,
64
+ steeringLimit,
65
+ );
66
+ const cornerTargetKph = arcadePreviewSpeed(car, race.track, previewLookahead, aggression, profile);
67
+ const edgePenalty = edgeGuard.pressure > 0.42
68
+ ? simUnitsToMeters(Math.max(0, car.trackState.crossTrackError - edgeGuard.softLimit)) *
69
+ (5.8 - aggression * 1.1) + edgeGuard.overLimitPressure * 48
70
+ : edgeGuard.overLimitPressure * 48;
71
+ const steeringPenalty = clamp((Math.abs(angleError) - 0.38) * 24, 0, 12);
72
+ const headingPenalty = clamp((Math.abs(headingError) - 0.48) * 42, 0, 24);
73
+ const recoveryAlignmentPenalty = edgeGuard.pressure > 0.2
74
+ ? clamp((Math.abs(headingError) - 0.46) * 64, 0, 38)
75
+ : 0;
76
+ const steeringLoadPenalty = clamp(
77
+ (Math.abs(steeringRequest) / Math.max(steeringLimit, 1e-6) - 0.82) * 42,
78
+ 0,
79
+ 18,
80
+ ) * clamp(Math.abs(headingError) / 0.48, 0, 1);
81
+ const trafficPenalty = calculateRacingTrafficPenalty(car, lanePlan) * (1 - aggression * 0.28);
82
+ const minimumDesiredSpeedKph = edgeGuard.pressure > 0.55 ? 34 : car.trackState.surface === 'kerb' ? 58 : 52;
83
+ const desiredSpeedKph = clamp(
84
+ cornerTargetKph - edgePenalty - steeringPenalty - headingPenalty - recoveryAlignmentPenalty - steeringLoadPenalty - trafficPenalty,
85
+ minimumDesiredSpeedKph,
86
+ 330,
87
+ );
88
+ // Corner capacity and physical traffic take precedence over maneuvering
89
+ // floors: a stopped leader may require the car to stop until the pass clears.
90
+ const desiredSpeed = Math.min(
91
+ metersToSimUnits(lanePlan.followingSpeed),
92
+ kphToSimSpeed(cornerTargetKph),
93
+ metersToSimUnits(arcadeTrackCornerSpeedLimit(car, pursuitCurve * metersToSimUnits(1), profile.lateralShare * arcadeSurfaceGrip(car))),
94
+ clamp(kphToSimSpeed(desiredSpeedKph), kphToSimSpeed(minimumDesiredSpeedKph), VEHICLE_LIMITS.maxSpeed),
95
+ );
96
+ const pedals = arcadeRacingSpeedControls(car, simUnitsToMeters(desiredSpeed), profile);
97
+ const brakeLimit = arcadeLongitudinalLimits(car).brakeDeceleration * car.mass / car.brakeNewtons;
98
+ let brakeAmount = pedals.brake;
99
+ if (edgeGuard.pressure > 0.46 && car.speed > kphToSimSpeed(48)) {
100
+ brakeAmount = Math.max(brakeAmount, clamp((edgeGuard.pressure - 0.42) * 0.9, 0, brakeLimit));
101
+ }
102
+ const recoveryThrottleScale = edgeGuard.pressure > 0.24
103
+ ? 1 - clamp((Math.abs(headingError) - 0.38) / 0.92, 0, 0.82)
104
+ : 1;
105
+
106
+ return createDriverInput()
107
+ .steer(steeringRequest)
108
+ .accelerate(brakeAmount > 0 ? 0 : pedals.throttle * recoveryThrottleScale)
109
+ .brake(brakeAmount)
110
+ .controls();
111
+ }
@@ -0,0 +1,63 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
3
+ import {
4
+ arcadeAccelerationLimit,
5
+ arcadeBrakingLimit,
6
+ arcadeResistanceForce,
7
+ arcadeSurfaceGrip,
8
+ tirePerformanceFactor,
9
+ VEHICLE_LIMITS,
10
+ } from '../vehicle/vehiclePhysics.js';
11
+ import { racingDriverProfile } from './racingDriverProfile.js';
12
+
13
+ const SPEED_RESPONSE_SECONDS = 0.0625;
14
+
15
+ function brakeDecelerationAt(car, speedSim, grip) {
16
+ return Math.min(car.brakeNewtons / car.mass, simUnitsToMeters(arcadeBrakingLimit(speedSim, grip)));
17
+ }
18
+
19
+ // All accelerations/speeds here are SI. Arcade has independent longitudinal
20
+ // and yaw limits, so this envelope does not add a fictional friction circle.
21
+ export function arcadeLongitudinalLimits(car, speedMps = simUnitsToMeters(car.speed)) {
22
+ const speed = clamp(speedMps, 0, simUnitsToMeters(VEHICLE_LIMITS.maxSpeed));
23
+ const speedSim = metersToSimUnits(speed);
24
+ const speedRatio = clamp(speedSim / VEHICLE_LIMITS.maxSpeed, 0, 1);
25
+ const grip = arcadeSurfaceGrip(car) * tirePerformanceFactor(car.tireEnergy ?? 100);
26
+ const engineForce = car.powerNewtons * (car.drsActive ? 1.08 : 1) *
27
+ Math.max(0.12, 1 - speedRatio ** 1.15);
28
+ const drsBonus = car.drsActive ? 1.06 : 1;
29
+ return {
30
+ speed,
31
+ engineForce,
32
+ drsBonus,
33
+ driveAcceleration: Math.min(engineForce / car.mass, simUnitsToMeters(arcadeAccelerationLimit(speedSim, grip))) * drsBonus,
34
+ brakeDeceleration: brakeDecelerationAt(car, speedSim, grip),
35
+ resistanceDeceleration: arcadeResistanceForce(car, speed) / car.mass,
36
+ };
37
+ }
38
+
39
+ export function arcadeBrakingDeceleration(car, speedMps, profile = racingDriverProfile(car)) {
40
+ const speed = clamp(speedMps, 0, simUnitsToMeters(VEHICLE_LIMITS.maxSpeed));
41
+ const grip = arcadeSurfaceGrip(car) * tirePerformanceFactor(car.tireEnergy ?? 100);
42
+ // Preview needs only braking, not engine taper or acceleration capacity.
43
+ return brakeDecelerationAt(car, metersToSimUnits(speed), grip) * clamp(profile.longitudinalShare, 0, 1) +
44
+ arcadeResistanceForce(car, speed) / car.mass;
45
+ }
46
+
47
+ export function arcadeRacingSpeedControls(car, targetSpeedMps, profile = racingDriverProfile(car)) {
48
+ const limits = arcadeLongitudinalLimits(car);
49
+ const target = clamp(targetSpeedMps, 0, simUnitsToMeters(VEHICLE_LIMITS.maxSpeed));
50
+ const share = clamp(profile.longitudinalShare, 0, 1);
51
+ // A single signed force request includes only resistance needed to hold a
52
+ // moving target. Stopping must not request throttle to overcome rolling drag.
53
+ const demand = car.mass * ((target - limits.speed) / SPEED_RESPONSE_SECONDS +
54
+ (target > 0 ? limits.resistanceDeceleration : 0));
55
+ const fullEngineForce = Math.max(limits.engineForce * limits.drsBonus, 1e-9);
56
+ const fullBrakeForce = Math.max(car.brakeNewtons, 1e-9);
57
+ return {
58
+ throttle: clamp(demand / fullEngineForce, 0,
59
+ Math.min(1, limits.driveAcceleration * car.mass * share / fullEngineForce)),
60
+ brake: clamp(-demand / fullBrakeForce, 0,
61
+ Math.min(1, limits.brakeDeceleration * car.mass * share / fullBrakeForce)),
62
+ };
63
+ }
@@ -1,9 +1,7 @@
1
1
  import { metersToSimUnits } from '../units.js';
2
2
 
3
3
  export const LANE_OFFSETS = [-8, -5.6, -3.2, 0, 3.2, 5.6, 8].map(metersToSimUnits);
4
- export const EDGE_RECOVERY_MIN_LOOKAHEAD = metersToSimUnits(28);
5
4
  export const LOOKAHEAD_BASE_DISTANCE = metersToSimUnits(52);
6
- export const LOOKAHEAD_MAX_DISTANCE = metersToSimUnits(115);
7
5
  export const BRAKING_LOOKAHEAD_MAX_DISTANCE = metersToSimUnits(560);
8
6
  export const TRAFFIC_GAP_AHEAD = metersToSimUnits(58);
9
7
  export const TRAFFIC_SIDE_GAP = metersToSimUnits(4.1);
@@ -4,7 +4,8 @@ import { decideRacingControls } from './racingControls.js';
4
4
  import { decideRejoinControls } from './rejoinControls.js';
5
5
  import { shouldContinueRejoinRecovery } from './edgeRecovery.js';
6
6
  import { decideSafetyCarControls } from './safetyCarControls.js';
7
- import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
7
+ import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
8
+ import { normalizeAngle } from '../simMath.js';
8
9
 
9
10
  export function decideDriverControls({ car, orderIndex, race }) {
10
11
  if (car.manualControls) return car.manualControls;
@@ -23,6 +24,15 @@ export function decideDriverControls({ car, orderIndex, race }) {
23
24
  return decideRejoinControls(car, race);
24
25
  }
25
26
 
27
+ // Forward path pursuit is ambiguous when the car faces away from the road.
28
+ // Let the existing recovery controller turn it around before resuming pace.
29
+ if (!isSimulatorPhysicsMode(race.physicsMode) &&
30
+ Math.abs(normalizeAngle(car.heading - car.trackState.heading)) > Math.PI / 2) {
31
+ car.rejoinRecoveryFrames = REJOIN_HOLD_FRAMES;
32
+ car.attackCommitmentFrames = 0;
33
+ return decideRejoinControls(car, race);
34
+ }
35
+
26
36
  if (shouldStabilizeSimulatorCar(car, race)) {
27
37
  car.rejoinRecoveryFrames = Math.max(car.rejoinRecoveryFrames ?? 0, Math.floor(REJOIN_HOLD_FRAMES * 0.45));
28
38
  car.attackCommitmentFrames = 0;
@@ -37,11 +47,12 @@ export function decideDriverControls({ car, orderIndex, race }) {
37
47
  }
38
48
 
39
49
  function shouldStabilizeSimulatorCar(car, race) {
40
- if (race.physicsMode !== 'advanced') return false;
41
- const gripUsage = car.gripUsage ?? 0;
50
+ if (!isSimulatorPhysicsMode(race.physicsMode)) return false;
42
51
  const slipAngle = Math.abs(car.slipAngleRadians ?? 0);
43
52
  const edgeDistance = race.track.width / 2 - VEHICLE_LIMITS.carWidth * 2.25;
44
53
  const nearEdge = (car.trackState?.crossTrackError ?? 0) > edgeDistance;
45
- const unstable = car.stabilityState === 'spin-risk' || gripUsage > 1.25 || slipAngle > 0.24;
46
- return unstable && (nearEdge || slipAngle > 0.34 || gripUsage > 1.7);
54
+ // Tire force demand can exceed capacity during a straight launch. Recovery
55
+ // responds to lost directional stability, not longitudinal demand alone.
56
+ const unstable = car.stabilityState === 'spin-risk' || slipAngle > 0.24;
57
+ return unstable && (nearEdge || slipAngle > 0.34);
47
58
  }
@@ -1,11 +1,10 @@
1
1
  import { clamp, normalizeAngle } from '../simMath.js';
2
- import { metersToSimUnits } from '../units.js';
3
2
  import { pointAt } from '../track/trackModel.js';
4
- import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
3
+ import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
5
4
  import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
6
5
 
7
6
  export function calculateTrackEdgeGuard(car, race) {
8
- const simulatorMode = race.physicsMode === 'advanced';
7
+ const simulatorMode = isSimulatorPhysicsMode(race.physicsMode);
9
8
  const trackLimit = race.track.width / 2;
10
9
  const halfCarWidth = VEHICLE_LIMITS.carWidth / 2;
11
10
  const wholeCarOutsideLimit = trackLimit + halfCarWidth;
@@ -1,399 +1,13 @@
1
- import { clamp, normalizeAngle } from '../simMath.js';
2
- import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../units.js';
3
- import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
- import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
5
- import { BRAKING_LOOKAHEAD_MAX_DISTANCE, CURVATURE_LOOKAHEAD_SAMPLES, EDGE_RECOVERY_MIN_LOOKAHEAD, LOOKAHEAD_BASE_DISTANCE, LOOKAHEAD_MAX_DISTANCE, OVERTAKE_LATERAL_MIN, PREVIEW_CURVATURE_CAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP } from './driverControlConstants.js';
6
- import { createDriverInput } from './driverInput.js';
7
- import { angleToPoint } from './driverMath.js';
8
- import { calculateTrackEdgeGuard } from './edgeRecovery.js';
9
- import { calculateActualOverlapPenalty, calculatePlannedTrafficPenalty, planRacingLine } from './racingLinePlan.js';
10
- import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
1
+ import { isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
2
+ import { decideArcadeRacingControls } from './arcadeRacingControls.js';
3
+ import { decideAdvancedRacingControls } from './advancedRacingControls.js';
11
4
 
12
- export function decideRacingControls(car, orderIndex, race) {
13
- if (race.physicsMode === 'advanced') {
14
- return decideSimulatorRacingControls(car, orderIndex, race);
15
- }
16
- return decideArcadeRacingControls(car, orderIndex, race);
17
- }
18
-
19
- export function decideArcadeRacingControls(car, orderIndex, race) {
20
- return decideRacingControlsForMode(car, orderIndex, race, {
21
- simulatorMode: false,
22
- baseLookaheadSpeedFactor: (aggression) => 1.62 - aggression * 0.12,
23
- previewLookaheadSpeedFactor: 3.15,
24
- edgeLookaheadPenaltyMeters: 18,
25
- cornerPressureCurvature: 0.001,
26
- edgeLineDamping: 0.8,
27
- edgeRecoveryWeight: 0.8,
28
- headingFeedForwardBase: (carEntry, edgeGuard) => 0.34 + carEntry.racecraft * 0.18 + edgeGuard.pressure * 0.12,
29
- headingFeedForwardLimit: 0.42,
30
- pathSteerGain: (carEntry, edgeGuard) => 0.72 + carEntry.racecraft * 0.12 + edgeGuard.pressure * 0.42,
31
- edgeRecoveryStart: 0.22,
32
- edgeRecoveryGain: 1.18,
33
- edgeRecoveryOverLimitGain: 0.55,
34
- edgeRecoveryLimit: 0.52,
35
- steeringLimitScale: () => 1,
36
- gripBudget: (carEntry, aggression) => 14.8 + carEntry.racecraft * 3.4 + (carEntry.tireEnergy ?? 100) * 0.011 + aggression * 1.8,
37
- cornerSpeedMultiplier: (aggression) => 1.12 + aggression * 0.08,
38
- paceBonus: (carEntry) => (carEntry.pace - 1) * 20,
39
- aggressionSpeedBonus: (aggression) => aggression * 10,
40
- minCornerSpeedKph: 96,
41
- maxCornerSpeedKph: 318,
42
- edgePenaltyMetersScale: (aggression) => 5.8 - aggression * 1.1,
43
- edgePenaltyPressure: 48,
44
- edgePressureSpeedPenalty: 0,
45
- edgeOutwardSpeedPenalty: 0,
46
- edgeHeadingOutwardPenalty: 0,
47
- steeringPenaltyStart: 0.38,
48
- steeringPenaltyGain: 24,
49
- steeringPenaltyMax: 12,
50
- headingPenaltyStart: 0.48,
51
- headingPenaltyGain: 42,
52
- headingPenaltyMax: 24,
53
- recoveryHeadingStart: 0.46,
54
- recoveryHeadingGain: 64,
55
- recoveryHeadingMax: 38,
56
- steeringLoadStart: 0.82,
57
- steeringLoadMax: 18,
58
- steeringLoadHeadingScale: 0.48,
59
- loadPenalty: () => 0,
60
- minimumSpeeds: { edge: 34, kerb: 58, track: 52 },
61
- desiredSpeedMaxKph: 330,
62
- minimumThrottle: (aggression) => 0.1 + aggression * 0.08,
63
- brakeLimits: { kerb: (aggression) => 0.42, track: (aggression) => 0.72 - aggression * 0.08 },
64
- brakeResponseKph: (aggression) => 28 + aggression * 11,
65
- extraGripBrake: () => 0,
66
- edgeBrakeLimitScale: 1,
67
- edgeSlideBrakeStart: 1.1,
68
- edgeSlideBrakeGain: 0,
69
- edgeSlideVelocityBrakeGain: 0,
70
- edgeSlideHeadingBrake: 0,
71
- edgeSlideBrakeLimitScale: 0,
72
- edgeSlideThrottleDamping: 0,
73
- recoveryHeadingThrottleStart: 0.38,
74
- throttleResponseKph: 16,
75
- throttleScale: () => 1,
76
- });
77
- }
5
+ export { decideArcadeRacingControls } from './arcadeRacingControls.js';
6
+ export { decideAdvancedRacingControls as decideSimulatorRacingControls } from './advancedRacingControls.js';
7
+ export { calculateRacingLineOffset, maxLookaheadCurvature } from './arcadeRacePace.js';
78
8
 
79
- export function decideSimulatorRacingControls(car, orderIndex, race) {
80
- return decideRacingControlsForMode(car, orderIndex, race, {
81
- simulatorMode: true,
82
- baseLookaheadSpeedFactor: (aggression) => 2.45 - aggression * 0.06,
83
- previewLookaheadSpeedFactor: 6.15,
84
- edgeLookaheadPenaltyMeters: 34,
85
- cornerPressureCurvature: 0.00072,
86
- edgeLineDamping: 1.55,
87
- edgeRecoveryWeight: 1.45,
88
- headingFeedForwardBase: (carEntry, edgeGuard) => 0.2 + carEntry.racecraft * 0.08 + edgeGuard.pressure * 0.07,
89
- headingFeedForwardLimit: 0.25,
90
- pathSteerGain: (carEntry, edgeGuard) => 0.46 + carEntry.racecraft * 0.06 + edgeGuard.pressure * 0.18,
91
- edgeRecoveryStart: 0.04,
92
- edgeRecoveryGain: 3.1,
93
- edgeRecoveryOverLimitGain: 1.15,
94
- edgeRecoveryLimit: 0.78,
95
- steeringLimitScale: (carEntry, speedRatio, gripUsage) => clamp(1 - speedRatio * 0.55 - gripUsage * 0.14, 0.26, 1),
96
- gripBudget: (carEntry, aggression) => 10.6 + carEntry.racecraft * 1.55 + (carEntry.tireEnergy ?? 100) * 0.006 + aggression * 0.55,
97
- cornerSpeedMultiplier: (aggression) => 0.93 + aggression * 0.035,
98
- paceBonus: (carEntry) => (carEntry.pace - 1) * 12,
99
- aggressionSpeedBonus: (aggression) => aggression * 4,
100
- minCornerSpeedKph: 58,
101
- maxCornerSpeedKph: 292,
102
- edgePenaltyMetersScale: (aggression) => 10.5 - aggression * 0.8,
103
- edgePenaltyPressure: 92,
104
- edgePressureSpeedPenalty: 32,
105
- edgeOutwardSpeedPenalty: 30,
106
- edgeHeadingOutwardPenalty: 22,
107
- steeringPenaltyStart: 0.27,
108
- steeringPenaltyGain: 42,
109
- steeringPenaltyMax: 28,
110
- headingPenaltyStart: 0.32,
111
- headingPenaltyGain: 62,
112
- headingPenaltyMax: 42,
113
- recoveryHeadingStart: 0.28,
114
- recoveryHeadingGain: 86,
115
- recoveryHeadingMax: 58,
116
- steeringLoadStart: 0.58,
117
- steeringLoadMax: 36,
118
- steeringLoadHeadingScale: 0.32,
119
- loadPenalty: (entry, gripUsage, slipAngle, cornerPressure, outwardDriftAngle) =>
120
- clamp((gripUsage - 0.52) * 92, 0, 54) +
121
- clamp(slipAngle * 130, 0, 42) +
122
- cornerPressure * 18 +
123
- clamp(outwardDriftAngle * 330, 0, 62),
124
- minimumSpeeds: { edge: 44, kerb: 58, track: 76 },
125
- desiredSpeedMaxKph: 302,
126
- minimumThrottle: (aggression) => 0.03 + aggression * 0.03,
127
- brakeLimits: { kerb: (aggression) => 0.36, track: (aggression) => 0.9 - aggression * 0.03 },
128
- brakeResponseKph: (aggression) => 15 + aggression * 5,
129
- extraGripBrake: (gripUsage, slipAngle, brakeLimit) => clamp((gripUsage - 0.68) * 0.42 + slipAngle * 0.5, 0, brakeLimit * 0.6),
130
- edgeBrakeLimitScale: 1.08,
131
- edgeSlideBrakeStart: 0.22,
132
- edgeSlideBrakeGain: 0.78,
133
- edgeSlideVelocityBrakeGain: 0.12,
134
- edgeSlideHeadingBrake: 0.1,
135
- edgeSlideBrakeLimitScale: 0.86,
136
- edgeSlideThrottleDamping: 0.9,
137
- recoveryHeadingThrottleStart: 0.3,
138
- throttleResponseKph: 18,
139
- throttleScale: (steeringLoad, gripUsage, slipAngle, edgeGuard) => clamp(
140
- 1 - steeringLoad * 0.42 - Math.max(0, gripUsage - 0.58) * 0.58 - slipAngle * 1.25 - edgeGuard.pressure * 0.42,
141
- 0,
142
- 1,
143
- ),
144
- });
145
- }
146
-
147
- function decideRacingControlsForMode(car, orderIndex, race, profile) {
148
- const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
149
- const simulatorMode = profile.simulatorMode;
150
- const edgeGuard = calculateTrackEdgeGuard(car, race);
151
- const edgeMotion = analyzeTrackEdgeMotion(car, race);
152
- const gripUsage = Number.isFinite(car.gripUsage) ? car.gripUsage : 0;
153
- const slipAngle = Math.abs(car.slipAngleRadians ?? 0);
154
- const baseLookahead = clamp(
155
- car.speed * profile.baseLookaheadSpeedFactor(aggression) +
156
- LOOKAHEAD_BASE_DISTANCE -
157
- edgeGuard.pressure * metersToSimUnits(profile.edgeLookaheadPenaltyMeters),
158
- EDGE_RECOVERY_MIN_LOOKAHEAD,
159
- LOOKAHEAD_MAX_DISTANCE,
160
- );
161
- const previewLookahead = clamp(
162
- car.speed * profile.previewLookaheadSpeedFactor + LOOKAHEAD_BASE_DISTANCE,
163
- baseLookahead,
164
- BRAKING_LOOKAHEAD_MAX_DISTANCE,
165
- );
166
- const previewCurvature = maxLookaheadCurvature(race.track, car.progress, previewLookahead);
167
- const cornerPressure = clamp(previewCurvature / profile.cornerPressureCurvature, 0, 1);
168
- const lookahead = clamp(
169
- baseLookahead * (1 - cornerPressure * 0.46),
170
- EDGE_RECOVERY_MIN_LOOKAHEAD,
171
- LOOKAHEAD_MAX_DISTANCE,
172
- );
173
- const targetBase = pointAt(race.track, car.progress + lookahead);
174
- const curvature = Math.max(
175
- car.trackState.curvature,
176
- targetBase.curvature,
177
- previewCurvature,
178
- );
179
- const lanePlan = planRacingLine(car, orderIndex, race);
180
- const cornerOffsetDamping = clamp(curvature / 0.0012, 0, 0.58);
181
- const recoveryOffset = edgeGuard.side * edgeGuard.recoveryOffset;
182
- const racingOffset = calculateRacingLineOffset(car, race, lookahead, curvature, edgeGuard);
183
- const trafficPressure = lanePlan.attackCommitted || lanePlan.sameLaneAhead || lanePlan.actualLaneAhead || lanePlan.sideRisk ? 1 : 0;
184
- const racingLineWeight = lanePlan.attackCommitted ? 0.18 : trafficPressure ? 0.52 : 1;
185
- const cornerOffsetFactor = 1 - cornerOffsetDamping * (lanePlan.attackCommitted ? 0.28 : trafficPressure ? 0.58 : 1);
186
- const targetOffset = clamp(
187
- (lanePlan.offset + racingOffset * racingLineWeight) * cornerOffsetFactor * (1 - edgeGuard.pressure * profile.edgeLineDamping) +
188
- recoveryOffset * edgeGuard.pressure * profile.edgeRecoveryWeight,
189
- -edgeGuard.recoveryOffset,
190
- edgeGuard.recoveryOffset,
191
- );
192
- const target = offsetTrackPoint(targetBase, targetOffset);
193
- const angleError = angleToPoint(car, target);
194
- const headingError = normalizeAngle(targetBase.heading - car.heading);
195
- const lateralError = car.trackState?.signedOffset ?? 0;
196
- const outwardDriftAngle = simulatorMode &&
197
- Math.sign(lateralError) !== 0 &&
198
- Math.sign(normalizeAngle(car.heading - (car.trackState?.heading ?? targetBase.heading))) === Math.sign(lateralError)
199
- ? Math.abs(normalizeAngle(car.heading - (car.trackState?.heading ?? targetBase.heading)))
200
- : 0;
201
- const cornerCommitment = clamp(Math.abs(headingError) / 0.9, 0, 0.72);
202
- const lateralCorrection = -Math.atan2(
203
- lateralError * (0.85 + edgeGuard.pressure * 0.5),
204
- Math.max(car.speed, kphToSimSpeed(35)),
205
- ) * (1 - cornerCommitment * (simulatorMode ? 0.78 : 0.62));
206
- const headingFeedForward = clamp(
207
- headingError * profile.headingFeedForwardBase(car, edgeGuard),
208
- -profile.headingFeedForwardLimit,
209
- profile.headingFeedForwardLimit,
210
- );
211
- let pathSteer = angleError * profile.pathSteerGain(car, edgeGuard) + headingFeedForward;
212
- if (edgeGuard.side !== 0 && Math.sign(pathSteer) === edgeGuard.side) {
213
- pathSteer *= 1 - edgeGuard.pressure * 0.85;
214
- }
215
- const edgeRecoverySteer = -edgeGuard.side * clamp(
216
- (edgeGuard.pressure - profile.edgeRecoveryStart) * profile.edgeRecoveryGain +
217
- edgeGuard.overLimitPressure * profile.edgeRecoveryOverLimitGain +
218
- (simulatorMode ? outwardDriftAngle * clamp(Math.abs(lateralError) / (race.track.width * 0.26), 0, 1) * 1.8 : 0),
219
- 0,
220
- profile.edgeRecoveryLimit,
221
- );
222
- const speedRatio = clamp(car.speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
223
- const simulatorSteeringLimit = VEHICLE_LIMITS.maxSteer *
224
- profile.steeringLimitScale(car, speedRatio, gripUsage);
225
- const steeringLimit = simulatorMode
226
- ? (car.trackState.surface === 'kerb' ? simulatorSteeringLimit * 0.82 : simulatorSteeringLimit)
227
- : car.trackState.surface === 'kerb'
228
- ? VEHICLE_LIMITS.maxSteer * 0.96
229
- : VEHICLE_LIMITS.maxSteer;
230
- let steeringRequest = clamp(pathSteer + lateralCorrection + edgeRecoverySteer, -steeringLimit, steeringLimit);
231
- if (simulatorMode && edgeGuard.pressure > 0.62 && Math.sign(steeringRequest) === edgeGuard.side) {
232
- steeringRequest = clamp(
233
- -edgeGuard.side * Math.max(Math.abs(steeringRequest), steeringLimit * 0.55),
234
- -steeringLimit,
235
- steeringLimit,
236
- );
237
- }
238
- const cornerRadiusMeters = simUnitsToMeters(1 / Math.max(curvature, 1e-7));
239
- const lateralGripBudget = profile.gripBudget(car, aggression);
240
- const cornerTargetKph = clamp(
241
- Math.sqrt(lateralGripBudget * cornerRadiusMeters) * 3.6 *
242
- profile.cornerSpeedMultiplier(aggression) +
243
- profile.paceBonus(car) + profile.aggressionSpeedBonus(aggression),
244
- profile.minCornerSpeedKph,
245
- profile.maxCornerSpeedKph,
246
- );
247
- const edgePenalty = edgeGuard.pressure > 0.42
248
- ? simUnitsToMeters(Math.max(0, car.trackState.crossTrackError - edgeGuard.softLimit)) *
249
- profile.edgePenaltyMetersScale(aggression) +
250
- edgeGuard.overLimitPressure * profile.edgePenaltyPressure
251
- : edgeGuard.overLimitPressure * profile.edgePenaltyPressure +
252
- edgeGuard.pressure * profile.edgePressureSpeedPenalty;
253
- const edgeMotionPenalty =
254
- edgeMotion.outwardSpeedMps * profile.edgeOutwardSpeedPenalty * clamp(edgeGuard.pressure, 0, 1) +
255
- (edgeMotion.headingOutward ? profile.edgeHeadingOutwardPenalty * clamp(edgeGuard.pressure, 0, 1) : 0);
256
- const steeringPenalty = clamp((Math.abs(angleError) - profile.steeringPenaltyStart) * profile.steeringPenaltyGain, 0, profile.steeringPenaltyMax);
257
- const headingPenalty = clamp((Math.abs(headingError) - profile.headingPenaltyStart) * profile.headingPenaltyGain, 0, profile.headingPenaltyMax);
258
- const recoveryAlignmentPenalty = edgeGuard.pressure > 0.2
259
- ? clamp((Math.abs(headingError) - profile.recoveryHeadingStart) * profile.recoveryHeadingGain, 0, profile.recoveryHeadingMax)
260
- : 0;
261
- const steeringLoadPenalty = clamp(
262
- (Math.abs(steeringRequest) / Math.max(steeringLimit, 1e-6) - profile.steeringLoadStart) * 42,
263
- 0,
264
- profile.steeringLoadMax,
265
- ) * clamp(Math.abs(headingError) / profile.steeringLoadHeadingScale, 0, 1);
266
- const simulatorLoadPenalty = profile.loadPenalty(car, gripUsage, slipAngle, cornerPressure, outwardDriftAngle);
267
- const actualLaneLateral = lanePlan.actualLaneAhead
268
- ? Math.abs(lanePlan.actualLaneAhead.signedOffset - (car.trackState?.signedOffset ?? 0))
269
- : Infinity;
270
- const plannedPassingOverlap = lanePlan.attackCommitted && lanePlan.sameLaneAhead
271
- ? Math.abs(lanePlan.sameLaneAhead.signedOffset - (car.trackState?.signedOffset ?? 0)) > OVERTAKE_LATERAL_MIN
272
- : false;
273
- const trafficPenalty = Math.max(
274
- lanePlan.sameLaneAhead
275
- ? calculatePlannedTrafficPenalty(lanePlan.sameLaneAhead, plannedPassingOverlap)
276
- : 0,
277
- lanePlan.actualLaneAhead && actualLaneLateral < OVERTAKE_LATERAL_MIN
278
- ? calculateActualOverlapPenalty(car, lanePlan.actualLaneAhead)
279
- : 0,
280
- lanePlan.sideRisk ? clamp(simUnitsToMeters(TRAFFIC_SIDE_GAP - lanePlan.sideRisk.lateral) * 0.78, 0, 30) : 0,
281
- ) * (1 - aggression * 0.28);
282
- const minimumDesiredSpeedKph = edgeGuard.pressure > 0.55
283
- ? profile.minimumSpeeds.edge
284
- : car.trackState.surface === 'kerb' ? profile.minimumSpeeds.kerb : profile.minimumSpeeds.track;
285
- const desiredSpeedKph = clamp(
286
- (car.drsActive ? cornerTargetKph + 22 : cornerTargetKph) -
287
- edgePenalty -
288
- edgeMotionPenalty -
289
- steeringPenalty -
290
- headingPenalty -
291
- recoveryAlignmentPenalty -
292
- steeringLoadPenalty -
293
- simulatorLoadPenalty -
294
- trafficPenalty,
295
- minimumDesiredSpeedKph,
296
- profile.desiredSpeedMaxKph,
297
- );
298
- const desiredSpeed = clamp(
299
- kphToSimSpeed(desiredSpeedKph),
300
- kphToSimSpeed(minimumDesiredSpeedKph),
301
- VEHICLE_LIMITS.maxSpeed,
302
- );
303
- const speedError = desiredSpeed - car.speed;
304
- const minimumThrottle = edgeGuard.pressure > 0.2 ? 0 : profile.minimumThrottle(aggression);
305
- const brakeLimit = simulatorMode
306
- ? car.trackState.surface === 'kerb'
307
- ? profile.brakeLimits.kerb(aggression)
308
- : profile.brakeLimits.track(aggression)
309
- : car.trackState.surface === 'kerb'
310
- ? profile.brakeLimits.kerb(aggression)
311
- : profile.brakeLimits.track(aggression);
312
- let brakeAmount = speedError < -kphToSimSpeed(3)
313
- ? clamp(Math.abs(speedError) / kphToSimSpeed(profile.brakeResponseKph(aggression)), 0, brakeLimit)
314
- : 0;
315
- const simulatorBrakeRisk = cornerPressure > 0.15 ||
316
- edgeGuard.pressure > 0.2 ||
317
- outwardDriftAngle > 0.03 ||
318
- car.speed > kphToSimSpeed(132) ||
319
- speedError < kphToSimSpeed(8);
320
- if (simulatorMode && simulatorBrakeRisk && (gripUsage > 0.9 || slipAngle > 0.12)) {
321
- brakeAmount = Math.max(brakeAmount, profile.extraGripBrake(gripUsage, slipAngle, brakeLimit));
322
- }
323
- if (simulatorMode && car.speed < kphToSimSpeed(58) && edgeGuard.pressure < 0.75) {
324
- brakeAmount *= 0.25;
325
- }
326
- if (edgeGuard.pressure > 0.46 && car.speed > kphToSimSpeed(48)) {
327
- brakeAmount = Math.max(brakeAmount, clamp((edgeGuard.pressure - 0.42) * 0.9 * profile.edgeBrakeLimitScale, 0, brakeLimit));
328
- }
329
- if (edgeGuard.pressure > profile.edgeSlideBrakeStart && edgeMotion.outwardSpeedMps > 0.25) {
330
- brakeAmount = Math.max(
331
- brakeAmount,
332
- clamp(
333
- (edgeGuard.pressure - profile.edgeSlideBrakeStart) * profile.edgeSlideBrakeGain +
334
- edgeMotion.outwardSpeedMps * profile.edgeSlideVelocityBrakeGain +
335
- (edgeMotion.headingOutward ? profile.edgeSlideHeadingBrake : 0),
336
- 0,
337
- brakeLimit * profile.edgeSlideBrakeLimitScale,
338
- ),
339
- );
340
- }
341
- const recoveryThrottleScale = edgeGuard.pressure > 0.24
342
- ? 1 - clamp((Math.abs(headingError) - profile.recoveryHeadingThrottleStart) / 0.92, 0, 0.82)
343
- : 1;
344
- const throttleRequest = speedError > kphToSimSpeed(1)
345
- ? clamp(speedError / kphToSimSpeed(profile.throttleResponseKph), minimumThrottle, 1)
346
- : 0;
347
- const steeringLoad = Math.abs(steeringRequest) / Math.max(steeringLimit, 1e-6);
348
- const simulatorThrottleScale = profile.throttleScale(steeringLoad, gripUsage, slipAngle, edgeGuard);
349
- const edgeSlideThrottleScale = 1 - clamp(
350
- (
351
- Math.max(0, edgeGuard.pressure - 0.18) +
352
- edgeMotion.outwardSpeedMps * 0.16 +
353
- (edgeMotion.headingOutward ? 0.18 : 0)
354
- ) * profile.edgeSlideThrottleDamping,
355
- 0,
356
- 0.92,
357
- );
358
-
359
- return createDriverInput()
360
- .steer(steeringRequest)
361
- .accelerate(brakeAmount > 0.05 ? 0 : throttleRequest * recoveryThrottleScale * simulatorThrottleScale * edgeSlideThrottleScale)
362
- .brake(brakeAmount)
363
- .controls();
364
- }
365
-
366
- export function maxLookaheadCurvature(track, progress, lookahead) {
367
- let maximum = 0;
368
- let previous = pointAt(track, progress);
369
- for (let index = 1; index <= CURVATURE_LOOKAHEAD_SAMPLES; index += 1) {
370
- const sample = pointAt(track, progress + lookahead * (index / CURVATURE_LOOKAHEAD_SAMPLES));
371
- const segmentDistance = Math.max(1, lookahead / CURVATURE_LOOKAHEAD_SAMPLES);
372
- const segmentCurvature = Math.abs(normalizeAngle(sample.heading - previous.heading)) / segmentDistance;
373
- maximum = Math.max(maximum, Math.abs(sample.curvature ?? 0), Math.min(segmentCurvature, PREVIEW_CURVATURE_CAP));
374
- previous = sample;
375
- }
376
- return maximum;
377
- }
378
-
379
- export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeGuard) {
380
- const cornerStrength = clamp((curvature - 0.00012) / 0.00095, 0, 1);
381
- if (cornerStrength <= 0) return 0;
382
-
383
- const entry = pointAt(race.track, car.progress - lookahead * 0.35);
384
- const apex = pointAt(race.track, car.progress + lookahead * 0.55);
385
- const exit = pointAt(race.track, car.progress + lookahead * 1.25);
386
- const signedEntry = normalizeAngle(apex.heading - entry.heading);
387
- const signedExit = normalizeAngle(exit.heading - apex.heading);
388
- const turnDirection = Math.sign(Math.abs(signedEntry) > Math.abs(signedExit) ? signedEntry : signedExit);
389
- if (turnDirection === 0) return 0;
390
-
391
- const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
392
- const simulatorMode = race.physicsMode === 'advanced';
393
- const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
394
- (simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
395
- const apexOffset = turnDirection * safeEdge *
396
- (simulatorMode ? 0.42 + aggression * 0.05 : 0.72 + aggression * 0.08);
397
-
398
- return apexOffset * cornerStrength * (1 - edgeGuard.pressure * (simulatorMode ? 0.82 : 0.65));
9
+ export function decideRacingControls(car, orderIndex, race) {
10
+ return isSimulatorPhysicsMode(race.physicsMode)
11
+ ? decideAdvancedRacingControls(car, orderIndex, race)
12
+ : decideArcadeRacingControls(car, orderIndex, race);
399
13
  }