@inventure71/paddockjs 1.0.0 → 2.0.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,363 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import {
3
+ REAL_F1_WHEELBASE_METERS,
4
+ SIM_UNITS_PER_METER,
5
+ TOP_SPEED_SIM_UNITS_PER_SECOND,
6
+ metersPerSecondToSimSpeed,
7
+ simUnitsToMeters,
8
+ metersToSimUnits,
9
+ } from '../units.js';
10
+ import { VEHICLE_GEOMETRY, getCarCorners, vehicleAxes } from './vehicleGeometry.js';
11
+
12
+ const G = 9.80665;
13
+
14
+ export const PHYSICS_MODES = ['arcade', 'simulator'];
15
+ export const DEFAULT_PHYSICS_MODE = 'arcade';
16
+
17
+ export const VEHICLE_LIMITS = {
18
+ wheelbase: metersToSimUnits(REAL_F1_WHEELBASE_METERS),
19
+ maxSteer: 0.56,
20
+ steerRate: 2.35,
21
+ maxSpeed: TOP_SPEED_SIM_UNITS_PER_SECOND,
22
+ carLength: VEHICLE_GEOMETRY.visualLength,
23
+ carWidth: VEHICLE_GEOMETRY.visualWidth,
24
+ };
25
+
26
+ const SURFACE_MODEL = {
27
+ track: { grip: 1, drag: 0, rollingResistance: 0 },
28
+ 'pit-entry': { grip: 0.96, drag: 0.04, rollingResistance: 0.012 },
29
+ 'pit-lane': { grip: 0.96, drag: 0.04, rollingResistance: 0.012 },
30
+ 'pit-exit': { grip: 0.96, drag: 0.04, rollingResistance: 0.012 },
31
+ 'pit-box': { grip: 0.94, drag: 0.08, rollingResistance: 0.025 },
32
+ kerb: { grip: 0.92, drag: 0.12, rollingResistance: 0.045 },
33
+ gravel: { grip: 0.43, drag: 4.0, rollingResistance: 0.68 },
34
+ grass: { grip: 0.34, drag: 1.9, rollingResistance: 0.36 },
35
+ barrier: { grip: 0.18, drag: 9, rollingResistance: 1.2 },
36
+ };
37
+ const WHEEL_DRAG_YAW_GAIN = 0.08;
38
+ const MAX_WHEEL_DRAG_YAW_RATE = 0.22;
39
+
40
+ const SIMULATOR_SURFACE_MODEL = {
41
+ track: { grip: 1, drag: 0, rollingResistance: 0.008, scrub: 0 },
42
+ 'pit-entry': { grip: 0.92, drag: 0.06, rollingResistance: 0.018, scrub: 1.2 },
43
+ 'pit-lane': { grip: 0.9, drag: 0.08, rollingResistance: 0.02, scrub: 1.4 },
44
+ 'pit-exit': { grip: 0.92, drag: 0.06, rollingResistance: 0.018, scrub: 1.2 },
45
+ 'pit-box': { grip: 0.82, drag: 0.12, rollingResistance: 0.035, scrub: 2.5 },
46
+ kerb: { grip: 0.78, drag: 0.42, rollingResistance: 0.09, scrub: 8.5 },
47
+ gravel: { grip: 0.36, drag: 5.4, rollingResistance: 0.24, scrub: 18 },
48
+ grass: { grip: 0.3, drag: 3.4, rollingResistance: 0.2, scrub: 15 },
49
+ barrier: { grip: 0.08, drag: 15, rollingResistance: 2.4, scrub: 30 },
50
+ };
51
+
52
+ export function normalizePhysicsMode(value) {
53
+ return value === 'simulator' ? 'simulator' : DEFAULT_PHYSICS_MODE;
54
+ }
55
+
56
+ function accelerationLimit(speed, surfaceGrip) {
57
+ const speedRatio = clamp(speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
58
+ return SIM_UNITS_PER_METER * surfaceGrip * (17 * (1 - speedRatio ** 2.1) + 1.2);
59
+ }
60
+
61
+ function brakingLimit(speed, surfaceGrip) {
62
+ const speedRatio = clamp(speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
63
+ return SIM_UNITS_PER_METER * surfaceGrip * (20 + speedRatio * 14);
64
+ }
65
+
66
+ export function tirePerformanceFactor(tireEnergy = 100) {
67
+ const normalized = clamp((Number(tireEnergy) || 1) / 100, 0.01, 1);
68
+ return clamp(0.45 + 0.55 * normalized ** 0.72, 0.45, 1);
69
+ }
70
+
71
+ function surfaceResistance(surfaceName, model = SURFACE_MODEL) {
72
+ const surface = model[surfaceName] ?? model.track;
73
+ return surface.drag * 0.14 + surface.rollingResistance * 2.4 + (1 - surface.grip) * 0.65;
74
+ }
75
+
76
+ function sideWheelResistance(wheels, side, model = SURFACE_MODEL) {
77
+ const sideWheels = wheels.filter((wheel) => wheel.id?.endsWith(`-${side}`));
78
+ if (!sideWheels.length) return 0;
79
+ return sideWheels.reduce((total, wheel) => total + surfaceResistance(wheel.surface, model), 0) / sideWheels.length;
80
+ }
81
+
82
+ function wheelDragYawRate(car, model = SURFACE_MODEL) {
83
+ const wheels = Array.isArray(car.wheelStates) ? car.wheelStates : [];
84
+ if (!wheels.length) return 0;
85
+ const leftResistance = sideWheelResistance(wheels, 'left', model);
86
+ const rightResistance = sideWheelResistance(wheels, 'right', model);
87
+ const speedFactor = clamp(car.speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
88
+ return clamp(
89
+ (rightResistance - leftResistance) * speedFactor * WHEEL_DRAG_YAW_GAIN,
90
+ -MAX_WHEEL_DRAG_YAW_RATE,
91
+ MAX_WHEEL_DRAG_YAW_RATE,
92
+ );
93
+ }
94
+
95
+ function setPhysicsTelemetry(car, {
96
+ lateralAcceleration = 0,
97
+ longitudinalAcceleration = 0,
98
+ gripUsage = 0,
99
+ slipAngleRadians = 0,
100
+ tractionLimited = false,
101
+ stabilityState = 'stable',
102
+ } = {}) {
103
+ car.lateralAcceleration = lateralAcceleration;
104
+ car.longitudinalAcceleration = longitudinalAcceleration;
105
+ car.lateralG = lateralAcceleration / G;
106
+ car.longitudinalG = longitudinalAcceleration / G;
107
+ car.gripUsage = Number.isFinite(gripUsage) ? gripUsage : 0;
108
+ car.slipAngleRadians = Number.isFinite(slipAngleRadians) ? slipAngleRadians : 0;
109
+ car.tractionLimited = Boolean(tractionLimited);
110
+ car.stabilityState = stabilityState;
111
+ }
112
+
113
+ export function integrateVehiclePhysics(car, controls, dt, options = {}) {
114
+ if (normalizePhysicsMode(options.physicsMode) === 'simulator') {
115
+ return integrateSimulatorVehiclePhysics(car, controls, dt, options);
116
+ }
117
+ return integrateArcadeVehiclePhysics(car, controls, dt, options);
118
+ }
119
+
120
+ function integrateArcadeVehiclePhysics(car, controls, dt, options = {}) {
121
+ const steeringTarget = clamp(controls.steering ?? 0, -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer);
122
+ const steerDelta = clamp(
123
+ steeringTarget - car.steeringAngle,
124
+ -VEHICLE_LIMITS.steerRate * dt,
125
+ VEHICLE_LIMITS.steerRate * dt,
126
+ );
127
+ car.steeringAngle += steerDelta;
128
+
129
+ const throttle = clamp(controls.throttle ?? 0, 0, 1);
130
+ const brake = clamp(controls.brake ?? 0, 0, 1);
131
+ const surface = SURFACE_MODEL[car.trackState?.surface] ?? SURFACE_MODEL.track;
132
+ const tireFactor = tirePerformanceFactor(car.tireEnergy ?? 100);
133
+ const surfaceGrip = surface.grip * tireFactor;
134
+ const dragMultiplier = car.drsActive ? 0.42 : 1;
135
+ const speedRatio = clamp(car.speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
136
+ const engineForce = throttle *
137
+ car.powerNewtons *
138
+ (car.drsActive ? 1.08 : 1) *
139
+ Math.max(0.12, 1 - speedRatio ** 1.15);
140
+ const brakeForce = brake * car.brakeNewtons;
141
+ const speedMetersPerSecondBefore = simUnitsToMeters(car.speed);
142
+ const dragForce = (car.dragCoefficient * dragMultiplier * 0.12 + surface.drag) *
143
+ speedMetersPerSecondBefore * speedMetersPerSecondBefore;
144
+ const rollingForce = surface.rollingResistance * car.mass * G;
145
+ const driveAcceleration = clamp((engineForce / car.mass) * SIM_UNITS_PER_METER, 0, accelerationLimit(car.speed, surfaceGrip));
146
+ const drsAcceleration = car.drsActive ? driveAcceleration * 0.06 : 0;
147
+ const brakeDeceleration = clamp((brakeForce / car.mass) * SIM_UNITS_PER_METER, 0, brakingLimit(car.speed, surfaceGrip));
148
+ const dragDeceleration = ((dragForce + rollingForce) / car.mass) * SIM_UNITS_PER_METER;
149
+ const acceleration = driveAcceleration + drsAcceleration - brakeDeceleration - dragDeceleration;
150
+
151
+ car.speed = clamp(car.speed + acceleration * dt, 0, VEHICLE_LIMITS.maxSpeed);
152
+
153
+ const speedMetersPerSecond = simUnitsToMeters(car.speed);
154
+ const rawYawRate = (speedMetersPerSecond / REAL_F1_WHEELBASE_METERS) *
155
+ Math.tan(car.steeringAngle) *
156
+ tireFactor;
157
+ const downforceGrip = car.downforceCoefficient * speedMetersPerSecond * speedMetersPerSecond / car.mass;
158
+ const tyreConditionGrip = tireFactor;
159
+ const maxYawRate = ((car.tireGrip * tyreConditionGrip * G + downforceGrip) * surfaceGrip) /
160
+ Math.max(speedMetersPerSecond, 1);
161
+ const steeringYawRate = clamp(rawYawRate, -maxYawRate, maxYawRate);
162
+ const requestedWheelDragYawRate = wheelDragYawRate(car);
163
+ car.yawRate = clamp(steeringYawRate + requestedWheelDragYawRate, -maxYawRate, maxYawRate);
164
+ car.wheelDragYawRate = car.yawRate - steeringYawRate;
165
+ car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
166
+ car.heading = normalizeAngle(car.heading + car.yawRate * dt);
167
+ car.x += Math.cos(car.heading) * car.speed * dt;
168
+ car.y += Math.sin(car.heading) * car.speed * dt;
169
+ car.throttle = throttle;
170
+ car.brake = brake;
171
+ car.steerSaturation = rawYawRate === 0 ? 0 : Math.abs(steeringYawRate / rawYawRate);
172
+ setPhysicsTelemetry(car, {
173
+ lateralAcceleration: speedMetersPerSecond * car.yawRate,
174
+ longitudinalAcceleration: acceleration / SIM_UNITS_PER_METER,
175
+ gripUsage: Math.min(1, Math.abs(speedMetersPerSecond * car.yawRate) /
176
+ Math.max((car.tireGrip * tyreConditionGrip * G + downforceGrip) * surfaceGrip, 1)),
177
+ tractionLimited: Math.abs(rawYawRate) > Math.abs(steeringYawRate) + 1e-6,
178
+ stabilityState: Math.abs(rawYawRate) > Math.abs(steeringYawRate) + 1e-6 ? 'understeer' : 'stable',
179
+ });
180
+ if (options.tireDegradationEnabled !== false) {
181
+ const tyreLoad = Math.abs(car.lateralAcceleration) / G;
182
+ const tireCare = clamp(Number(car.tireCare) || 1, 0.45, 1.8);
183
+ const wearRate = (0.035 + tyreLoad * 0.11 + brake * 0.07 + throttle * 0.025) / tireCare;
184
+ car.tireEnergy = clamp((car.tireEnergy ?? 100) - wearRate * dt, 1, 100);
185
+ }
186
+
187
+ return car;
188
+ }
189
+
190
+ function simulatorSurface(surfaceName) {
191
+ return SIMULATOR_SURFACE_MODEL[surfaceName] ?? SIMULATOR_SURFACE_MODEL.track;
192
+ }
193
+
194
+ function aggregateSimulatorSurface(car) {
195
+ const wheels = Array.isArray(car.wheelStates) ? car.wheelStates : [];
196
+ if (!wheels.length) return simulatorSurface(car.trackState?.surface ?? 'track');
197
+ const total = wheels.reduce((sum, wheel) => {
198
+ const surface = simulatorSurface(wheel.surface);
199
+ return {
200
+ grip: sum.grip + surface.grip,
201
+ drag: sum.drag + surface.drag,
202
+ rollingResistance: sum.rollingResistance + surface.rollingResistance,
203
+ scrub: sum.scrub + surface.scrub,
204
+ };
205
+ }, { grip: 0, drag: 0, rollingResistance: 0, scrub: 0 });
206
+ return {
207
+ grip: total.grip / wheels.length,
208
+ drag: total.drag / wheels.length,
209
+ rollingResistance: total.rollingResistance / wheels.length,
210
+ scrub: total.scrub / wheels.length,
211
+ };
212
+ }
213
+
214
+ function simulatorStabilityState({ gripUsage, slipAngle, surfaceName, brake, throttle }) {
215
+ if (gripUsage > 1.32 || Math.abs(slipAngle) > 0.42) return 'spin-risk';
216
+ if (gripUsage > 1.02) return brake > 0.2 && throttle < 0.2 ? 'oversteer' : 'understeer';
217
+ if (surfaceName === 'kerb' && gripUsage > 0.72) return 'oversteer';
218
+ if ((surfaceName === 'gravel' || surfaceName === 'grass') && gripUsage > 0.5) return 'understeer';
219
+ return 'stable';
220
+ }
221
+
222
+ function ensureSimulatorVelocity(car) {
223
+ if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) return;
224
+ const speed = clamp(Number(car.speed) || 0, 0, VEHICLE_LIMITS.maxSpeed);
225
+ car.velocityX = Math.cos(car.heading ?? 0) * speed;
226
+ car.velocityY = Math.sin(car.heading ?? 0) * speed;
227
+ }
228
+
229
+ function limitSimulatorSpeed(car) {
230
+ const speed = Math.hypot(car.velocityX, car.velocityY);
231
+ if (speed <= VEHICLE_LIMITS.maxSpeed || speed <= 0) return speed;
232
+ const scale = VEHICLE_LIMITS.maxSpeed / speed;
233
+ car.velocityX *= scale;
234
+ car.velocityY *= scale;
235
+ return VEHICLE_LIMITS.maxSpeed;
236
+ }
237
+
238
+ function integrateSimulatorVehiclePhysics(car, controls, dt, options = {}) {
239
+ ensureSimulatorVelocity(car);
240
+ const steeringTarget = clamp(controls.steering ?? 0, -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer);
241
+ const steerDelta = clamp(
242
+ steeringTarget - car.steeringAngle,
243
+ -VEHICLE_LIMITS.steerRate * dt,
244
+ VEHICLE_LIMITS.steerRate * dt,
245
+ );
246
+ car.steeringAngle += steerDelta;
247
+
248
+ const throttle = clamp(controls.throttle ?? 0, 0, 1);
249
+ const brake = clamp(controls.brake ?? 0, 0, 1);
250
+ const surfaceName = car.trackState?.surface ?? 'track';
251
+ const surface = aggregateSimulatorSurface(car);
252
+ const tireFactor = tirePerformanceFactor(car.tireEnergy ?? 100);
253
+ const surfaceGrip = surface.grip * tireFactor;
254
+ const speedBefore = Math.hypot(car.velocityX, car.velocityY);
255
+ const speedBeforeMps = simUnitsToMeters(speedBefore);
256
+ const speedRatio = clamp(speedBefore / VEHICLE_LIMITS.maxSpeed, 0, 1);
257
+ const downforceGrip = car.downforceCoefficient * speedBeforeMps * speedBeforeMps / car.mass;
258
+ const totalGripAcceleration = Math.max(0.1, (car.tireGrip * tireFactor * G + downforceGrip) * surfaceGrip);
259
+ const axes = vehicleAxes(car.heading ?? 0);
260
+ const forwardVelocity = car.velocityX * axes.forward.x + car.velocityY * axes.forward.y;
261
+ const lateralVelocity = car.velocityX * axes.right.x + car.velocityY * axes.right.y;
262
+ const forwardSpeedMps = simUnitsToMeters(forwardVelocity);
263
+ const lateralSpeedMps = simUnitsToMeters(lateralVelocity);
264
+ const currentSlipAngle = Math.atan2(lateralSpeedMps, Math.max(Math.abs(forwardSpeedMps), 0.1));
265
+ const engineForce = throttle *
266
+ car.powerNewtons *
267
+ (car.drsActive ? 1.07 : 1) *
268
+ Math.max(0.08, 1 - speedRatio ** 1.22);
269
+ const brakeForce = brake * car.brakeNewtons;
270
+ const driveAcceleration = Math.min(
271
+ engineForce / car.mass,
272
+ accelerationLimit(car.speed, surfaceGrip) / SIM_UNITS_PER_METER,
273
+ );
274
+ const brakeDeceleration = Math.min(
275
+ brakeForce / car.mass,
276
+ brakingLimit(car.speed, surfaceGrip) / SIM_UNITS_PER_METER,
277
+ );
278
+ const dragMultiplier = car.drsActive ? 0.46 : 1;
279
+ const dragAcceleration = (
280
+ (car.dragCoefficient * dragMultiplier * 0.28 + surface.drag * 0.018) *
281
+ speedBeforeMps * speedBeforeMps
282
+ ) / car.mass;
283
+ const rollingAcceleration = surface.rollingResistance * G;
284
+ const normalizedSteer = Math.abs(car.steeringAngle) / VEHICLE_LIMITS.maxSteer;
285
+ const speedSensitiveSteer = 1 / (1 + (Math.abs(forwardSpeedMps) / 58) ** 1.85 * 2.35);
286
+ const effectiveSteeringAngle = car.steeringAngle * clamp(speedSensitiveSteer, 0.16, 1);
287
+ const rawYawRate = Math.abs(forwardSpeedMps) <= 0.05
288
+ ? 0
289
+ : (forwardSpeedMps / REAL_F1_WHEELBASE_METERS) * Math.tan(effectiveSteeringAngle);
290
+ const sideSlipDamping = 4.2 + surfaceGrip * 2.1 + speedRatio * 1.6;
291
+ const desiredLateralAcceleration = forwardSpeedMps * rawYawRate - lateralSpeedMps * sideSlipDamping;
292
+ const longitudinalDemand = Math.max(driveAcceleration, brakeDeceleration);
293
+ const longitudinalUsage = clamp(longitudinalDemand / Math.max(totalGripAcceleration * 0.52, 1), 0, 0.94);
294
+ const lateralCapacity = totalGripAcceleration * Math.sqrt(Math.max(0.08, 1 - longitudinalUsage ** 2));
295
+ const desiredLateralAbs = Math.abs(desiredLateralAcceleration);
296
+ const lateralUsage = desiredLateralAbs / Math.max(lateralCapacity, 0.1);
297
+ const gripUsage = Math.sqrt(lateralUsage ** 2 + longitudinalUsage ** 2);
298
+ const tractionLimited = gripUsage > 1 || longitudinalUsage > 0.9;
299
+ const actualLateralAcceleration = clamp(desiredLateralAcceleration, -lateralCapacity, lateralCapacity);
300
+ const steeringYawRate = Math.abs(forwardSpeedMps) <= 0.05 ? 0 : actualLateralAcceleration / Math.max(Math.abs(forwardSpeedMps), 0.1);
301
+ const steeringScrubAcceleration = normalizedSteer ** 1.55 *
302
+ speedRatio ** 1.05 *
303
+ (42 + surface.scrub + Math.max(0, gripUsage - 0.55) * 18);
304
+ const tractionPowerScale = tractionLimited
305
+ ? clamp(1 - (gripUsage - 1) * 0.55 - longitudinalUsage * 0.18, 0.12, 1)
306
+ : 1;
307
+ const brakingDirection = forwardSpeedMps >= -0.2 ? 1 : -1;
308
+ const longitudinalAcceleration =
309
+ driveAcceleration * tractionPowerScale -
310
+ brakeDeceleration * brakingDirection -
311
+ dragAcceleration * Math.sign(forwardSpeedMps || 1) -
312
+ rollingAcceleration * Math.sign(forwardSpeedMps || 1) -
313
+ steeringScrubAcceleration;
314
+
315
+ const requestedWheelDragYawRate = wheelDragYawRate(car, SIMULATOR_SURFACE_MODEL) * (surfaceName === 'kerb' ? 1.6 : 1);
316
+ const maxYawRate = speedBeforeMps <= 0.05 ? 0 : totalGripAcceleration / Math.max(speedBeforeMps, 0.1);
317
+ const slipSteeringAuthority = clamp(1 - Math.abs(currentSlipAngle) * 1.8, 0.35, 1);
318
+ const yawTarget = clamp(steeringYawRate * slipSteeringAuthority + requestedWheelDragYawRate, -maxYawRate, maxYawRate);
319
+ const yawResponse = 1.6 + surfaceGrip * 0.9 + speedRatio * 0.5;
320
+ const yawAccelerationCap = speedRatio < 0.18 ? 6.2 : 3.2;
321
+ const maxYawDelta = Math.min(Math.max(0.08, maxYawRate * yawResponse), yawAccelerationCap) * dt;
322
+ car.yawRate = clamp(
323
+ (car.yawRate ?? 0) + clamp(yawTarget - (car.yawRate ?? 0), -maxYawDelta, maxYawDelta),
324
+ -maxYawRate,
325
+ maxYawRate,
326
+ );
327
+ car.wheelDragYawRate = requestedWheelDragYawRate;
328
+ car.heading = normalizeAngle(car.heading + car.yawRate * dt);
329
+
330
+ const accelerationX = axes.forward.x * longitudinalAcceleration + axes.right.x * actualLateralAcceleration;
331
+ const accelerationY = axes.forward.y * longitudinalAcceleration + axes.right.y * actualLateralAcceleration;
332
+ car.velocityX += metersToSimUnits(accelerationX) * dt;
333
+ car.velocityY += metersToSimUnits(accelerationY) * dt;
334
+ car.speed = limitSimulatorSpeed(car);
335
+ car.x += car.velocityX * dt;
336
+ car.y += car.velocityY * dt;
337
+ car.throttle = throttle;
338
+ car.brake = brake;
339
+ car.steerSaturation = rawYawRate === 0 ? 0 : Math.abs(steeringYawRate / rawYawRate);
340
+ car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
341
+ const velocityHeading = car.speed <= 0.01 ? car.heading : Math.atan2(car.velocityY, car.velocityX);
342
+ const slipAngle = normalizeAngle(velocityHeading - car.heading);
343
+ setPhysicsTelemetry(car, {
344
+ lateralAcceleration: actualLateralAcceleration,
345
+ longitudinalAcceleration,
346
+ gripUsage,
347
+ slipAngleRadians: slipAngle,
348
+ tractionLimited,
349
+ stabilityState: simulatorStabilityState({ gripUsage, slipAngle, surfaceName, brake, throttle }),
350
+ });
351
+
352
+ if (options.tireDegradationEnabled !== false) {
353
+ const tyreLoad = Math.abs(car.lateralAcceleration) / G;
354
+ const tireCare = clamp(Number(car.tireCare) || 1, 0.45, 1.8);
355
+ const heatPenalty = Math.max(0, gripUsage - 0.72) * 0.24 + normalizedSteer * speedRatio * 0.08;
356
+ const wearRate = (0.045 + tyreLoad * 0.16 + brake * 0.09 + throttle * 0.035 + heatPenalty) / tireCare;
357
+ car.tireEnergy = clamp((car.tireEnergy ?? 100) - wearRate * dt, 1, 100);
358
+ }
359
+
360
+ return car;
361
+ }
362
+
363
+ export { getCarCorners };
@@ -0,0 +1,316 @@
1
+ import { serializeParticipantInteraction } from '../participants/participantInteractions.js';
2
+
3
+ export function finiteOrNull(value) {
4
+ return Number.isFinite(value) ? value : null;
5
+ }
6
+
7
+ function serializeAppliedControls(car, dependencies) {
8
+ const { VEHICLE_LIMITS } = dependencies;
9
+ const controls = car.appliedControls ?? {};
10
+ const maxSteer = VEHICLE_LIMITS?.maxSteer ?? 1;
11
+ return {
12
+ steering: maxSteer > 0 ? (controls.steering ?? 0) / maxSteer : 0,
13
+ steeringRadians: controls.steering ?? 0,
14
+ throttle: controls.throttle ?? 0,
15
+ brake: controls.brake ?? 0,
16
+ };
17
+ }
18
+
19
+ export function serializeWheels(wheels = []) {
20
+ return wheels.map((wheel) => ({
21
+ id: wheel.id,
22
+ x: wheel.x,
23
+ y: wheel.y,
24
+ signedOffset: wheel.signedOffset,
25
+ crossTrackError: wheel.crossTrackError,
26
+ surface: wheel.surface,
27
+ onTrack: Boolean(wheel.onTrack),
28
+ inPitLane: Boolean(wheel.inPitLane),
29
+ fullyOutsideWhiteLine: Boolean(wheel.fullyOutsideWhiteLine),
30
+ }));
31
+ }
32
+
33
+ export function serializeCar(car, rank, penaltySeconds = 0, dependencies) {
34
+ const {
35
+ createLapTelemetry,
36
+ normalizePitIntent,
37
+ PIT_INTENT_NONE,
38
+ serializeLapTelemetry,
39
+ serializePitStop,
40
+ simSpeedToKph,
41
+ simUnitsToMeters,
42
+ VEHICLE_LIMITS,
43
+ } = dependencies;
44
+ const finishTime = car.finishTime ?? null;
45
+ const raceStatus = (car.destroyed || car.outOfRace) ? 'destroyed' : car.finished ? 'waved-flag' : 'racing';
46
+ return {
47
+ id: car.id,
48
+ code: car.code,
49
+ timingCode: car.timingCode,
50
+ driverNumber: car.driverNumber,
51
+ icon: car.icon,
52
+ raceName: car.raceName,
53
+ name: car.name,
54
+ color: car.color,
55
+ team: car.team ? { ...car.team } : null,
56
+ tire: car.tire,
57
+ personality: { ...car.personality },
58
+ aggression: car.aggression,
59
+ aggressionPercent: Math.round((car.aggression ?? 0) * 100),
60
+ setup: {
61
+ vehicleId: car.vehicleId,
62
+ vehicleName: car.vehicleName,
63
+ vehicleRatings: car.vehicleRatings ? { ...car.vehicleRatings } : null,
64
+ maxSpeedKph: simSpeedToKph(VEHICLE_LIMITS.maxSpeed),
65
+ powerUnitKn: car.powerNewtons / 1000,
66
+ brakeSystemKn: car.brakeNewtons / 1000,
67
+ dragCoefficient: car.dragCoefficient,
68
+ downforceCoefficient: car.downforceCoefficient,
69
+ tireGrip: car.tireGrip,
70
+ tireCare: car.tireCare,
71
+ massKg: car.mass,
72
+ pace: car.pace,
73
+ racecraft: car.racecraft,
74
+ },
75
+ interaction: serializeParticipantInteraction(car.interaction),
76
+ rank,
77
+ classifiedRank: car.classifiedRank ?? rank,
78
+ finishRank: car.finishRank ?? null,
79
+ status: raceStatus,
80
+ raceStatus,
81
+ wavedFlag: Boolean(car.finished),
82
+ finished: Boolean(car.finished),
83
+ destroyed: Boolean(car.destroyed),
84
+ destroyReason: car.destroyReason ?? null,
85
+ destroyedAt: car.destroyedAt ?? null,
86
+ dnf: Boolean(car.destroyed || car.outOfRace),
87
+ dnfReason: car.dnfReason ?? car.destroyReason ?? null,
88
+ dnfAt: car.dnfAt ?? car.destroyedAt ?? null,
89
+ dnfOrder: car.dnfOrder ?? null,
90
+ outOfRace: Boolean(car.outOfRace || car.destroyed),
91
+ finishTime,
92
+ penaltySeconds,
93
+ adjustedFinishTime: finishTime == null ? null : finishTime + penaltySeconds,
94
+ previousX: car.previousX ?? car.x,
95
+ previousY: car.previousY ?? car.y,
96
+ x: car.x,
97
+ y: car.y,
98
+ previousHeading: car.previousHeading ?? car.heading,
99
+ heading: car.heading,
100
+ steeringAngle: car.steeringAngle,
101
+ yawRate: car.yawRate,
102
+ turnRadius: car.turnRadius,
103
+ speed: car.speed,
104
+ speedKph: simSpeedToKph(car.speed),
105
+ throttle: car.throttle,
106
+ brake: car.brake,
107
+ appliedControls: serializeAppliedControls(car, dependencies),
108
+ lateralAcceleration: car.lateralAcceleration,
109
+ longitudinalAcceleration: car.longitudinalAcceleration ?? 0,
110
+ lateralG: car.lateralG ?? 0,
111
+ longitudinalG: car.longitudinalG ?? 0,
112
+ gripUsage: car.gripUsage ?? 0,
113
+ slipAngleRadians: car.slipAngleRadians ?? 0,
114
+ tractionLimited: Boolean(car.tractionLimited),
115
+ stabilityState: car.stabilityState ?? 'stable',
116
+ progress: car.progress,
117
+ raceDistance: car.raceDistance,
118
+ distanceMeters: simUnitsToMeters(car.raceDistance),
119
+ lap: car.lap,
120
+ lapTelemetry: serializeLapTelemetry(car.lapTelemetry ?? createLapTelemetry({ length: 1 }, 0, 0)),
121
+ gapAhead: car.gapAhead,
122
+ gapAheadMeters: Number.isFinite(car.gapAhead) ? simUnitsToMeters(car.gapAhead) : Infinity,
123
+ gapAheadSeconds: car.gapAheadSeconds,
124
+ intervalAheadSeconds: car.intervalAheadSeconds,
125
+ leaderGapSeconds: car.leaderGapSeconds,
126
+ gapAheadLaps: car.gapAheadLaps ?? 0,
127
+ intervalAheadLaps: car.intervalAheadLaps ?? car.gapAheadLaps ?? 0,
128
+ leaderGapLaps: car.leaderGapLaps ?? 0,
129
+ drsEligible: car.drsEligible,
130
+ drsActive: car.drsActive,
131
+ drsZoneId: car.drsZoneId,
132
+ drsZoneEnabled: car.drsZoneEnabled,
133
+ canAttack: car.canAttack,
134
+ signedOffset: car.trackState?.signedOffset ?? 0,
135
+ crossTrackError: car.trackState?.crossTrackError ?? 0,
136
+ surface: car.trackState?.surface ?? 'track',
137
+ wheels: serializeWheels(car.wheelStates),
138
+ inPitLane: Boolean(car.trackState?.inPitLane),
139
+ pitLanePart: car.trackState?.pitLanePart ?? null,
140
+ pitBoxId: car.trackState?.pitBoxId ?? null,
141
+ pitLaneCrossTrackError: car.trackState?.pitLaneCrossTrackError ?? null,
142
+ contactCooldown: car.contactCooldown,
143
+ tireEnergy: car.tireEnergy,
144
+ usedTireCompounds: [...(car.usedTireCompounds ?? [])],
145
+ pitIntent: normalizePitIntent(car.pitStop?.intent) ?? PIT_INTENT_NONE,
146
+ pitStop: serializePitStop(car.pitStop),
147
+ positionSource: 'integrated-vehicle',
148
+ };
149
+ }
150
+
151
+ export function serializeRenderCar(car, dependencies) {
152
+ const { serializeRenderPitStop } = dependencies;
153
+ return {
154
+ id: car.id,
155
+ color: car.color,
156
+ previousX: car.previousX ?? car.x,
157
+ previousY: car.previousY ?? car.y,
158
+ x: car.x,
159
+ y: car.y,
160
+ previousHeading: car.previousHeading ?? car.heading,
161
+ heading: car.heading,
162
+ drsActive: Boolean(car.drsActive),
163
+ destroyed: Boolean(car.destroyed),
164
+ destroyReason: car.destroyReason ?? null,
165
+ dnf: Boolean(car.destroyed || car.outOfRace),
166
+ dnfReason: car.dnfReason ?? car.destroyReason ?? null,
167
+ dnfAt: car.dnfAt ?? car.destroyedAt ?? null,
168
+ dnfOrder: car.dnfOrder ?? null,
169
+ outOfRace: Boolean(car.outOfRace || car.destroyed),
170
+ pitStop: serializeRenderPitStop(car.pitStop),
171
+ interaction: serializeParticipantInteraction(car.interaction),
172
+ };
173
+ }
174
+
175
+ export function serializeObservationCar(car, rank, dependencies) {
176
+ const {
177
+ createLapTelemetry,
178
+ normalizePitIntent,
179
+ PIT_INTENT_NONE,
180
+ serializeLapTelemetry,
181
+ serializeObservationPitStop,
182
+ serializeWheels: serializeWheelState = serializeWheels,
183
+ simSpeedToKph,
184
+ } = dependencies;
185
+ return {
186
+ id: car.id,
187
+ rank,
188
+ previousX: car.previousX ?? car.x,
189
+ previousY: car.previousY ?? car.y,
190
+ x: car.x,
191
+ y: car.y,
192
+ previousHeading: car.previousHeading ?? car.heading,
193
+ heading: car.heading,
194
+ steeringAngle: car.steeringAngle,
195
+ yawRate: car.yawRate,
196
+ speed: car.speed,
197
+ speedKph: simSpeedToKph(car.speed),
198
+ throttle: car.throttle,
199
+ brake: car.brake,
200
+ appliedControls: serializeAppliedControls(car, dependencies),
201
+ lateralG: car.lateralG ?? 0,
202
+ longitudinalG: car.longitudinalG ?? 0,
203
+ gripUsage: car.gripUsage ?? 0,
204
+ slipAngleRadians: car.slipAngleRadians ?? 0,
205
+ tractionLimited: Boolean(car.tractionLimited),
206
+ stabilityState: car.stabilityState ?? 'stable',
207
+ destroyed: Boolean(car.destroyed),
208
+ destroyReason: car.destroyReason ?? null,
209
+ destroyedAt: car.destroyedAt ?? null,
210
+ dnf: Boolean(car.destroyed || car.outOfRace),
211
+ dnfReason: car.dnfReason ?? car.destroyReason ?? null,
212
+ dnfAt: car.dnfAt ?? car.destroyedAt ?? null,
213
+ dnfOrder: car.dnfOrder ?? null,
214
+ outOfRace: Boolean(car.outOfRace || car.destroyed),
215
+ progress: car.progress,
216
+ raceDistance: car.raceDistance,
217
+ lap: car.lap,
218
+ lapTelemetry: serializeLapTelemetry(car.lapTelemetry ?? createLapTelemetry({ length: 1 }, 0, 0)),
219
+ signedOffset: car.trackState?.signedOffset ?? 0,
220
+ crossTrackError: car.trackState?.crossTrackError ?? 0,
221
+ surface: car.trackState?.surface ?? 'track',
222
+ trackHeadingError: car.trackHeadingError,
223
+ trackState: car.trackState ? {
224
+ distance: car.trackState.distance,
225
+ signedOffset: car.trackState.signedOffset,
226
+ crossTrackError: car.trackState.crossTrackError,
227
+ surface: car.trackState.surface,
228
+ inPitLane: Boolean(car.trackState.inPitLane),
229
+ pitLanePart: car.trackState.pitLanePart ?? null,
230
+ pitBoxId: car.trackState.pitBoxId ?? null,
231
+ curvature: car.trackState.curvature ?? 0,
232
+ } : null,
233
+ wheels: serializeWheelState(car.wheelStates),
234
+ inPitLane: Boolean(car.trackState?.inPitLane),
235
+ pitLanePart: car.trackState?.pitLanePart ?? null,
236
+ pitBoxId: car.trackState?.pitBoxId ?? null,
237
+ tireEnergy: car.tireEnergy ?? null,
238
+ pitIntent: normalizePitIntent(car.pitStop?.intent) ?? PIT_INTENT_NONE,
239
+ pitStop: serializeObservationPitStop(car.pitStop),
240
+ interaction: serializeParticipantInteraction(car.interaction),
241
+ };
242
+ }
243
+
244
+ export function serializeTrainingCar(car, rank, dependencies) {
245
+ const {
246
+ normalizePitIntent,
247
+ PIT_INTENT_NONE,
248
+ serializeObservationPitStop,
249
+ serializeWheels: serializeWheelState = serializeWheels,
250
+ simSpeedToKph,
251
+ simUnitsToMeters,
252
+ } = dependencies;
253
+ return {
254
+ id: car.id,
255
+ rank,
256
+ previousX: car.previousX ?? car.x,
257
+ previousY: car.previousY ?? car.y,
258
+ x: car.x,
259
+ y: car.y,
260
+ previousHeading: car.previousHeading ?? car.heading,
261
+ heading: car.heading,
262
+ steeringAngle: car.steeringAngle,
263
+ yawRate: car.yawRate,
264
+ speed: car.speed,
265
+ speedKph: simSpeedToKph(car.speed),
266
+ throttle: car.throttle,
267
+ brake: car.brake,
268
+ appliedControls: serializeAppliedControls(car, dependencies),
269
+ lateralG: car.lateralG ?? 0,
270
+ longitudinalG: car.longitudinalG ?? 0,
271
+ gripUsage: car.gripUsage ?? 0,
272
+ slipAngleRadians: car.slipAngleRadians ?? 0,
273
+ tractionLimited: Boolean(car.tractionLimited),
274
+ stabilityState: car.stabilityState ?? 'stable',
275
+ destroyed: Boolean(car.destroyed),
276
+ destroyReason: car.destroyReason ?? null,
277
+ destroyedAt: car.destroyedAt ?? null,
278
+ dnf: Boolean(car.destroyed || car.outOfRace),
279
+ dnfReason: car.dnfReason ?? car.destroyReason ?? null,
280
+ dnfAt: car.dnfAt ?? car.destroyedAt ?? null,
281
+ dnfOrder: car.dnfOrder ?? null,
282
+ outOfRace: Boolean(car.outOfRace || car.destroyed),
283
+ progress: car.progress,
284
+ raceDistance: car.raceDistance,
285
+ distanceMeters: simUnitsToMeters(car.raceDistance ?? car.progress ?? 0),
286
+ lap: car.lap,
287
+ lapTelemetry: {
288
+ completedLaps: car.lapTelemetry?.completedLaps ?? 0,
289
+ lastLapTime: car.lapTelemetry?.lastLapTime ?? null,
290
+ bestLapTime: car.lapTelemetry?.bestLapTime ?? null,
291
+ },
292
+ signedOffset: car.trackState?.signedOffset ?? 0,
293
+ crossTrackError: car.trackState?.crossTrackError ?? 0,
294
+ surface: car.trackState?.surface ?? 'track',
295
+ trackHeadingError: car.trackHeadingError,
296
+ trackState: car.trackState ? {
297
+ distance: car.trackState.distance,
298
+ signedOffset: car.trackState.signedOffset,
299
+ crossTrackError: car.trackState.crossTrackError,
300
+ surface: car.trackState.surface,
301
+ inPitLane: Boolean(car.trackState.inPitLane),
302
+ pitLanePart: car.trackState.pitLanePart ?? null,
303
+ pitBoxId: car.trackState.pitBoxId ?? null,
304
+ curvature: car.trackState.curvature ?? 0,
305
+ heading: car.trackState.heading,
306
+ } : null,
307
+ wheels: serializeWheelState(car.wheelStates),
308
+ inPitLane: Boolean(car.trackState?.inPitLane),
309
+ pitLanePart: car.trackState?.pitLanePart ?? null,
310
+ pitBoxId: car.trackState?.pitBoxId ?? null,
311
+ tireEnergy: car.tireEnergy ?? null,
312
+ pitIntent: normalizePitIntent(car.pitStop?.intent) ?? PIT_INTENT_NONE,
313
+ pitStop: serializeObservationPitStop(car.pitStop),
314
+ interaction: serializeParticipantInteraction(car.interaction),
315
+ };
316
+ }