@inventure71/paddockjs 4.2.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 (249) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INSTALL_AND_UPDATE.md +23 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  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/grid_walk.md +80 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +36 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +23 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
@@ -0,0 +1,112 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { metersToSimUnits, simUnitsToMeters, TOP_SPEED_SIM_UNITS_PER_SECOND } from '../units.js';
3
+ import { pointAtInto } from '../track/trackModel.js';
4
+ import { tirePerformanceFactor } from '../vehicle/vehiclePhysics.js';
5
+ import { advancedDriveForce } from '../vehicle/advancedVehicleModel.js';
6
+ import { createAdvancedRacingEnvelopeScratch, estimateAdvancedRacingLateralCapacity,
7
+ estimateAdvancedRacingLongitudinalLimits, estimateAdvancedRacingBrakeForce } from './advancedRacingEnvelope.js';
8
+ import { racingDriverProfile } from './racingDriverProfile.js';
9
+ import { advancedRacingPreviewReserveMeters } from './advancedRacingTiming.js';
10
+ import { followingSpeedLimit } from './trafficScan.js';
11
+
12
+ const MAX_SPEED = simUnitsToMeters(TOP_SPEED_SIM_UNITS_PER_SECOND);
13
+ const SAMPLE_SPACING = 12; // meters
14
+ const PREVIEW_POINT_SCRATCH = { x: 0, y: 0, heading: 0, normalX: 0, normalY: 0, curvature: 0, distance: 0 };
15
+ const CAPACITY_SCRATCH = createAdvancedRacingEnvelopeScratch();
16
+ const LONGITUDINAL_SCRATCH = createAdvancedRacingEnvelopeScratch();
17
+
18
+ export function advancedCornerSpeed(car, curvature, tireFactor, profile = racingDriverProfile(car)) {
19
+ return cornerSpeedWithEndpoints(car, curvature, tireFactor, profile);
20
+ }
21
+
22
+ function endpointCapacity(car, speed, tireFactor, direction, endpoints) {
23
+ if (!endpoints || (speed !== MAX_SPEED && speed !== 0)) {
24
+ return estimateAdvancedRacingLateralCapacity(car, speed, tireFactor, CAPACITY_SCRATCH, { turnDirection: direction });
25
+ }
26
+ const key = `${direction}:${speed === MAX_SPEED ? 'maximum' : 'zero'}`;
27
+ return endpoints[key] ??= estimateAdvancedRacingLateralCapacity(car, speed, tireFactor, CAPACITY_SCRATCH, { turnDirection: direction });
28
+ }
29
+
30
+ function cornerSpeedWithEndpoints(car, curvature, tireFactor, profile, endpoints = null) {
31
+ const curve = Math.abs(curvature);
32
+ if (curve < 1e-6) return MAX_SPEED;
33
+ const direction = Math.sign(curvature);
34
+ let high = MAX_SPEED ** 2;
35
+ let highValue = profile.lateralShare * endpointCapacity(car, MAX_SPEED, tireFactor, direction, endpoints) - curve * high;
36
+ if (highValue >= 0) return MAX_SPEED;
37
+ let low = 0;
38
+ let lowValue = profile.lateralShare * endpointCapacity(car, 0, tireFactor, direction, endpoints);
39
+ let speed = 0;
40
+ // Keep an explicit feasible/infeasible bracket: mixed surfaces and load
41
+ // transfer must not inherit the old static-capacity concavity assumption.
42
+ for (let iteration = 0; iteration < 8; iteration += 1) {
43
+ const next = low + (high - low) * lowValue / (lowValue - highValue);
44
+ const nextSpeed = Math.sqrt(next);
45
+ const value = profile.lateralShare * endpointCapacity(car, nextSpeed, tireFactor, direction, endpoints) - curve * next;
46
+ if (value < 0) { high = next; highValue = value; }
47
+ else {
48
+ const difference = nextSpeed - speed;
49
+ low = next; lowValue = value; speed = nextSpeed;
50
+ if (difference < 0.01) return speed;
51
+ }
52
+ }
53
+ return speed;
54
+ }
55
+
56
+ export function advancedRaceTargetSpeed(car, track, pathCurvature, profile = racingDriverProfile(car)) {
57
+ const speed = simUnitsToMeters(car.speed);
58
+ const tireFactor = tirePerformanceFactor(car.tireEnergy ?? 100);
59
+ const horizon = Math.max(80, speed * 3);
60
+ const anticipationDistance = advancedRacingPreviewReserveMeters(speed, profile, SAMPLE_SPACING);
61
+ let targetSpeed = MAX_SPEED;
62
+ // Endpoints depend on the car's current tires, surfaces and aero, but not
63
+ // preview curvature. Reuse them only during this synchronous decision.
64
+ const endpoints = Object.create(null);
65
+ const farDistance = Math.ceil(horizon / SAMPLE_SPACING) * SAMPLE_SPACING;
66
+ let futureHeading = pointAtInto(track, car.progress + metersToSimUnits(farDistance + SAMPLE_SPACING), PREVIEW_POINT_SCRATCH).heading;
67
+ // Work backward from future corners. Reserve lateral grip when estimating
68
+ // braking; the lower future speed also gives a conservative aero estimate.
69
+ // Anticipation and judgement precision depend on the existing driver skills.
70
+ for (let distance = farDistance; distance >= 0; distance -= SAMPLE_SPACING) {
71
+ const sample = pointAtInto(track, car.progress + metersToSimUnits(distance), PREVIEW_POINT_SCRATCH);
72
+ // Track samples store curvature magnitude; recover turn direction from
73
+ // consecutive tangents for asymmetric wheel-surface/load estimates.
74
+ const turnDirection = Math.sign(normalizeAngle(futureHeading - sample.heading)) || Math.sign(pathCurvature) || 1;
75
+ const curve = sample.curvature * metersToSimUnits(1) * turnDirection;
76
+ futureHeading = sample.heading;
77
+ const corner = cornerSpeedWithEndpoints(car, curve, tireFactor, profile, endpoints);
78
+ const futureSpeed = Math.min(targetSpeed, corner);
79
+ const limits = estimateAdvancedRacingBrakeForce(car, futureSpeed, tireFactor, futureSpeed ** 2 * curve, LONGITUDINAL_SCRATCH);
80
+ const deceleration = (limits.brakeForce * profile.longitudinalShare + limits.brakeResistanceForce) / car.mass;
81
+ // Inspect every nearby bend, including those inside the reaction window.
82
+ // Only distance beyond pursuit/response/driver anticipation can be spent
83
+ // braking. Shifting the sample origin would silently skip nearby corners.
84
+ const usableDistance = Math.max(0, distance + SAMPLE_SPACING - anticipationDistance) -
85
+ Math.max(0, distance - anticipationDistance);
86
+ targetSpeed = Math.min(corner, Math.sqrt(futureSpeed ** 2 + 2 * deceleration * usableDistance));
87
+ }
88
+ // Passing and path correction may demand a tighter turn than the centerline.
89
+ return Math.min(MAX_SPEED, Math.min(targetSpeed, cornerSpeedWithEndpoints(car, pathCurvature, tireFactor, profile, endpoints)) * profile.speedJudgementScale);
90
+ }
91
+
92
+ export function advancedRacingSpeedControls(car, targetSpeed, profile = racingDriverProfile(car)) {
93
+ const speed = simUnitsToMeters(car.speed);
94
+ const actualLateral = car.lateralAcceleration ?? speed * (car.yawRate ?? 0);
95
+ const limits = estimateAdvancedRacingLongitudinalLimits(car, speed, tirePerformanceFactor(car.tireEnergy ?? 100), actualLateral, LONGITUDINAL_SCRATCH);
96
+ const resistance = targetSpeed > 0 ? limits.resistanceForce : 0;
97
+ // Request net acceleration, then add the force needed merely to hold speed.
98
+ // Both pedals share one signed demand so a slowdown cannot request throttle
99
+ // and braking together. Tire/engine limits remain those of the actual car.
100
+ const responseSeconds = 0.05 + 0.045 * (1 - profile.ability);
101
+ const requestedForce = car.mass * (targetSpeed - speed) / responseSeconds + resistance;
102
+ return {
103
+ throttle: clamp(requestedForce / advancedDriveForce(car, speed), 0,
104
+ Math.min(1, limits.driveForce * profile.longitudinalShare / advancedDriveForce(car, speed))),
105
+ brake: clamp(-requestedForce / car.brakeNewtons, 0,
106
+ Math.min(1, limits.brakeForce * profile.longitudinalShare / car.brakeNewtons)),
107
+ };
108
+ }
109
+
110
+ export function advancedFollowingSpeed(car, cars, trackLength) {
111
+ return followingSpeedLimit(car, cars, trackLength);
112
+ }
@@ -0,0 +1,61 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
3
+ import { pointAtInto } from '../track/trackModel.js';
4
+ import { VEHICLE_LIMITS, tirePerformanceFactor } from '../vehicle/vehiclePhysics.js';
5
+ import { advancedRaceTargetSpeed, advancedRacingSpeedControls } from './advancedRacePace.js';
6
+ import { advancedLateralAccelerationLimit } from '../vehicle/advancedTireForces.js';
7
+ import { mainTrackCenterOffset } from './trafficScan.js';
8
+ import { createDriverInput } from './driverInput.js';
9
+ import { advancedRacingPathCurvature, advancedRacingSteeringToPoint } from './advancedRacingSteering.js';
10
+ import { advancedRacingLookaheadMeters } from './advancedRacingTiming.js';
11
+ import { racingDriverProfile } from './racingDriverProfile.js';
12
+ import { planRacingLine, calculateRacingTrafficPenalty } from './racingLinePlan.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 decideAdvancedRacingControls(car, orderIndex, race) {
18
+ const speed = simUnitsToMeters(car.speed);
19
+ const profile = racingDriverProfile(car);
20
+ const lane = planRacingLine(car, orderIndex, race);
21
+ const lookahead = advancedRacingLookaheadMeters(speed);
22
+ const target = pointAtInto(race.track, car.progress + metersToSimUnits(lookahead), TARGET_BASE_SCRATCH);
23
+ const offsetLimit = race.track.width / 2 - VEHICLE_LIMITS.carWidth;
24
+ const offset = clamp(
25
+ clamp(advancedRacingOffset(car, lane.offset + metersToSimUnits(profile.lineErrorMeters), race.track.width, profile), lane.minimumOffset, lane.maximumOffset),
26
+ -offsetLimit, offsetLimit,
27
+ );
28
+ TARGET_POINT_SCRATCH.x = target.x + target.normalX * offset;
29
+ TARGET_POINT_SCRATCH.y = target.y + target.normalY * offset;
30
+ const pathCurvature = advancedRacingPathCurvature(car, TARGET_POINT_SCRATCH);
31
+
32
+ let targetSpeed = advancedRaceTargetSpeed(car, race.track, pathCurvature, profile);
33
+ const trafficPenalty = calculateRacingTrafficPenalty(car, lane) * (1 - (car.aggression ?? 0.5) * 0.28);
34
+ targetSpeed = Math.max(0, targetSpeed - trafficPenalty / 3.6);
35
+ // A passing intention does not create physical clearance. Match a slower
36
+ // car until the actual footprints separate, including enough reaction gap.
37
+ targetSpeed = Math.min(targetSpeed, lane.followingSpeed);
38
+ const { throttle, brake } = advancedRacingSpeedControls(car, targetSpeed, profile);
39
+ const { steering } = advancedRacingSteeringToPoint(car, TARGET_POINT_SCRATCH, profile, { throttle, brake });
40
+ return createDriverInput().steer(steering).accelerate(throttle).brake(brake).controls();
41
+ }
42
+
43
+ export function advancedRacingOffset(car, plannedOffset, trackWidth, profile = racingDriverProfile(car)) {
44
+ const speed = simUnitsToMeters(car.speed);
45
+ const capacity = advancedLateralAccelerationLimit(car, speed, tirePerformanceFactor(car.tireEnergy ?? 100));
46
+ const cornerUsage = clamp(Math.abs(speed * (car.yawRate ?? 0)) / (capacity * profile.lateralShare * 0.9), 0, 1);
47
+ const currentOffset = mainTrackCenterOffset(car);
48
+ // Hold a consistent line when cornering consumes the racing grip budget.
49
+ // As the car unwinds on exit, progressively allow the planned lane change.
50
+ const comfortLimit = Math.max(0, trackWidth / 2 - VEHICLE_LIMITS.carWidth * 2);
51
+ const edgePressure = clamp((Math.abs(currentOffset) - comfortLimit) / VEHICLE_LIMITS.carWidth, 0, 1);
52
+ const needsEdgeCorrection = cornerUsage > 0.5 && edgePressure > 0;
53
+ if (needsEdgeCorrection) {
54
+ // Holding a loaded corner line must not preserve a developing excursion.
55
+ // Correct inward on the current side before considering a cross-track pass.
56
+ plannedOffset = Math.sign(currentOffset) * Math.min(Math.abs(plannedOffset), comfortLimit);
57
+ }
58
+ const laneChangeShare = Math.max(1 - cornerUsage ** 2, needsEdgeCorrection ? edgePressure : 0);
59
+ const offsetLimit = trackWidth / 2 - VEHICLE_LIMITS.carWidth;
60
+ return clamp(currentOffset + (plannedOffset - currentOffset) * laneChangeShare, -offsetLimit, offsetLimit);
61
+ }
@@ -0,0 +1,255 @@
1
+ import { advancedContactCapacity, advancedNormalLoads } from '../vehicle/advancedTireForces.js';
2
+ import { ADVANCED_VEHICLE_MODEL as MODEL, ADVANCED_WHEELS, advancedWheelSurface, advancedDragFactor } from '../vehicle/advancedVehicleModel.js';
3
+
4
+ const SOLVE_PASSES = 3;
5
+ const FEASIBILITY_MARGIN = 0.999;
6
+ const INVOCATION_INPUTS = Symbol('racingEnvelopeInvocationInputs');
7
+
8
+ function prepareInvocationInputs(car, speedMps, scratch) {
9
+ // Buffers persist with scratch; their contents are refreshed unconditionally
10
+ // for this invocation. No car reference or identity-based validity is stored.
11
+ let inputs = scratch[INVOCATION_INPUTS];
12
+ if (!inputs) {
13
+ inputs = { surfaces: [null, null, null, null], nominalLoads: [0, 0, 0, 0] };
14
+ Object.defineProperty(scratch, INVOCATION_INPUTS, { value: inputs });
15
+ }
16
+ inputs.uniformRolling = true;
17
+ inputs.strongestSurface = null;
18
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
19
+ const wheel = ADVANCED_WHEELS[index];
20
+ const surface = advancedWheelSurface(car, wheel.id);
21
+ inputs.surfaces[index] = surface;
22
+ inputs.nominalLoads[index] = car.mass * MODEL.gravity * (wheel.front ? MODEL.frontWeightFraction : 1 - MODEL.frontWeightFraction) / 2;
23
+ if (index > 0 && surface.rolling !== inputs.surfaces[0].rolling) inputs.uniformRolling = false;
24
+ if (!inputs.strongestSurface || surface.grip > inputs.strongestSurface.grip) inputs.strongestSurface = surface;
25
+ }
26
+ inputs.staticWeight = car.mass * MODEL.gravity;
27
+ inputs.tireGrip = car.tireGrip;
28
+ inputs.dragForce = advancedDragFactor(car) * speedMps ** 2;
29
+ return inputs;
30
+ }
31
+
32
+ export function createAdvancedRacingEnvelopeScratch() {
33
+ return {
34
+ loads: [0, 0, 0, 0],
35
+ capacities: [0, 0, 0, 0],
36
+ lateralForces: [0, 0, 0, 0],
37
+ longitudinalRemaining: [0, 0, 0, 0],
38
+ driveRemaining: [0, 0, 0, 0],
39
+ brakeRemaining: [0, 0, 0, 0],
40
+ };
41
+ }
42
+
43
+ // The scratch is owned by one synchronous policy decision, never by a car
44
+ // identity cache. Lateral force is yaw-balanced between the two equal-length
45
+ // axles, then apportioned within each axle by contact capacity.
46
+ export function writeAdvancedRacingWheelEnvelope(car, speedMps, tireFactor, scratch, {
47
+ longitudinalAcceleration = 0,
48
+ lateralAcceleration = 0,
49
+ } = {}) {
50
+ const inputs = prepareInvocationInputs(car, speedMps, scratch);
51
+ return writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, longitudinalAcceleration, lateralAcceleration);
52
+ }
53
+
54
+ function writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, longitudinalAcceleration = 0, lateralAcceleration = 0) {
55
+ const loads = advancedNormalLoads(car, speedMps, longitudinalAcceleration, lateralAcceleration);
56
+ let frontCapacity = 0;
57
+ let rearCapacity = 0;
58
+ let rollingResistance = 0;
59
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
60
+ const wheel = ADVANCED_WHEELS[index];
61
+ const surface = inputs.surfaces[index];
62
+ const nominalLoad = inputs.nominalLoads[index];
63
+ const capacity = advancedContactCapacity(loads[index], nominalLoad, inputs.tireGrip, tireFactor, surface);
64
+ scratch.loads[index] = loads[index];
65
+ scratch.capacities[index] = capacity;
66
+ if (wheel.front) frontCapacity += capacity;
67
+ else rearCapacity += capacity;
68
+ rollingResistance += surface.rolling * loads[index];
69
+ }
70
+ const lateralAxleForce = Math.abs(lateralAcceleration) * car.mass / 2;
71
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
72
+ const capacity = scratch.capacities[index];
73
+ const axleCapacity = ADVANCED_WHEELS[index].front ? frontCapacity : rearCapacity;
74
+ const lateralForce = axleCapacity > 0 ? lateralAxleForce * capacity / axleCapacity : lateralAxleForce / 2;
75
+ scratch.lateralForces[index] = Math.sign(lateralAcceleration) * lateralForce;
76
+ scratch.longitudinalRemaining[index] = Math.sqrt(Math.max(0, capacity ** 2 - lateralForce ** 2));
77
+ }
78
+ scratch.frontCapacity = frontCapacity;
79
+ scratch.rearCapacity = rearCapacity;
80
+ scratch.lateralCapacity = 2 * Math.min(frontCapacity, rearCapacity) / car.mass;
81
+ scratch.lateralFeasible = lateralAxleForce <= Math.min(frontCapacity, rearCapacity);
82
+ scratch.resistanceForce = rollingResistance + inputs.dragForce;
83
+ scratch.uniformRolling = inputs.uniformRolling;
84
+ return scratch;
85
+ }
86
+
87
+ export function estimateAdvancedRacingLateralCapacity(car, speedMps, tireFactor, scratch, {
88
+ turnDirection = Math.sign(car.yawRate || car.lateralAcceleration || 1),
89
+ } = {}) {
90
+ const direction = turnDirection < 0 ? -1 : 1;
91
+ const inputs = prepareInvocationInputs(car, speedMps, scratch);
92
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs);
93
+ let candidate = scratch.lateralCapacity;
94
+ let previous = candidate;
95
+ for (let pass = 0; pass < SOLVE_PASSES; pass += 1) {
96
+ previous = candidate;
97
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, 0, direction * candidate);
98
+ candidate = scratch.lateralCapacity;
99
+ }
100
+ let conservative = Math.min(candidate, previous) * FEASIBILITY_MARGIN;
101
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, 0, direction * conservative);
102
+ // A bound must fit its own predicted load transfer, including mixed surfaces.
103
+ // One final conservative projection handles unusual rating/load extremes.
104
+ if (!scratch.lateralFeasible) {
105
+ conservative = Math.min(conservative, scratch.lateralCapacity) * FEASIBILITY_MARGIN;
106
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, 0, direction * conservative);
107
+ }
108
+ if (!scratch.lateralFeasible) return 0;
109
+ return conservative;
110
+ }
111
+
112
+ function availableLongitudinalForce(scratch, braking) {
113
+ if (!scratch.lateralFeasible) return 0;
114
+ if (!braking) return 2 * Math.min(scratch.longitudinalRemaining[2], scratch.longitudinalRemaining[3]);
115
+ let force = Infinity;
116
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
117
+ const fraction = (ADVANCED_WHEELS[index].front ? MODEL.frontBrakeFraction : 1 - MODEL.frontBrakeFraction) / 2;
118
+ force = Math.min(force, scratch.longitudinalRemaining[index] / fraction);
119
+ }
120
+ return force;
121
+ }
122
+
123
+ function writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, force, braking, inputs) {
124
+ const signedForce = braking ? -force : force;
125
+ // Wheel force changes longitudinal load transfer. Drag and rolling oppose
126
+ // forward motion and contribute to the net acceleration seen by physics.
127
+ const passes = scratch.uniformRolling ? 1 : 3;
128
+ for (let pass = 0; pass < passes; pass += 1) {
129
+ const resistance = scratch.resistanceForce;
130
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, (signedForce - resistance) / car.mass, lateralAcceleration);
131
+ }
132
+ }
133
+
134
+ function longitudinalMargin(scratch, force, braking) {
135
+ let margin = Infinity;
136
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
137
+ const wheel = ADVANCED_WHEELS[index];
138
+ if (!braking && wheel.front) {
139
+ margin = Math.min(margin, scratch.capacities[index] - Math.abs(scratch.lateralForces[index]));
140
+ continue;
141
+ }
142
+ const fraction = braking ? (wheel.front ? MODEL.frontBrakeFraction : 1 - MODEL.frontBrakeFraction) / 2 : 0.5;
143
+ const squared = scratch.capacities[index] ** 2 - scratch.lateralForces[index] ** 2;
144
+ // Keep a signed continuous residual when lateral demand is infeasible.
145
+ // Clamping this to zero would hide feasible nonzero load-transfer windows.
146
+ const remaining = Math.sign(squared) * Math.sqrt(Math.abs(squared));
147
+ margin = Math.min(margin, remaining / fraction - force);
148
+ }
149
+ return margin;
150
+ }
151
+
152
+ function longitudinalForceUpper(car, tireFactor, scratch, inputs) {
153
+ let totalLoad = 0;
154
+ for (let index = 0; index < scratch.loads.length; index += 1) totalLoad += scratch.loads[index];
155
+ // Concavity bounds the sum of contact capacities by distributing normal
156
+ // load in proportion to nominal load, with the strongest surface at all
157
+ // contacts. This bounds force without imposing engine or brake hardware.
158
+ return advancedContactCapacity(totalLoad, inputs.staticWeight, inputs.tireGrip, tireFactor, inputs.strongestSurface);
159
+ }
160
+
161
+ function bracketLongitudinalForce(car, speedMps, tireFactor, lateralAcceleration, scratch, braking, candidate, inputs) {
162
+ const upper = longitudinalForceUpper(car, tireFactor, scratch, inputs);
163
+ const marginAt = (force) => {
164
+ writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, force, braking, inputs);
165
+ return longitudinalMargin(scratch, force, braking);
166
+ };
167
+ let low = candidate;
168
+ if (!(low > 0) || marginAt(low) < 0) {
169
+ low = braking ? Math.min(upper, Math.max(0, (car.brake ?? 0) * car.brakeNewtons)) : 0;
170
+ if (marginAt(low) < 0) {
171
+ // Zero force need not be feasible: braking can load an otherwise weak
172
+ // front axle. Find a feasible seed before bracketing its upper root.
173
+ let left = 0;
174
+ let right = upper;
175
+ const ratio = (Math.sqrt(5) - 1) / 2;
176
+ let first = right - (right - left) * ratio;
177
+ let second = left + (right - left) * ratio;
178
+ let firstMargin = marginAt(first);
179
+ let secondMargin = marginAt(second);
180
+ for (let iteration = 0; iteration < 20; iteration += 1) {
181
+ if (firstMargin < secondMargin) {
182
+ left = first; first = second; firstMargin = secondMargin;
183
+ second = left + (right - left) * ratio; secondMargin = marginAt(second);
184
+ } else {
185
+ right = second; second = first; secondMargin = firstMargin;
186
+ first = right - (right - left) * ratio; firstMargin = marginAt(first);
187
+ }
188
+ }
189
+ low = firstMargin >= secondMargin ? first : second;
190
+ if (marginAt(low) < 0) return 0;
191
+ }
192
+ }
193
+ let high = upper;
194
+ for (let iteration = 0; iteration < 16; iteration += 1) {
195
+ const middle = (low + high) / 2;
196
+ if (marginAt(middle) >= 0) low = middle;
197
+ else high = middle;
198
+ }
199
+ // Shrinking toward zero can leave a disconnected feasible interval. Keep
200
+ // the validated feasible endpoint instead of applying a blanket force scale.
201
+ marginAt(low);
202
+ return low;
203
+ }
204
+
205
+ function solveLongitudinalForce(car, speedMps, tireFactor, lateralAcceleration, scratch, braking, remaining, inputs) {
206
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, 0, lateralAcceleration);
207
+ let candidate = availableLongitudinalForce(scratch, braking);
208
+ let previous = candidate;
209
+ for (let pass = 0; pass < SOLVE_PASSES; pass += 1) {
210
+ previous = candidate;
211
+ writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, candidate, braking, inputs);
212
+ candidate = availableLongitudinalForce(scratch, braking);
213
+ }
214
+ candidate = Math.min(candidate, previous) * FEASIBILITY_MARGIN;
215
+ writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, candidate, braking, inputs);
216
+ if (candidate > availableLongitudinalForce(scratch, braking)) {
217
+ candidate = availableLongitudinalForce(scratch, braking) * FEASIBILITY_MARGIN;
218
+ writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, candidate, braking, inputs);
219
+ }
220
+ // Retain the cheap iteration when it is feasible and close to the upper
221
+ // root; otherwise a safeguarded bracket handles near-limit transfer.
222
+ const margin = longitudinalMargin(scratch, candidate, braking);
223
+ const probe = candidate / 0.985;
224
+ writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, probe, braking, inputs);
225
+ const probeMargin = longitudinalMargin(scratch, probe, braking);
226
+ if (!(candidate > 0) || margin < 0 || probeMargin >= 0) {
227
+ candidate = bracketLongitudinalForce(car, speedMps, tireFactor, lateralAcceleration, scratch, braking, candidate, inputs);
228
+ }
229
+ writeLongitudinalPrediction(car, speedMps, tireFactor, lateralAcceleration, scratch, candidate, braking, inputs);
230
+ if (longitudinalMargin(scratch, candidate, braking) < 0) candidate = 0;
231
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) remaining[index] = scratch.longitudinalRemaining[index];
232
+ return candidate;
233
+ }
234
+
235
+ export function estimateAdvancedRacingBrakeForce(car, speedMps, tireFactor, lateralAcceleration, scratch) {
236
+ const inputs = prepareInvocationInputs(car, speedMps, scratch);
237
+ return estimateBrakeForce(car, speedMps, tireFactor, lateralAcceleration, scratch, inputs);
238
+ }
239
+
240
+ function estimateBrakeForce(car, speedMps, tireFactor, lateralAcceleration, scratch, inputs) {
241
+ scratch.brakeForce = solveLongitudinalForce(car, speedMps, tireFactor, lateralAcceleration, scratch, true, scratch.brakeRemaining, inputs);
242
+ scratch.brakeResistanceForce = scratch.resistanceForce;
243
+ return scratch;
244
+ }
245
+
246
+ export function estimateAdvancedRacingLongitudinalLimits(car, speedMps, tireFactor, lateralAcceleration, scratch) {
247
+ const inputs = prepareInvocationInputs(car, speedMps, scratch);
248
+ estimateBrakeForce(car, speedMps, tireFactor, lateralAcceleration, scratch, inputs);
249
+ scratch.driveForce = solveLongitudinalForce(car, speedMps, tireFactor, lateralAcceleration, scratch, false, scratch.driveRemaining, inputs);
250
+ scratch.driveResistanceForce = scratch.resistanceForce;
251
+ // Feedforward users see the actual present load state, not the last solver's
252
+ // hypothetical drive state. The solved remaining budgets stay separate.
253
+ writeWheelEnvelope(car, speedMps, tireFactor, scratch, inputs, car.longitudinalAcceleration ?? 0, lateralAcceleration);
254
+ return scratch;
255
+ }
@@ -0,0 +1,76 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { simUnitsToMeters } from '../units.js';
3
+ import { VEHICLE_LIMITS, tirePerformanceFactor } from '../vehicle/vehiclePhysics.js';
4
+ import { ADVANCED_VEHICLE_MODEL as MODEL, ADVANCED_WHEELS, advancedWheelSurface } from '../vehicle/advancedVehicleModel.js';
5
+ import { advancedLateralSlipAngle } from '../vehicle/advancedTireForces.js';
6
+ import { createAdvancedRacingEnvelopeScratch, writeAdvancedRacingWheelEnvelope } from './advancedRacingEnvelope.js';
7
+ import { racingDriverProfile } from './racingDriverProfile.js';
8
+ import { advancedRacingYawResponseSeconds } from './advancedRacingTiming.js';
9
+
10
+ const WHEEL_SCRATCH = createAdvancedRacingEnvelopeScratch();
11
+
12
+ export function advancedRacingPathCurvature(car, target) {
13
+ const speed = simUnitsToMeters(car.speed);
14
+ const dx = simUnitsToMeters(target.x - car.x);
15
+ const dy = simUnitsToMeters(target.y - car.y);
16
+ const velocityHeading = speed > 2 && Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
17
+ ? Math.atan2(car.velocityY, car.velocityX) : car.heading;
18
+ const angle = normalizeAngle(Math.atan2(dy, dx) - velocityHeading);
19
+ return 2 * Math.sin(angle) / Math.max(Math.hypot(dx, dy), 1);
20
+ }
21
+
22
+ // Racing pursuit follows the velocity direction. The front wheels also need
23
+ // tire slip to generate the requested force; geometric steering alone omits
24
+ // that demand as the car approaches the nonlinear brush envelope.
25
+ export function advancedRacingSteeringToPoint(car, target, profile = racingDriverProfile(car), controls = car) {
26
+ const speed = simUnitsToMeters(car.speed);
27
+ const curvature = advancedRacingPathCurvature(car, target);
28
+ const geometricSteering = Math.atan(MODEL.wheelbase * curvature);
29
+ if (speed < 2) return { curvature, steering: geometricSteering };
30
+
31
+ const cos = Math.cos(car.heading);
32
+ const sin = Math.sin(car.heading);
33
+ const vx = Number.isFinite(car.velocityX) ? simUnitsToMeters(car.velocityX) : speed * cos;
34
+ const vy = Number.isFinite(car.velocityY) ? simUnitsToMeters(car.velocityY) : speed * sin;
35
+ const u = vx * cos + vy * sin;
36
+ const v = -vx * sin + vy * cos;
37
+ const yaw = car.yawRate ?? 0;
38
+ const desiredYaw = speed * curvature;
39
+ const responseSeconds = advancedRacingYawResponseSeconds(profile);
40
+ const frontForce = car.mass * speed ** 2 * curvature / 2 +
41
+ car.mass * MODEL.yawInertiaPerMass * (desiredYaw - yaw) / (MODEL.wheelbase * responseSeconds);
42
+ const envelope = writeAdvancedRacingWheelEnvelope(car, speed, tirePerformanceFactor(car.tireEnergy ?? 100), WHEEL_SCRATCH, {
43
+ longitudinalAcceleration: car.longitudinalAcceleration ?? 0,
44
+ lateralAcceleration: car.lateralAcceleration ?? 0,
45
+ });
46
+ const nominalLoad = car.mass * MODEL.gravity * MODEL.frontWeightFraction / 2;
47
+ const brakingForce = (controls.brake ?? 0) * car.brakeNewtons * MODEL.frontBrakeFraction / 2;
48
+ let weightedSteering = 0;
49
+ let totalWeight = 0;
50
+ for (let index = 0; index < 2; index += 1) {
51
+ const wheel = ADVANCED_WHEELS[index];
52
+ const capacity = envelope.capacities[index];
53
+ const remaining = Math.sqrt(Math.max(0, capacity ** 2 - brakingForce ** 2));
54
+ const lateralForce = clamp(
55
+ frontForce * capacity / Math.max(envelope.frontCapacity, 1e-9),
56
+ -remaining * 0.995, remaining * 0.995,
57
+ );
58
+ const localVelocityAngle = Math.atan2(v + yaw * wheel.x, Math.max(u - yaw * wheel.y, MODEL.lowSpeedSlipReference));
59
+ const bodyLongitudinalForce = -brakingForce - advancedWheelSurface(car, wheel.id).rolling * envelope.loads[index];
60
+ let wheelAngle = localVelocityAngle;
61
+ for (let iteration = 0; iteration < 3; iteration += 1) {
62
+ // Trail braking rotates longitudinal contact force into body-lateral
63
+ // force. Invert that projection along with the brush response.
64
+ const tireLateralForce = clamp((lateralForce - bodyLongitudinalForce * Math.sin(wheelAngle)) /
65
+ Math.max(0.7, Math.cos(wheelAngle)), -remaining * 0.995, remaining * 0.995);
66
+ wheelAngle = localVelocityAngle + advancedLateralSlipAngle({ normalLoad: envelope.loads[index], nominalLoad,
67
+ longitudinalForce: brakingForce, lateralForce: tireLateralForce, capacity });
68
+ }
69
+ weightedSteering += wheelAngle * capacity;
70
+ totalWeight += capacity;
71
+ }
72
+ const forceSteering = totalWeight > 0 ? weightedSteering / totalWeight : geometricSteering;
73
+ const blend = clamp((speed - 2) / 6, 0, 1);
74
+ return { curvature, steering: clamp(geometricSteering + (forceSteering - geometricSteering) * blend,
75
+ -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer) };
76
+ }
@@ -0,0 +1,16 @@
1
+ import { clamp } from '../simMath.js';
2
+
3
+ // Keep steering's pursuit horizon and pace's braking reservation in the same
4
+ // geometry. The car must slow before steering starts building corner force.
5
+ export function advancedRacingLookaheadMeters(speedMps) {
6
+ return clamp(12 + speedMps * 0.65, 12, 55);
7
+ }
8
+
9
+ export function advancedRacingYawResponseSeconds(profile) {
10
+ return 0.18 + 0.12 * (1 - profile.ability);
11
+ }
12
+
13
+ export function advancedRacingPreviewReserveMeters(speedMps, profile, sampleSpacingMeters) {
14
+ return advancedRacingLookaheadMeters(speedMps) + speedMps *
15
+ (advancedRacingYawResponseSeconds(profile) + profile.anticipationSeconds) + sampleSpacingMeters;
16
+ }
@@ -0,0 +1,83 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
3
+ import { sampleHeadingAt, sampleHeadingCurvatureAtInto } from '../track/trackModel.js';
4
+ import { VEHICLE_LIMITS, isSimulatorPhysicsMode, arcadeTrackCornerSpeedLimit, arcadeSurfaceGrip } from '../vehicle/vehiclePhysics.js';
5
+ import { BRAKING_LOOKAHEAD_MAX_DISTANCE, CURVATURE_LOOKAHEAD_SAMPLES, LOOKAHEAD_BASE_DISTANCE, PREVIEW_CURVATURE_CAP } from './driverControlConstants.js';
6
+ import { arcadeBrakingDeceleration } from './arcadeRacingEnvelope.js';
7
+ import { racingDriverProfile } from './racingDriverProfile.js';
8
+
9
+ const LOOKAHEAD_SAMPLE_SCRATCH = { heading: 0, curvature: 0 };
10
+
11
+ export function arcadePreviewLookahead(speed) {
12
+ return Math.min(speed * 3.15 + LOOKAHEAD_BASE_DISTANCE, BRAKING_LOOKAHEAD_MAX_DISTANCE);
13
+ }
14
+
15
+ export function arcadeRacingLookahead(speed) {
16
+ // A long pursuit chord cuts across tight bends before asking for their
17
+ // actual steering curvature. Shorten racing path tracking independently
18
+ // from the longer braking preview and shared recovery/safety policies.
19
+ return metersToSimUnits(clamp(8 + simUnitsToMeters(speed) * 0.35, 8, 35));
20
+ }
21
+
22
+ export function maxLookaheadCurvature(track, progress, lookahead) {
23
+ let maximum = 0;
24
+ const sample = LOOKAHEAD_SAMPLE_SCRATCH;
25
+ let previousHeading = sampleHeadingCurvatureAtInto(track, progress, sample).heading;
26
+ const segmentDistance = Math.max(1, lookahead / CURVATURE_LOOKAHEAD_SAMPLES);
27
+ for (let index = 1; index <= CURVATURE_LOOKAHEAD_SAMPLES; index += 1) {
28
+ sampleHeadingCurvatureAtInto(track, progress + lookahead * (index / CURVATURE_LOOKAHEAD_SAMPLES), sample);
29
+ const segmentCurvature = Math.abs(normalizeAngle(sample.heading - previousHeading)) / segmentDistance;
30
+ maximum = Math.max(maximum, Math.abs(sample.curvature ?? 0), Math.min(segmentCurvature, PREVIEW_CURVATURE_CAP));
31
+ previousHeading = sample.heading;
32
+ }
33
+ return maximum;
34
+ }
35
+
36
+ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeGuard) {
37
+ const cornerStrength = clamp((curvature - 0.00012) / 0.00095, 0, 1);
38
+ if (cornerStrength <= 0) return 0;
39
+
40
+ const entryHeading = sampleHeadingAt(race.track, car.progress - lookahead * 0.35);
41
+ const apexHeading = sampleHeadingAt(race.track, car.progress + lookahead * 0.55);
42
+ const exitHeading = sampleHeadingAt(race.track, car.progress + lookahead * 1.25);
43
+ const signedEntry = normalizeAngle(apexHeading - entryHeading);
44
+ const signedExit = normalizeAngle(exitHeading - apexHeading);
45
+ const turnDirection = Math.sign(Math.abs(signedEntry) > Math.abs(signedExit) ? signedEntry : signedExit);
46
+ if (turnDirection === 0) return 0;
47
+
48
+ const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
49
+ const simulatorMode = isSimulatorPhysicsMode(race.physicsMode);
50
+ const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
51
+ (simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
52
+ const apexOffset = turnDirection * safeEdge *
53
+ (simulatorMode ? 0.42 + aggression * 0.05 : 0.72 + aggression * 0.08);
54
+
55
+ return apexOffset * cornerStrength * (1 - edgeGuard.pressure * (simulatorMode ? 0.82 : 0.65));
56
+ }
57
+
58
+ export function arcadePreviewSpeed(car, track, horizon, aggression, profile = racingDriverProfile(car)) {
59
+ const speed = simUnitsToMeters(car.speed);
60
+ const maxSpeed = simUnitsToMeters(VEHICLE_LIMITS.maxSpeed);
61
+ const surfaceGrip = arcadeSurfaceGrip(car);
62
+ let target = maxSpeed;
63
+ const previewHorizon = Math.min(horizon, BRAKING_LOOKAHEAD_MAX_DISTANCE);
64
+ const spacing = metersToSimUnits(12);
65
+ let previous = sampleHeadingAt(track, car.progress);
66
+ const sample = { heading: 0, curvature: 0 };
67
+ for (let distance = 0; distance <= previewHorizon; distance += spacing) {
68
+ sampleHeadingCurvatureAtInto(track, car.progress + distance, sample);
69
+ const segmentCurve = distance > 0 ? Math.min(Math.abs(normalizeAngle(sample.heading - previous)) / spacing, PREVIEW_CURVATURE_CAP) : 0;
70
+ previous = sample.heading;
71
+ const curve = Math.max(Math.abs(sample.curvature), segmentCurve);
72
+ const curvePerMeter = curve * metersToSimUnits(1);
73
+ const physicalCorner = arcadeTrackCornerSpeedLimit(car, curvePerMeter, surfaceGrip);
74
+ const ratedCorner = arcadeTrackCornerSpeedLimit(car, curvePerMeter, profile.lateralShare * surfaceGrip);
75
+ const corner = Math.min(physicalCorner, ratedCorner * profile.speedJudgementScale);
76
+ const availableDistance = Math.max(0, simUnitsToMeters(distance) - speed * profile.anticipationSeconds);
77
+ // Braking rises with speed in this model. Evaluating it at the lower future
78
+ // corner speed retains a conservative bound through the approach.
79
+ const deceleration = arcadeBrakingDeceleration(car, corner, profile);
80
+ target = Math.min(target, Math.sqrt(corner ** 2 + 2 * deceleration * availableDistance));
81
+ }
82
+ return target * 3.6;
83
+ }