@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,174 @@
1
+ import { clamp } from '../simMath.js';
2
+
3
+ export function createCenterlineSegments(samples, segmentCount) {
4
+ const startX = new Float64Array(segmentCount);
5
+ const startY = new Float64Array(segmentCount);
6
+ const endX = new Float64Array(segmentCount);
7
+ const endY = new Float64Array(segmentCount);
8
+ const deltaX = new Float64Array(segmentCount);
9
+ const deltaY = new Float64Array(segmentCount);
10
+ const lengthSquared = new Float64Array(segmentCount);
11
+ const startDistance = new Float64Array(segmentCount);
12
+ const endDistance = new Float64Array(segmentCount);
13
+ const distanceSpan = new Float64Array(segmentCount);
14
+ const heading = new Float64Array(segmentCount);
15
+ const normalX = new Float64Array(segmentCount);
16
+ const normalY = new Float64Array(segmentCount);
17
+ const endNormalX = new Float64Array(segmentCount);
18
+ const endNormalY = new Float64Array(segmentCount);
19
+ const curvature = new Float64Array(segmentCount);
20
+ const minX = new Float64Array(segmentCount);
21
+ const maxX = new Float64Array(segmentCount);
22
+ const minY = new Float64Array(segmentCount);
23
+ const maxY = new Float64Array(segmentCount);
24
+
25
+ for (let id = 0; id < segmentCount; id += 1) {
26
+ const start = samples[id];
27
+ const end = samples[id + 1];
28
+ startX[id] = start.x;
29
+ startY[id] = start.y;
30
+ endX[id] = end.x;
31
+ endY[id] = end.y;
32
+ deltaX[id] = end.x - start.x;
33
+ deltaY[id] = end.y - start.y;
34
+ lengthSquared[id] = deltaX[id] * deltaX[id] + deltaY[id] * deltaY[id];
35
+ startDistance[id] = start.distance;
36
+ endDistance[id] = end.distance;
37
+ distanceSpan[id] = end.distance - start.distance;
38
+ heading[id] = start.heading;
39
+ normalX[id] = start.normalX;
40
+ normalY[id] = start.normalY;
41
+ endNormalX[id] = end.normalX;
42
+ endNormalY[id] = end.normalY;
43
+ curvature[id] = start.curvature ?? 0;
44
+ minX[id] = Math.min(start.x, end.x);
45
+ maxX[id] = Math.max(start.x, end.x);
46
+ minY[id] = Math.min(start.y, end.y);
47
+ maxY[id] = Math.max(start.y, end.y);
48
+ }
49
+
50
+ return {
51
+ segmentCount,
52
+ startX,
53
+ startY,
54
+ endX,
55
+ endY,
56
+ deltaX,
57
+ deltaY,
58
+ lengthSquared,
59
+ startDistance,
60
+ endDistance,
61
+ distanceSpan,
62
+ heading,
63
+ normalX,
64
+ normalY,
65
+ endNormalX,
66
+ endNormalY,
67
+ curvature,
68
+ minX,
69
+ maxX,
70
+ minY,
71
+ maxY,
72
+ };
73
+ }
74
+
75
+ export function createPrecomputedSegmentNeighborhoods(segmentCount, radii) {
76
+ if (!segmentCount || !radii?.size) return new Map();
77
+ const neighborhoodsByRadius = new Map();
78
+ for (const radius of radii) {
79
+ const width = radius * 2 + 1;
80
+ const neighborhoods = new Array(segmentCount);
81
+ for (let centerSegmentId = 0; centerSegmentId < segmentCount; centerSegmentId += 1) {
82
+ const segmentIds = new Int32Array(width);
83
+ for (let index = 0; index < width; index += 1) {
84
+ segmentIds[index] = ((centerSegmentId + (index - radius)) % segmentCount + segmentCount) % segmentCount;
85
+ }
86
+ neighborhoods[centerSegmentId] = segmentIds;
87
+ }
88
+ neighborhoodsByRadius.set(radius, neighborhoods);
89
+ }
90
+ return neighborhoodsByRadius;
91
+ }
92
+
93
+ export function getPrecomputedSegmentNeighborhood(centerline, centerSegmentId, radius) {
94
+ const neighborhoods = centerline?.precomputedNeighborhoodsByRadius?.get(radius);
95
+ return neighborhoods?.[centerSegmentId] ?? null;
96
+ }
97
+
98
+ export function writeProjectionCandidate(target, {
99
+ segmentId,
100
+ x,
101
+ y,
102
+ distance,
103
+ heading,
104
+ normalX,
105
+ normalY,
106
+ curvature,
107
+ signedOffset,
108
+ distanceSquared,
109
+ }) {
110
+ target.segmentId = segmentId;
111
+ target.x = x;
112
+ target.y = y;
113
+ target.distance = distance;
114
+ target.heading = heading;
115
+ target.normalX = normalX;
116
+ target.normalY = normalY;
117
+ target.curvature = curvature;
118
+ target.signedOffset = signedOffset;
119
+ target.crossTrackError = Math.abs(signedOffset);
120
+ target.distanceSquared = distanceSquared;
121
+ return target;
122
+ }
123
+
124
+ export function projectIndexedSegment(centerline, segmentId, position) {
125
+ if (centerline?.stats) centerline.stats.candidateProjectionObjectAllocations += 1;
126
+ const ax = centerline.startX[segmentId];
127
+ const ay = centerline.startY[segmentId];
128
+ const dx = centerline.deltaX[segmentId];
129
+ const dy = centerline.deltaY[segmentId];
130
+ const amount = centerline.lengthSquared[segmentId] > 0
131
+ ? clamp(((position.x - ax) * dx + (position.y - ay) * dy) / centerline.lengthSquared[segmentId], 0, 1)
132
+ : 0;
133
+ const x = ax + dx * amount;
134
+ const y = ay + dy * amount;
135
+ const distance = centerline.startDistance[segmentId] +
136
+ centerline.distanceSpan[segmentId] * amount;
137
+ const heading = centerline.heading[segmentId];
138
+ const normalX = centerline.normalX[segmentId];
139
+ const normalY = centerline.normalY[segmentId];
140
+ const px = position.x - x;
141
+ const py = position.y - y;
142
+ const signedOffset = px * normalX + py * normalY;
143
+
144
+ return {
145
+ segmentId,
146
+ x,
147
+ y,
148
+ distance,
149
+ heading,
150
+ normalX,
151
+ normalY,
152
+ curvature: centerline.curvature[segmentId],
153
+ signedOffset,
154
+ crossTrackError: Math.abs(signedOffset),
155
+ distanceSquared: px * px + py * py,
156
+ };
157
+ }
158
+
159
+ export function segmentFromIndex(centerline, segmentId) {
160
+ if (centerline?.stats) centerline.stats.raySegmentObjectAllocations += 1;
161
+ return {
162
+ segmentId,
163
+ startX: centerline.startX[segmentId],
164
+ startY: centerline.startY[segmentId],
165
+ endX: centerline.endX[segmentId],
166
+ endY: centerline.endY[segmentId],
167
+ startDistance: centerline.startDistance[segmentId],
168
+ endDistance: centerline.endDistance[segmentId],
169
+ normalX: centerline.normalX[segmentId],
170
+ normalY: centerline.normalY[segmentId],
171
+ endNormalX: centerline.endNormalX[segmentId],
172
+ endNormalY: centerline.endNormalY[segmentId],
173
+ };
174
+ }
@@ -51,3 +51,15 @@ export function recordStat(index, bucket, key) {
51
51
  target[key] = (target[key] ?? 0) + 1;
52
52
  index.stats[bucket] = target;
53
53
  }
54
+
55
+ export function resetTrackQueryStats(track) {
56
+ const stats = track?.queryIndex?.stats;
57
+ if (!stats) return null;
58
+ Object.assign(stats, createQueryStats());
59
+ return stats;
60
+ }
61
+
62
+ export function snapshotTrackQueryStats(track) {
63
+ const stats = track?.queryIndex?.stats;
64
+ return stats ? JSON.parse(JSON.stringify(stats)) : null;
65
+ }
@@ -0,0 +1,165 @@
1
+ import { gridCellForPoint } from './trackQueryGrid.js';
2
+ import { ensureQueryScratch, ensureScratchArray, nextScratchEpoch } from './trackQueryScratch.js';
3
+ import { segmentFromIndex } from './trackQuerySegments.js';
4
+ import { finitePoint } from './trackMath.js';
5
+
6
+ const RAY_TRACE_BUCKET_DEGREES = 10;
7
+
8
+ export function queryTrackSegmentIdsAlongRay(track, origin, vector, maxDistance, margin = 0) {
9
+ const index = track?.queryIndex;
10
+ if (
11
+ !index?.centerline?.segmentCount ||
12
+ !finitePoint(origin) ||
13
+ !Number.isFinite(vector?.x) ||
14
+ !Number.isFinite(vector?.y) ||
15
+ !Number.isFinite(maxDistance) ||
16
+ maxDistance < 0
17
+ ) return null;
18
+
19
+ const grid = index.segmentGrid ?? index.grid;
20
+ const scratch = ensureQueryScratch(index);
21
+ const segmentMarks = ensureScratchArray(scratch, 'raySegmentMarks', index.centerline.segmentCount);
22
+ const cellMarks = ensureScratchArray(scratch, 'rayCellMarks', grid.columns * grid.rows);
23
+ const segmentEpoch = nextScratchEpoch(scratch, 'raySegmentEpoch', segmentMarks);
24
+ const cellEpoch = nextScratchEpoch(scratch, 'rayCellEpoch', cellMarks);
25
+ const ids = scratch.raySegmentIds;
26
+ ids.length = 0;
27
+ const radius = Math.max(0, Math.ceil(Math.max(0, margin) / grid.cellSize));
28
+ const step = Math.max(grid.cellSize * 0.5, 1);
29
+ const sampleCount = Math.max(1, Math.ceil(maxDistance / step));
30
+ const visitedCells = scratch.rayVisitedCells;
31
+ visitedCells.length = 0;
32
+ const point = scratch.rayTracePoint ?? { x: 0, y: 0 };
33
+ scratch.rayTracePoint = point;
34
+
35
+ const seed = seedRayTraceFromNearbyCache(index, grid, origin, vector, radius, maxDistance, segmentMarks, segmentEpoch, cellMarks, cellEpoch, ids);
36
+
37
+ for (let sample = 0; sample <= sampleCount; sample += 1) {
38
+ const distance = Math.min(maxDistance, sample * step);
39
+ point.x = origin.x + vector.x * distance;
40
+ point.y = origin.y + vector.y * distance;
41
+ const center = gridCellForPoint(grid, point, false);
42
+ if (!center) continue;
43
+ for (let row = center.row - radius; row <= center.row + radius; row += 1) {
44
+ for (let column = center.column - radius; column <= center.column + radius; column += 1) {
45
+ if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows) continue;
46
+ const cellIndex = row * grid.columns + column;
47
+ if (cellMarks[cellIndex] === cellEpoch) continue;
48
+ cellMarks[cellIndex] = cellEpoch;
49
+ visitedCells.push(cellIndex);
50
+ const cell = grid.cells[cellIndex];
51
+ if (!cell) continue;
52
+ for (const segmentId of cell) {
53
+ if (segmentMarks[segmentId] === segmentEpoch) continue;
54
+ segmentMarks[segmentId] = segmentEpoch;
55
+ ids.push(segmentId);
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ storeRayTraceCache(index, grid, origin, vector, radius, maxDistance, ids, visitedCells, seed);
62
+ return ids;
63
+ }
64
+
65
+ export function queryTrackSegmentsAlongRay(track, origin, vector, maxDistance, margin = 0) {
66
+ const index = track?.queryIndex;
67
+ const ids = queryTrackSegmentIdsAlongRay(track, origin, vector, maxDistance, margin);
68
+ if (!ids) return null;
69
+ if (!ids.length) return [];
70
+ return ids.map((segmentId) => segmentFromIndex(index.centerline, segmentId));
71
+ }
72
+
73
+ function seedRayTraceFromNearbyCache(
74
+ index,
75
+ grid,
76
+ origin,
77
+ vector,
78
+ radius,
79
+ maxDistance,
80
+ segmentMarks,
81
+ segmentEpoch,
82
+ cellMarks,
83
+ cellEpoch,
84
+ ids,
85
+ ) {
86
+ const scratch = ensureQueryScratch(index);
87
+ const traceCache = scratch.rayTraceCache;
88
+ const originCell = gridCellForPoint(grid, origin, false);
89
+ if (!originCell) return null;
90
+ const bucket = rayTraceBucket(vector);
91
+ const base = rayTraceBaseKey(grid, originCell, radius);
92
+ const minimumSeedDistance = maxDistance * 0.85;
93
+ let seed = traceCache.get(rayTraceKey(base, bucket));
94
+ let bestSeedDistance = seed?.maxDistance ?? -Infinity;
95
+ if (bestSeedDistance < minimumSeedDistance) {
96
+ seed = null;
97
+ bestSeedDistance = -Infinity;
98
+ }
99
+ const beforeSeed = traceCache.get(rayTraceKey(base, bucket - 1));
100
+ if ((beforeSeed?.maxDistance ?? -Infinity) >= minimumSeedDistance && beforeSeed.maxDistance > bestSeedDistance) {
101
+ seed = beforeSeed;
102
+ bestSeedDistance = beforeSeed.maxDistance;
103
+ }
104
+ const afterSeed = traceCache.get(rayTraceKey(base, bucket + 1));
105
+ if ((afterSeed?.maxDistance ?? -Infinity) >= minimumSeedDistance && afterSeed.maxDistance > bestSeedDistance) {
106
+ seed = afterSeed;
107
+ }
108
+ if (!seed) return { base, bucket };
109
+
110
+ for (const segmentId of seed.segmentIds) {
111
+ if (segmentMarks[segmentId] === segmentEpoch) continue;
112
+ segmentMarks[segmentId] = segmentEpoch;
113
+ ids.push(segmentId);
114
+ }
115
+ for (const cellId of seed.cellIds) {
116
+ cellMarks[cellId] = cellEpoch;
117
+ }
118
+ return { base, bucket };
119
+ }
120
+
121
+ function storeRayTraceCache(index, grid, origin, vector, radius, maxDistance, segmentIds, cellIds, seed) {
122
+ const scratch = ensureQueryScratch(index);
123
+ const traceCache = scratch.rayTraceCache;
124
+ const originCell = gridCellForPoint(grid, origin, false);
125
+ if (!originCell) return;
126
+ const bucket = seed?.bucket ?? rayTraceBucket(vector);
127
+ const base = seed?.base ?? rayTraceBaseKey(grid, originCell, radius);
128
+ const key = rayTraceKey(base, bucket);
129
+ let entry = traceCache.get(key);
130
+ if (!entry) {
131
+ entry = {
132
+ maxDistance: 0,
133
+ segmentIds: [],
134
+ cellIds: [],
135
+ };
136
+ traceCache.set(key, entry);
137
+ if (index.stats) index.stats.rayTraceCacheEntryAllocations += 1;
138
+ }
139
+ entry.maxDistance = maxDistance;
140
+ entry.segmentIds.length = segmentIds.length;
141
+ for (let index = 0; index < segmentIds.length; index += 1) {
142
+ entry.segmentIds[index] = segmentIds[index];
143
+ }
144
+ entry.cellIds.length = cellIds.length;
145
+ for (let index = 0; index < cellIds.length; index += 1) {
146
+ entry.cellIds[index] = cellIds[index];
147
+ }
148
+ if (traceCache.size > 96) {
149
+ const oldestKey = traceCache.keys().next().value;
150
+ if (oldestKey) traceCache.delete(oldestKey);
151
+ }
152
+ }
153
+
154
+ function rayTraceBaseKey(grid, center, radius) {
155
+ return `${center.column}:${center.row}:${radius}:${grid.columns}:${grid.rows}`;
156
+ }
157
+
158
+ function rayTraceKey(base, bucket) {
159
+ return `${base}:${bucket}`;
160
+ }
161
+
162
+ function rayTraceBucket(vector) {
163
+ const angleDegrees = Math.atan2(vector.y, vector.x) * 180 / Math.PI;
164
+ return Math.round(angleDegrees / RAY_TRACE_BUCKET_DEGREES);
165
+ }
@@ -0,0 +1,89 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { SIM_UNITS_PER_METER } from '../units.js';
3
+ import { ADVANCED_VEHICLE_MODEL } from './advancedVehicleModel.js';
4
+ import { VEHICLE_GEOMETRY } from './vehicleGeometry.js';
5
+
6
+ function inverseMass(car, fixed) {
7
+ if (fixed) return 0;
8
+ return 1 / (Number.isFinite(car.mass) && car.mass > 0 ? car.mass : 798);
9
+ }
10
+
11
+ function supportOffset(car, normalX, normalY, coordinate) {
12
+ const cos = Math.cos(car.heading);
13
+ const sin = Math.sin(car.heading);
14
+ const along = normalX * cos + normalY * sin;
15
+ const across = -normalX * sin + normalY * cos;
16
+ // A normal parallel to a face selects its center, rather than an arbitrary corner.
17
+ const longitudinal = Math.abs(along) < 1e-10 ? 0 : Math.sign(along) * VEHICLE_GEOMETRY.bodyLength / 2;
18
+ const lateral = Math.abs(across) < 1e-10 ? 0 : Math.sign(across) * VEHICLE_GEOMETRY.bodyWidth / 2;
19
+ return coordinate === 'x' ? cos * longitudinal - sin * lateral : sin * longitudinal + cos * lateral;
20
+ }
21
+
22
+ function velocityComponent(car, coordinate) {
23
+ if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
24
+ return coordinate === 'x' ? car.velocityX : car.velocityY;
25
+ }
26
+ return (coordinate === 'x' ? Math.cos(car.heading) : Math.sin(car.heading)) * car.speed;
27
+ }
28
+
29
+ function writeVelocity(car, velocityX, velocityY, yawRate) {
30
+ car.velocityX = velocityX * SIM_UNITS_PER_METER;
31
+ car.velocityY = velocityY * SIM_UNITS_PER_METER;
32
+ car.speed = Math.hypot(car.velocityX, car.velocityY);
33
+ car.yawRate = yawRate;
34
+ }
35
+
36
+ export function applyAdvancedContactResponse(first, second, axis, {
37
+ restitution = 0,
38
+ firstFixed = false,
39
+ secondFixed = false,
40
+ } = {}) {
41
+ const axisLength = Math.hypot(axis.x, axis.y);
42
+ if (!(axisLength > 0)) return 0;
43
+ const nx = axis.x / axisLength;
44
+ const ny = axis.y / axisLength;
45
+ const firstInverseMass = inverseMass(first, firstFixed);
46
+ const secondInverseMass = inverseMass(second, secondFixed);
47
+ if (firstInverseMass + secondInverseMass === 0) return 0;
48
+ const firstInverseInertia = firstInverseMass / ADVANCED_VEHICLE_MODEL.yawInertiaPerMass;
49
+ const secondInverseInertia = secondInverseMass / ADVANCED_VEHICLE_MODEL.yawInertiaPerMass;
50
+
51
+ let firstLever = 0;
52
+ let secondLever = 0;
53
+ if ([first.x, first.y, second.x, second.y].every(Number.isFinite)) {
54
+ // SAT supplies a normal but no clipped contact manifold. Approximate a common
55
+ // contact point by the midpoint of opposing body support features. This keeps
56
+ // equal/opposite impulses and their moments consistent without rewinding poses.
57
+ const dx = second.x - first.x;
58
+ const dy = second.y - first.y;
59
+ const supportX = supportOffset(first, nx, ny, 'x') + supportOffset(second, -nx, -ny, 'x');
60
+ const supportY = supportOffset(first, nx, ny, 'y') + supportOffset(second, -nx, -ny, 'y');
61
+ firstLever = ((dx + supportX) * ny - (dy + supportY) * nx) / (2 * SIM_UNITS_PER_METER);
62
+ secondLever = ((-dx + supportX) * ny - (-dy + supportY) * nx) / (2 * SIM_UNITS_PER_METER);
63
+ }
64
+
65
+ let firstVX = velocityComponent(first, 'x') / SIM_UNITS_PER_METER;
66
+ let firstVY = velocityComponent(first, 'y') / SIM_UNITS_PER_METER;
67
+ let secondVX = velocityComponent(second, 'x') / SIM_UNITS_PER_METER;
68
+ let secondVY = velocityComponent(second, 'y') / SIM_UNITS_PER_METER;
69
+ let firstYaw = first.yawRate ?? 0;
70
+ let secondYaw = second.yawRate ?? 0;
71
+ const closingVelocity = (secondVX - firstVX) * nx + (secondVY - firstVY) * ny +
72
+ secondYaw * secondLever - firstYaw * firstLever;
73
+ let impulse = 0;
74
+ if (closingVelocity < 0) {
75
+ const inverseEffectiveMass = firstInverseMass + secondInverseMass +
76
+ firstLever * firstLever * firstInverseInertia + secondLever * secondLever * secondInverseInertia;
77
+ impulse = -(1 + clamp(restitution, 0, 1)) * closingVelocity / inverseEffectiveMass;
78
+ firstVX -= impulse * nx * firstInverseMass;
79
+ firstVY -= impulse * ny * firstInverseMass;
80
+ secondVX += impulse * nx * secondInverseMass;
81
+ secondVY += impulse * ny * secondInverseMass;
82
+ firstYaw -= impulse * firstLever * firstInverseInertia;
83
+ secondYaw += impulse * secondLever * secondInverseInertia;
84
+ }
85
+
86
+ if (!firstFixed) writeVelocity(first, firstVX, firstVY, firstYaw);
87
+ if (!secondFixed) writeVelocity(second, secondVX, secondVY, secondYaw);
88
+ return impulse;
89
+ }
@@ -0,0 +1,92 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { ADVANCED_VEHICLE_MODEL as MODEL, ADVANCED_WHEELS, advancedWheelSurface } from './advancedVehicleModel.js';
3
+
4
+ // Quasi-static load transfer: both axles conserve their total vertical load.
5
+ // A wheel can unload, but cannot pull down on the ground. This planar model
6
+ // does not simulate roll/pitch displacement or suspension travel.
7
+ export function advancedNormalLoads(car, speedMps, longitudinalAcceleration, lateralAcceleration) {
8
+ const weight = car.mass * MODEL.gravity;
9
+ const downforce = car.downforceCoefficient * MODEL.downforceScale * speedMps ** 2;
10
+ const aero = car.drsActive ? downforce * 0.82 : downforce;
11
+ const transfer = car.mass * longitudinalAcceleration * MODEL.centerOfGravityHeight / MODEL.wheelbase;
12
+ const total = weight + aero;
13
+ const front = clamp(weight * MODEL.frontWeightFraction + aero * MODEL.frontAeroFraction - transfer, 0, total);
14
+ const rear = total - front;
15
+ const lateralTransfer = car.mass * lateralAcceleration * MODEL.centerOfGravityHeight / MODEL.trackWidth;
16
+ const frontTransfer = clamp(lateralTransfer * MODEL.frontRollTransferFraction, -front / 2, front / 2);
17
+ const rearTransfer = clamp(lateralTransfer * (1 - MODEL.frontRollTransferFraction), -rear / 2, rear / 2);
18
+ return [front / 2 + frontTransfer, front / 2 - frontTransfer, rear / 2 + rearTransfer, rear / 2 - rearTransfer];
19
+ }
20
+
21
+ export function advancedTireForce({ normalLoad, nominalLoad, grip, tireFactor, surface, longitudinalDemand, forwardVelocity, lateralVelocity }) {
22
+ if (normalLoad <= 0) return { longitudinal: 0, lateral: 0, usage: 0, capacity: 0, slipAngle: 0 };
23
+ const capacity = contactCapacity(normalLoad, nominalLoad, grip, tireFactor, surface);
24
+ const slipAngle = Math.atan2(lateralVelocity, Math.max(Math.abs(forwardVelocity), MODEL.lowSpeedSlipReference));
25
+ const stiffness = MODEL.corneringStiffnessPerLoad * nominalLoad * (normalLoad / nominalLoad) ** 0.8;
26
+ const lateralDemand = -stiffness * Math.tan(slipAngle);
27
+ const demand = Math.hypot(longitudinalDemand, lateralDemand);
28
+ const usage = demand / Math.max(capacity, 1e-9);
29
+ const longitudinal = clamp(longitudinalDemand, -capacity, capacity);
30
+ const lateralCapacity = Math.sqrt(Math.max(0, capacity ** 2 - longitudinal ** 2));
31
+ // Longitudinal input is a requested force, not a slip ratio. Reserve its
32
+ // friction-circle share; the brush curve applies to the lateral slip demand.
33
+ const q = lateralCapacity > 1e-9 ? Math.min(Math.abs(lateralDemand) / (3 * lateralCapacity), 1) : 1;
34
+ let lateral = Math.sign(lateralDemand) * lateralCapacity * (1 - (1 - q) ** 3);
35
+ let longitudinalForce = longitudinal;
36
+ // With brake demand beyond adhesion, a sliding contact opposes its actual
37
+ // velocity. This captures directional loss under lock without wheel-spin state.
38
+ if (Math.abs(longitudinalDemand) >= capacity && longitudinalDemand * forwardVelocity < 0) {
39
+ const speed = Math.hypot(forwardVelocity, lateralVelocity);
40
+ longitudinalForce = -capacity * forwardVelocity / speed;
41
+ lateral = -capacity * lateralVelocity / speed;
42
+ }
43
+ return { longitudinal: longitudinalForce, lateral, usage, capacity, slipAngle };
44
+
45
+ }
46
+
47
+ // Policy-facing capacity estimate; the driver chooses how much margin to keep.
48
+ // No controller gets additional grip beyond these same contact-patch capacities.
49
+ function contactCapacity(normalLoad, nominalLoad, grip, tireFactor, surface) {
50
+ return grip * MODEL.tireGripScale * tireFactor * surface.grip * nominalLoad *
51
+ (normalLoad / nominalLoad) ** MODEL.tireLoadExponent;
52
+ }
53
+
54
+ export { contactCapacity as advancedContactCapacity };
55
+
56
+ // Signed wheel-angle correction needed to produce the requested lateral
57
+ // force. Tire-local velocity slip has the opposite sign in advancedTireForce.
58
+ export function advancedLateralSlipAngle({ normalLoad, nominalLoad, longitudinalForce = 0, lateralForce, capacity }) {
59
+ if (!(normalLoad > 0) || !(nominalLoad > 0) || !(capacity > 0)) return 0;
60
+ const remaining = Math.sqrt(Math.max(0, capacity ** 2 - longitudinalForce ** 2));
61
+ if (!(remaining > 0)) return 0;
62
+ const q = 1 - Math.cbrt(1 - clamp(Math.abs(lateralForce) / remaining, 0, 1));
63
+ const lateralDemand = 3 * remaining * q;
64
+ const stiffness = MODEL.corneringStiffnessPerLoad * nominalLoad * (normalLoad / nominalLoad) ** 0.8;
65
+ return Math.sign(lateralForce) * Math.atan(lateralDemand / stiffness);
66
+ }
67
+
68
+ function summedCapacity(car, speedMps, tireFactor, rearOnly) {
69
+ const loads = advancedNormalLoads(car, speedMps, 0, 0);
70
+ let force = 0;
71
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
72
+ const wheel = ADVANCED_WHEELS[index];
73
+ if (rearOnly && wheel.front) continue;
74
+ const nominalLoad = car.mass * MODEL.gravity * (wheel.front ? MODEL.frontWeightFraction : 1 - MODEL.frontWeightFraction) / 2;
75
+ const surface = advancedWheelSurface(car, wheel.id);
76
+ force += contactCapacity(loads[index], nominalLoad, car.tireGrip, tireFactor, surface);
77
+ }
78
+ return force;
79
+ }
80
+
81
+ export function advancedLateralAccelerationLimit(car, speedMps, tireFactor) {
82
+ return summedCapacity(car, speedMps, tireFactor, false) / car.mass;
83
+ }
84
+
85
+ export function advancedRearForceCapacity(car, speedMps, tireFactor) {
86
+ return summedCapacity(car, speedMps, tireFactor, true);
87
+ }
88
+
89
+ export function advancedRollingResistanceForce(car, speedMps) {
90
+ const loads = advancedNormalLoads(car, speedMps, 0, 0);
91
+ return ADVANCED_WHEELS.reduce((force, wheel, index) => force + advancedWheelSurface(car, wheel.id).rolling * loads[index], 0);
92
+ }
@@ -0,0 +1,65 @@
1
+ import { VEHICLE_GEOMETRY } from './vehicleGeometry.js';
2
+ import { simUnitsToMeters } from '../units.js';
3
+
4
+ // SI calibration for the package's DRS-era Formula-style car. Existing setup
5
+ // ratings remain force/grip parameters; this is not a particular team's dataset.
6
+ export const ADVANCED_VEHICLE_MODEL = Object.freeze({
7
+ gravity: 9.80665,
8
+ wheelbase: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLongitudinalOffset * 2),
9
+ trackWidth: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLateralOffset * 2),
10
+ frontWeightFraction: 0.5, // stored pose/velocity and collision origin are the chassis center
11
+ frontAeroFraction: 0.44,
12
+ frontRollTransferFraction: 0.52,
13
+ centerOfGravityHeight: 0.3,
14
+ yawInertiaPerMass: 1.65, // m²: yaw inertia = mass × radius of gyration squared
15
+ frontBrakeFraction: 0.57,
16
+ tireGripScale: 0.72,
17
+ tireLoadExponent: 0.9,
18
+ corneringStiffnessPerLoad: 28, // N/rad per N of normal load
19
+ maxIntegrationStep: 1 / 240,
20
+ lowSpeedSlipReference: 2.5, // m/s, regularizes the static tire limit
21
+ engineForceScale: 0.5, // legacy force rating to peak rear-axle wheel force
22
+ enginePowerSpeed: 28, // m/s: peak force × crossover speed gives the power envelope
23
+ airDensity: 1.225,
24
+ dragAreaScale: 4.5, // m² per existing dragCoefficient unit
25
+ downforceScale: 0.5, // existing coefficient × v² × scale gives newtons
26
+ });
27
+
28
+ export const ADVANCED_WHEELS = Object.freeze([
29
+ { id: 'front-left', front: true, side: -1 },
30
+ { id: 'front-right', front: true, side: 1 },
31
+ { id: 'rear-left', front: false, side: -1 },
32
+ { id: 'rear-right', front: false, side: 1 },
33
+ ].map((wheel) => Object.freeze({
34
+ ...wheel,
35
+ x: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLongitudinalOffset) * (wheel.front ? 1 : -1),
36
+ y: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLateralOffset) * wheel.side,
37
+ })));
38
+
39
+ const SURFACES = {
40
+ track: { grip: 1, rolling: 0.012 },
41
+ 'pit-entry': { grip: 0.97, rolling: 0.015 },
42
+ 'pit-lane': { grip: 0.97, rolling: 0.015 },
43
+ 'pit-exit': { grip: 0.97, rolling: 0.015 },
44
+ 'pit-box': { grip: 0.94, rolling: 0.02 },
45
+ kerb: { grip: 0.82, rolling: 0.035 },
46
+ gravel: { grip: 0.38, rolling: 0.19 },
47
+ grass: { grip: 0.3, rolling: 0.1 },
48
+ barrier: { grip: 0.2, rolling: 0.25 },
49
+ };
50
+
51
+ export function advancedWheelSurface(car, wheelId) {
52
+ const name = car.wheelStates?.find((wheel) => wheel.id === wheelId)?.surface ?? car.trackState?.surface;
53
+ return SURFACES[name] ?? SURFACES.track;
54
+ }
55
+
56
+ // Requested longitudinal drive force at full throttle, before tire adhesion.
57
+ export function advancedDriveForce(car, forwardSpeedMps) {
58
+ return car.powerNewtons * ADVANCED_VEHICLE_MODEL.engineForceScale * Math.min(1, ADVANCED_VEHICLE_MODEL.enginePowerSpeed / Math.max(Math.abs(forwardSpeedMps), ADVANCED_VEHICLE_MODEL.enginePowerSpeed));
59
+ }
60
+
61
+ // Drag force is this coefficient times speed squared. The integrator applies
62
+ // it opposite the velocity vector; pace control estimates straight resistance.
63
+ export function advancedDragFactor(car) {
64
+ return 0.5 * ADVANCED_VEHICLE_MODEL.airDensity * car.dragCoefficient * ADVANCED_VEHICLE_MODEL.dragAreaScale * (car.drsActive ? 0.72 : 1);
65
+ }