@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,127 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { SIM_UNITS_PER_METER, TOP_SPEED_SIM_UNITS_PER_SECOND } from '../units.js';
3
+ import { ADVANCED_VEHICLE_MODEL as MODEL, ADVANCED_WHEELS, advancedWheelSurface, advancedDriveForce, advancedDragFactor } from './advancedVehicleModel.js';
4
+ import { advancedNormalLoads, advancedTireForce } from './advancedTireForces.js';
5
+
6
+ export function integrateAdvancedVehiclePhysics(car, controls, dt, { tireFactor, tireDegradationEnabled, maxSteer, steerRate }) {
7
+ if (!(dt > 0) || !Number.isFinite(dt)) return car;
8
+ if (!Number.isFinite(car.velocityX) || !Number.isFinite(car.velocityY)) {
9
+ car.velocityX = Math.cos(car.heading) * car.speed;
10
+ car.velocityY = Math.sin(car.heading) * car.speed;
11
+ }
12
+ const throttle = clamp(controls.throttle ?? 0, 0, 1);
13
+ const brake = clamp(controls.brake ?? 0, 0, 1);
14
+ const steeringTarget = clamp(controls.steering ?? 0, -maxSteer, maxSteer);
15
+ const steps = Math.ceil(dt / MODEL.maxIntegrationStep);
16
+ const h = dt / steps;
17
+ const initialVelocityX = car.velocityX / SIM_UNITS_PER_METER;
18
+ const initialVelocityY = car.velocityY / SIM_UNITS_PER_METER;
19
+ const initialHeading = car.heading;
20
+ let frontUsage = 0;
21
+ let rearUsage = 0;
22
+ let resistanceYawAcceleration = 0;
23
+ for (let step = 0; step < steps; step += 1) {
24
+ car.steeringAngle += clamp(steeringTarget - car.steeringAngle, -steerRate * h, steerRate * h);
25
+ const cos = Math.cos(car.heading);
26
+ const sin = Math.sin(car.heading);
27
+ const vx = car.velocityX / SIM_UNITS_PER_METER;
28
+ const vy = car.velocityY / SIM_UNITS_PER_METER;
29
+ const u = vx * cos + vy * sin;
30
+ const v = -vx * sin + vy * cos;
31
+ const speed = Math.hypot(vx, vy);
32
+ const yaw = car.yawRate ?? 0;
33
+ const loads = advancedNormalLoads(car, speed, car.longitudinalAcceleration ?? 0, car.lateralAcceleration ?? 0);
34
+ // The existing newton-valued power rating defines launch force. Above the
35
+ // crossover speed, the envelope is constant power instead of constant force.
36
+ const drive = throttle * advancedDriveForce(car, u);
37
+ let fx = 0;
38
+ let fy = 0;
39
+ let moment = 0;
40
+ let resistanceMoment = 0;
41
+ frontUsage = 0;
42
+ rearUsage = 0;
43
+ for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
44
+ const wheel = ADVANCED_WHEELS[index];
45
+ const { x, y } = wheel;
46
+ const steer = wheel.front ? car.steeringAngle : 0;
47
+ const cs = Math.cos(steer);
48
+ const sn = Math.sin(steer);
49
+ const wheelU = (u - yaw * y) * cs + (v + yaw * x) * sn;
50
+ const wheelV = -(u - yaw * y) * sn + (v + yaw * x) * cs;
51
+ const surface = advancedWheelSurface(car, wheel.id);
52
+ const engine = wheel.front ? 0 : drive / 2;
53
+ const brakeDemand = brake * car.brakeNewtons * (wheel.front ? MODEL.frontBrakeFraction : 1 - MODEL.frontBrakeFraction) / 2;
54
+ // Brake/static resistance cannot manufacture a reverse launch at rest.
55
+ const brakingDirection = Math.sign(wheelU || engine);
56
+ const longitudinalLever = x * sn - y * cs;
57
+ const inverseContactMass = 1 / car.mass + longitudinalLever ** 2 / (car.mass * MODEL.yawInertiaPerMass);
58
+ // Four simultaneous contacts share the stopping impulse. Include yaw
59
+ // inertia and share the budget between braking and rolling resistance;
60
+ // independently spending it twice would create motion near rest.
61
+ const stoppingBudget = Math.abs(wheelU) / (4 * h * inverseContactMass);
62
+ const braking = brakingDirection * Math.min(brakeDemand, stoppingBudget + engine);
63
+ const remainingBudget = Math.max(0, stoppingBudget - Math.max(0, (braking - engine) * Math.sign(wheelU)));
64
+ const rolling = Math.sign(wheelU) * Math.min(surface.rolling * loads[index], remainingBudget);
65
+ const nominalLoad = car.mass * MODEL.gravity * (wheel.front ? MODEL.frontWeightFraction : 1 - MODEL.frontWeightFraction) / 2;
66
+ const tire = advancedTireForce({
67
+ normalLoad: loads[index], nominalLoad, grip: car.tireGrip, tireFactor, surface,
68
+ longitudinalDemand: engine - braking, forwardVelocity: wheelU, lateralVelocity: wheelV,
69
+ });
70
+ const wheelFx = tire.longitudinal - rolling;
71
+ const bodyFx = wheelFx * cs - tire.lateral * sn;
72
+ const bodyFy = wheelFx * sn + tire.lateral * cs;
73
+ fx += bodyFx;
74
+ fy += bodyFy;
75
+ moment += x * bodyFy - y * bodyFx;
76
+ resistanceMoment += x * (-rolling * sn) - y * (-rolling * cs);
77
+ if (wheel.front) frontUsage = Math.max(frontUsage, tire.usage);
78
+ else rearUsage = Math.max(rearUsage, tire.usage);
79
+ }
80
+ const dragFactor = advancedDragFactor(car);
81
+ fx -= dragFactor * speed * u;
82
+ fy -= dragFactor * speed * v;
83
+ car.longitudinalAcceleration = fx / car.mass;
84
+ car.lateralAcceleration = fy / car.mass;
85
+ const ax = (fx * cos - fy * sin) / car.mass;
86
+ const ay = (fx * sin + fy * cos) / car.mass;
87
+ car.velocityX += ax * h * SIM_UNITS_PER_METER;
88
+ car.velocityY += ay * h * SIM_UNITS_PER_METER;
89
+ car.yawRate = yaw + moment / (car.mass * MODEL.yawInertiaPerMass) * h;
90
+ resistanceYawAcceleration = resistanceMoment / (car.mass * MODEL.yawInertiaPerMass);
91
+ car.heading = normalizeAngle(car.heading + car.yawRate * h);
92
+ car.speed = Math.hypot(car.velocityX, car.velocityY);
93
+ // Retain the package's supported top-speed envelope, including externally
94
+ // placed cars. Engine/drag balance determines normal acceleration below it.
95
+ if (car.speed > TOP_SPEED_SIM_UNITS_PER_SECOND) {
96
+ const scale = TOP_SPEED_SIM_UNITS_PER_SECOND / car.speed;
97
+ car.velocityX *= scale;
98
+ car.velocityY *= scale;
99
+ car.speed = TOP_SPEED_SIM_UNITS_PER_SECOND;
100
+ }
101
+ car.x += car.velocityX * h;
102
+ car.y += car.velocityY * h;
103
+ }
104
+ const ax = (car.velocityX / SIM_UNITS_PER_METER - initialVelocityX) / dt;
105
+ const ay = (car.velocityY / SIM_UNITS_PER_METER - initialVelocityY) / dt;
106
+ car.longitudinalAcceleration = ax * Math.cos(initialHeading) + ay * Math.sin(initialHeading);
107
+ car.lateralAcceleration = -ax * Math.sin(initialHeading) + ay * Math.cos(initialHeading);
108
+ car.longitudinalG = car.longitudinalAcceleration / MODEL.gravity;
109
+ car.lateralG = car.lateralAcceleration / MODEL.gravity;
110
+ car.gripUsage = Math.max(frontUsage, rearUsage);
111
+ car.slipAngleRadians = car.speed > 0.01 ? normalizeAngle(Math.atan2(car.velocityY, car.velocityX) - car.heading) : 0;
112
+ car.tractionLimited = car.gripUsage > 1;
113
+ car.stabilityState = Math.abs(car.slipAngleRadians) > 0.35 ? 'spin-risk'
114
+ : rearUsage > 1 && rearUsage > frontUsage * 1.15 ? 'oversteer'
115
+ : frontUsage > 1 ? 'understeer' : 'stable';
116
+ car.wheelDragYawRate = resistanceYawAcceleration * dt;
117
+ car.steerSaturation = 1 / Math.max(1, frontUsage);
118
+ car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
119
+ car.throttle = throttle;
120
+ car.brake = brake;
121
+ if (tireDegradationEnabled !== false) {
122
+ const care = clamp(Number(car.tireCare) || 1, 0.45, 1.8);
123
+ const wear = (0.035 + Math.abs(car.lateralG) * 0.11 + brake * 0.07 + throttle * 0.025 + Math.max(0, car.gripUsage - 1) * 0.03) / care;
124
+ car.tireEnergy = clamp((car.tireEnergy ?? 100) - wear * dt, 1, 100);
125
+ }
126
+ return car;
127
+ }
@@ -7,7 +7,8 @@ import {
7
7
  import { canCollide, isCollidable } from '../participants/participantInteractions.js';
8
8
  import { clamp, normalizeAngle } from '../simMath.js';
9
9
  import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from './vehiclePhysics.js';
10
- import { shiftPreviousRenderPose } from '../pit/pitRouting.js';
10
+ import { shiftPreviousRenderPose } from './vehicleKinematics.js';
11
+ import { applyAdvancedContactResponse } from './advancedContactResponse.js';
11
12
 
12
13
  const MAX_COLLISION_CORRECTION = 4.5;
13
14
 
@@ -113,23 +114,29 @@ export function resolveCollisionsForSimulation(sim) {
113
114
  second.y += secondCorrectionY;
114
115
  }
115
116
 
116
- if (oneCarFixed) {
117
+ const advancedPhysics = isSimulatorPhysicsMode(sim.physicsMode);
118
+ if (advancedPhysics) {
119
+ applyContactVelocityResponse(sim, first, second, collision.axis, {
120
+ firstFixed: firstPitControlled,
121
+ secondFixed: secondPitControlled,
122
+ });
123
+ } else if (oneCarFixed) {
117
124
  if (!firstPitControlled) dampPitContactVelocity(first, 0.985, isSimulatorPhysicsMode(sim.physicsMode));
118
125
  if (!secondPitControlled) dampPitContactVelocity(second, 0.985, isSimulatorPhysicsMode(sim.physicsMode));
119
126
  } else {
120
127
  applyContactVelocityResponse(sim, first, second, collision.axis);
121
128
  }
122
129
 
123
- const yawNudge = clamp(collision.depth * 0.0025, 0.008, 0.035);
130
+ const yawNudge = advancedPhysics ? 0 : clamp(collision.depth * 0.0025, 0.008, 0.035);
124
131
  const freshContact = first.contactCooldown <= 0 && second.contactCooldown <= 0;
125
132
  const firstHeadingCorrection = firstPitControlled ? 0 : -collision.axis.y * yawNudge;
126
133
  const secondHeadingCorrection = secondPitControlled ? 0 : collision.axis.y * yawNudge;
127
134
  if (!firstPitControlled) {
128
- first.heading = normalizeAngle(first.heading + firstHeadingCorrection);
135
+ if (!advancedPhysics) first.heading = normalizeAngle(first.heading + firstHeadingCorrection);
129
136
  shiftPreviousRenderPose(first, firstCorrectionX, firstCorrectionY, firstHeadingCorrection);
130
137
  }
131
138
  if (!secondPitControlled) {
132
- second.heading = normalizeAngle(second.heading + secondHeadingCorrection);
139
+ if (!advancedPhysics) second.heading = normalizeAngle(second.heading + secondHeadingCorrection);
133
140
  shiftPreviousRenderPose(second, secondCorrectionX, secondCorrectionY, secondHeadingCorrection);
134
141
  }
135
142
  first.contactCooldown = 1;
@@ -166,8 +173,15 @@ export function resolveCollisionsForSimulation(sim) {
166
173
 
167
174
  export function applyContactVelocityResponse(sim, first, second, axis, options = {}) {
168
175
  recordContactVelocityResponse(options.stats);
176
+ if (isSimulatorPhysicsMode(sim.physicsMode)) {
177
+ applyAdvancedContactResponse(first, second, axis, {
178
+ restitution: sim.rules.collisionRestitution,
179
+ firstFixed: options.firstFixed,
180
+ secondFixed: options.secondFixed,
181
+ });
182
+ return;
183
+ }
169
184
  if (
170
- isSimulatorPhysicsMode(sim.physicsMode) ||
171
185
  Number.isFinite(first.velocityX) ||
172
186
  Number.isFinite(second.velocityX)
173
187
  ) {
@@ -37,17 +37,6 @@ export function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset
37
37
  };
38
38
  }
39
39
 
40
- export function analyticWheelState(patch, centerState, track, trackLimit) {
41
- return writeAnalyticWheelStateWithProjectedHalfWidth(
42
- {},
43
- patch,
44
- centerState,
45
- track,
46
- trackLimit,
47
- analyticWheelProjectedHalfWidth(patch, centerState),
48
- );
49
- }
50
-
51
40
  export function writeAnalyticWheelState(target, patch, centerState, track, trackLimit) {
52
41
  return writeAnalyticWheelStateWithProjectedHalfWidth(
53
42
  target,
@@ -87,7 +76,7 @@ export function writeAnalyticWheelStateWithProjectedHalfWidth(
87
76
  const state = sampledStates.length === 1 ? (sampledStates[0] ?? {}) : {};
88
77
  sampledStates.length = 1;
89
78
  sampledStates[0] = state;
90
- writeStateFromSignedOffset(state, centerState, track, signedOffset);
79
+ writeStateFromSignedOffset(state, track, signedOffset);
91
80
 
92
81
  target.id = patch.id;
93
82
  target.x = patch.center.x;
@@ -107,7 +96,7 @@ export function writeAnalyticWheelStateWithProjectedHalfWidth(
107
96
  return target;
108
97
  }
109
98
 
110
- function writeStateFromSignedOffset(target, centerState, track, signedOffset) {
99
+ function writeStateFromSignedOffset(target, track, signedOffset) {
111
100
  const crossTrackError = Math.abs(signedOffset);
112
101
  const surface = mainTrackSurface(track, crossTrackError);
113
102
  target.signedOffset = signedOffset;
@@ -38,10 +38,6 @@ export function canSkipConnectorPitChecks(track, geometry, centerState, epsilon
38
38
  return true;
39
39
  }
40
40
 
41
- export function pitPatchOffsetRange(patch, centerState) {
42
- return patchPitOffsetRange(patch, centerState);
43
- }
44
-
45
41
  export function pitPatchInsideRoad(patch, centerState, epsilon = 0.001) {
46
42
  const roadWidth = Number(centerState?.pitLaneRoadWidth);
47
43
  if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
@@ -50,14 +46,6 @@ export function pitPatchInsideRoad(patch, centerState, epsilon = 0.001) {
50
46
  return range.minimum >= -halfRoadWidth - epsilon && range.maximum <= halfRoadWidth + epsilon;
51
47
  }
52
48
 
53
- export function pitPatchOverlapsRoad(patch, centerState, epsilon = 0.001) {
54
- const roadWidth = Number(centerState?.pitLaneRoadWidth);
55
- if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
56
- const halfRoadWidth = roadWidth / 2;
57
- const range = patchPitOffsetRange(patch, centerState);
58
- return range.minimum <= halfRoadWidth + epsilon && range.maximum >= -halfRoadWidth - epsilon;
59
- }
60
-
61
49
  export function writeAnalyticPitWheelState(target, patch, centerState) {
62
50
  const range = patchPitOffsetRange(patch, centerState);
63
51
  const signedOffset = centerState.mainTrackSignedOffset ?? centerState.signedOffset;
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from '../timing/drsState.js';
1
2
  import {
2
3
  VEHICLE_GEOMETRY,
3
4
  vehicleAxes,
@@ -89,10 +90,7 @@ function destroyCarOnBarrier(sim, car) {
89
90
  car.outOfRace = true;
90
91
  markCarDnf(sim, car, { reason: 'barrier' });
91
92
  car.canAttack = false;
92
- car.drsActive = false;
93
- car.drsEligible = false;
94
- car.drsZoneId = null;
95
- car.drsZoneEnabled = false;
93
+ clearCurrentDrsState(car);
96
94
  freezeBarrierMotion(car, sim.physicsMode);
97
95
  sim.events.unshift({
98
96
  type: 'car-destroyed',
@@ -0,0 +1,266 @@
1
+ import { ensureOrientedRectCorners } from './vehicleGeometry.js';
2
+ import { analyticWheelProjectedHalfWidth, wheelFullyOutside, writeAnalyticWheelState } from './mainTrackWheelSurface.js';
3
+ import { pitPatchInsideRoad, writeAnalyticPitWheelState } from './pitWheelSurface.js';
4
+ import { worstState } from './surfacePriority.js';
5
+ import { resetWheelSummary, recordWheelSummary, finishWheelSummary } from './wheelSurfaceSummary.js';
6
+ import {
7
+ nearestTrackStateForCar,
8
+ queryHintedTrackStateForCar,
9
+ queryLocalSegmentTrackProjectionsForCar,
10
+ queryLocalSegmentTrackStateForCar,
11
+ queryLocalSegmentTrackStatesForCar,
12
+ } from '../track/trackStatePolicy.js';
13
+
14
+ const STRAIGHT_CONNECTOR_CURVATURE_EPSILON = 1e-8;
15
+ const MAIN_TRACK_PATCH_MARGIN = 0.001;
16
+
17
+ function writeFullWheelState(target, patch, sampledStates, trackLimit) {
18
+ const state = worstState(sampledStates);
19
+ let minimumSignedOffset = Infinity;
20
+ let maximumSignedOffset = -Infinity;
21
+ for (let index = 0; index < sampledStates.length; index += 1) {
22
+ const signedOffset = sampledStates[index].signedOffset;
23
+ if (signedOffset < minimumSignedOffset) minimumSignedOffset = signedOffset;
24
+ if (signedOffset > maximumSignedOffset) maximumSignedOffset = signedOffset;
25
+ }
26
+ const outside = wheelFullyOutside(sampledStates, trackLimit);
27
+
28
+ target.id = patch.id;
29
+ target.x = patch.center.x;
30
+ target.y = patch.center.y;
31
+ target.signedOffset = state.signedOffset;
32
+ target.crossTrackError = state.crossTrackError;
33
+ target.surface = state.surface;
34
+ target.onTrack = Boolean(state.onTrack);
35
+ target.inPitLane = Boolean(state.inPitLane);
36
+ target.pitLanePart = state.pitLanePart ?? null;
37
+ target.pitBoxId = state.pitBoxId ?? null;
38
+ target.minimumSignedOffset = minimumSignedOffset;
39
+ target.maximumSignedOffset = maximumSignedOffset;
40
+ target.fullyOutsideWhiteLine = outside.fullyOutsideWhiteLine;
41
+ target.outsideSide = outside.outsideSide;
42
+ target.sampledStates = sampledStates;
43
+ return target;
44
+ }
45
+
46
+ function queryConnectorSampleState(track, car, point, progressHint, centerState) {
47
+ const baseSegmentId = Number.isInteger(centerState?.segmentId) ? centerState.segmentId : null;
48
+ if (baseSegmentId == null) return queryHintedTrackStateForCar(track, car, point, progressHint);
49
+ return queryLocalSegmentTrackStateForCar(
50
+ track,
51
+ car,
52
+ point,
53
+ baseSegmentId,
54
+ progressHint,
55
+ { skipPitOverrideInsideMainRoad: true },
56
+ );
57
+ }
58
+
59
+ function sampleFullConnectorPatchInto(sampledStates, patch, track, car, progressHint, centerState) {
60
+ const corners = ensureOrientedRectCorners(patch);
61
+ sampledStates.length = 1 + corners.length;
62
+ sampledStates[0] = queryConnectorSampleState(track, car, patch.center, progressHint, centerState);
63
+ for (let index = 0; index < corners.length; index += 1) {
64
+ sampledStates[index + 1] = queryConnectorSampleState(track, car, corners[index], progressHint, centerState);
65
+ }
66
+ return sampledStates;
67
+ }
68
+
69
+ function sampleFullNearestPatchInto(sampledStates, patch, track, car) {
70
+ const corners = ensureOrientedRectCorners(patch);
71
+ sampledStates.length = 1 + corners.length;
72
+ sampledStates[0] = nearestTrackStateForCar(track, car, patch.center, car.progress);
73
+ for (let index = 0; index < corners.length; index += 1) {
74
+ sampledStates[index + 1] = nearestTrackStateForCar(track, car, corners[index], car.progress);
75
+ }
76
+ return sampledStates;
77
+ }
78
+
79
+ function writeConnectorWheelState(target, {
80
+ patch,
81
+ wheelCenterState,
82
+ centerState,
83
+ car,
84
+ track,
85
+ trackLimit,
86
+ progressHint,
87
+ sampledStates,
88
+ }) {
89
+ if (!wheelCenterState) {
90
+ return writeAnalyticWheelState(target, patch, centerState, track, trackLimit);
91
+ }
92
+ if (!wheelCenterState.inPitLane) {
93
+ return writeAnalyticWheelState(target, patch, wheelCenterState, track, trackLimit);
94
+ }
95
+ if (pitPatchInsideRoad(patch, wheelCenterState)) {
96
+ return writeAnalyticPitWheelState(target, patch, wheelCenterState);
97
+ }
98
+ sampleFullConnectorPatchInto(sampledStates, patch, track, car, progressHint, centerState);
99
+ return writeFullWheelState(target, patch, sampledStates, trackLimit);
100
+ }
101
+
102
+ function collectPatchCenters(target, patches) {
103
+ target.length = patches.length;
104
+ for (let index = 0; index < patches.length; index += 1) {
105
+ target[index] = patches[index].center;
106
+ }
107
+ return target;
108
+ }
109
+
110
+ export function connectorWheelCenterStates(
111
+ track,
112
+ car,
113
+ patches,
114
+ centerState,
115
+ progressHint,
116
+ scratchStates = null,
117
+ scratchProjections = null,
118
+ scratchPositions = null,
119
+ ) {
120
+ const baseSegmentId = Number.isInteger(centerState?.segmentId) ? centerState.segmentId : null;
121
+ const states = scratchStates ?? new Array(patches.length);
122
+ states.length = patches.length;
123
+ if (baseSegmentId == null) {
124
+ for (let index = 0; index < patches.length; index += 1) {
125
+ states[index] = queryHintedTrackStateForCar(track, car, patches[index].center, progressHint);
126
+ }
127
+ return states;
128
+ }
129
+ const positions = collectPatchCenters(scratchPositions ?? new Array(patches.length), patches);
130
+ return queryLocalSegmentTrackStatesForCar(
131
+ track,
132
+ car,
133
+ positions,
134
+ baseSegmentId,
135
+ progressHint,
136
+ {
137
+ radius: 2,
138
+ skipPitOverrideInsideMainRoad: true,
139
+ target: states,
140
+ projections: scratchProjections,
141
+ },
142
+ );
143
+ }
144
+
145
+ export function connectorWheelCenterProjections(
146
+ track,
147
+ car,
148
+ patches,
149
+ centerState,
150
+ progressHint,
151
+ scratchProjections = null,
152
+ scratchPositions = null,
153
+ ) {
154
+ const baseSegmentId = Number.isInteger(centerState?.segmentId) ? centerState.segmentId : null;
155
+ if (baseSegmentId == null) return null;
156
+ const positions = collectPatchCenters(scratchPositions ?? new Array(patches.length), patches);
157
+ return queryLocalSegmentTrackProjectionsForCar(
158
+ track,
159
+ car,
160
+ positions,
161
+ baseSegmentId,
162
+ progressHint,
163
+ {
164
+ radius: 2,
165
+ projections: scratchProjections,
166
+ },
167
+ );
168
+ }
169
+
170
+ export function canUseStraightConnectorMainTrackAnalytic(track, geometry, centerState, trackLimit) {
171
+ if (
172
+ !track ||
173
+ !geometry?.contactPatches?.length ||
174
+ !centerState ||
175
+ centerState.inPitLane ||
176
+ !centerState.onTrack ||
177
+ Math.abs(centerState.curvature ?? 0) > STRAIGHT_CONNECTOR_CURVATURE_EPSILON
178
+ ) {
179
+ return false;
180
+ }
181
+
182
+ for (let index = 0; index < geometry.contactPatches.length; index += 1) {
183
+ const patch = geometry.contactPatches[index];
184
+ const wheelCenterOffset =
185
+ (patch.center.x - centerState.x) * centerState.normalX +
186
+ (patch.center.y - centerState.y) * centerState.normalY;
187
+ const projectedHalfWidth = analyticWheelProjectedHalfWidth(patch, centerState);
188
+ if (
189
+ wheelCenterOffset - projectedHalfWidth < -trackLimit - MAIN_TRACK_PATCH_MARGIN ||
190
+ wheelCenterOffset + projectedHalfWidth > trackLimit + MAIN_TRACK_PATCH_MARGIN
191
+ ) {
192
+ return false;
193
+ }
194
+ }
195
+ return true;
196
+ }
197
+
198
+ export function writeConnectorWheelStatesWithSummary({
199
+ targetWheels,
200
+ patches,
201
+ wheelCenterStates,
202
+ centerState,
203
+ car,
204
+ track,
205
+ trackLimit,
206
+ progressHint,
207
+ sampledStatePools,
208
+ summaryTarget = null,
209
+ }) {
210
+ const summary = resetWheelSummary(summaryTarget ?? {});
211
+ for (let index = 0; index < patches.length; index += 1) {
212
+ const sampledStates = sampledStatePools[index] ?? [];
213
+ sampledStatePools[index] = sampledStates;
214
+ const wheel = writeConnectorWheelState(targetWheels[index], {
215
+ patch: patches[index],
216
+ wheelCenterState: wheelCenterStates[index],
217
+ centerState,
218
+ car,
219
+ track,
220
+ trackLimit,
221
+ progressHint,
222
+ sampledStates,
223
+ });
224
+ recordWheelSummary(summary, wheel);
225
+ }
226
+ finishWheelSummary(summary, track, trackLimit);
227
+
228
+ return {
229
+ wheels: targetWheels,
230
+ effectiveSurface: summary.effectiveSurface,
231
+ representative: summary.representative,
232
+ trackLimits: summary.trackLimits,
233
+ allOnTrackOrPit: summary.allOnTrackOrPit,
234
+ allInPitLane: summary.allInPitLane,
235
+ };
236
+ }
237
+
238
+ export function writeNearestSampledWheelStatesWithSummary({
239
+ targetWheels,
240
+ patches,
241
+ car,
242
+ track,
243
+ trackLimit,
244
+ sampledStatePools,
245
+ summaryTarget = null,
246
+ }) {
247
+ const summary = resetWheelSummary(summaryTarget ?? {});
248
+ for (let index = 0; index < patches.length; index += 1) {
249
+ const patch = patches[index];
250
+ const sampledStates = sampledStatePools[index] ?? [];
251
+ sampledStatePools[index] = sampledStates;
252
+ sampleFullNearestPatchInto(sampledStates, patch, track, car);
253
+ const wheel = writeFullWheelState(targetWheels[index], patch, sampledStates, trackLimit);
254
+ recordWheelSummary(summary, wheel);
255
+ }
256
+ finishWheelSummary(summary, track, trackLimit);
257
+
258
+ return {
259
+ wheels: targetWheels,
260
+ effectiveSurface: summary.effectiveSurface,
261
+ representative: summary.representative,
262
+ trackLimits: summary.trackLimits,
263
+ allOnTrackOrPit: summary.allOnTrackOrPit,
264
+ allInPitLane: summary.allInPitLane,
265
+ };
266
+ }
@@ -110,24 +110,6 @@ function writeOrientedRectValues(target, {
110
110
  return rect;
111
111
  }
112
112
 
113
- function writeOrientedRect(target, { id, type, center, heading, length, width }) {
114
- const cos = Math.cos(heading);
115
- const sin = Math.sin(heading);
116
- return writeOrientedRectValues(target, {
117
- id,
118
- type,
119
- centerX: center.x,
120
- centerY: center.y,
121
- heading,
122
- length,
123
- width,
124
- forwardX: cos,
125
- forwardY: sin,
126
- rightX: -sin,
127
- rightY: cos,
128
- });
129
- }
130
-
131
113
  function writeVehicleGeometry(target, pose) {
132
114
  const geometry = target ?? {};
133
115
  const forwardX = Math.cos(pose.heading);
@@ -238,10 +220,6 @@ function writeCurrentVehicleGeometry(target, pose) {
238
220
  return geometry;
239
221
  }
240
222
 
241
- export function createOrientedRect({ id, type, center, heading, length, width }) {
242
- return writeOrientedRect(null, { id, type, center, heading, length, width });
243
- }
244
-
245
223
  export function createVehicleGeometry(car, options = {}) {
246
224
  const pose = vehiclePose(car, options);
247
225
  return writeVehicleGeometry(null, pose);
@@ -323,21 +301,6 @@ function writeVehicleShapeAabb(target, shape) {
323
301
  return target;
324
302
  }
325
303
 
326
- export function mergeAabbs(aabbs) {
327
- let minX = Infinity;
328
- let maxX = -Infinity;
329
- let minY = Infinity;
330
- let maxY = -Infinity;
331
- for (let index = 0; index < aabbs.length; index += 1) {
332
- const aabb = aabbs[index];
333
- if (aabb.minX < minX) minX = aabb.minX;
334
- if (aabb.maxX > maxX) maxX = aabb.maxX;
335
- if (aabb.minY < minY) minY = aabb.minY;
336
- if (aabb.maxY > maxY) maxY = aabb.maxY;
337
- }
338
- return { minX, maxX, minY, maxY };
339
- }
340
-
341
304
  function writeGeometryPose(target, car) {
342
305
  const pose = target ?? {};
343
306
  const x = finiteOr(car.x, 0);
@@ -455,25 +418,6 @@ export function ensureOrientedRectCorners(rect) {
455
418
  }).corners;
456
419
  }
457
420
 
458
- export function createVehicleAabb(car, options = {}) {
459
- const geometry = createVehicleGeometry(car, options);
460
- return mergeAabbs(geometry.shapes.map(createVehicleShapeAabb));
461
- }
462
-
463
- export function interpolateVehiclePose(car, amount) {
464
- const previousX = finiteOr(car.previousX, car.x);
465
- const previousY = finiteOr(car.previousY, car.y);
466
- const previousHeading = finiteOr(car.previousHeading, car.heading);
467
- const headingDelta = normalizeAngle(finiteOr(car.heading, 0) - previousHeading);
468
-
469
- return {
470
- ...car,
471
- x: previousX + (finiteOr(car.x, previousX) - previousX) * amount,
472
- y: previousY + (finiteOr(car.y, previousY) - previousY) * amount,
473
- heading: normalizeAngle(previousHeading + headingDelta * amount),
474
- };
475
- }
476
-
477
421
  export function getCarCorners(car) {
478
422
  return createVehicleGeometry(car).body.corners;
479
423
  }
@@ -1,3 +1,5 @@
1
+ import { normalizeAngle } from '../simMath.js';
2
+
1
3
  export function freezeVehicleMotion(car, {
2
4
  brake = 1,
3
5
  clearManualControls = false,
@@ -35,3 +37,11 @@ export function syncVehicleVelocityToHeading(car) {
35
37
  car.velocityX = Math.cos(heading) * speed;
36
38
  car.velocityY = Math.sin(heading) * speed;
37
39
  }
40
+
41
+ export function shiftPreviousRenderPose(car, dx, dy, dheading = 0) {
42
+ car.previousX = Number.isFinite(car.previousX) ? car.previousX + dx : car.x + dx;
43
+ car.previousY = Number.isFinite(car.previousY) ? car.previousY + dy : car.y + dy;
44
+ car.previousHeading = normalizeAngle(
45
+ (Number.isFinite(car.previousHeading) ? car.previousHeading : car.heading) + dheading,
46
+ );
47
+ }