@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
package/src/index.d.ts CHANGED
@@ -1,7 +1,14 @@
1
+ import type { PaddockProceduralTrackProfile, PaddockProceduralTrackOptions, PaddockTrackPoint, PaddockDrsZone, PaddockResolvedProceduralTrackOptions, PaddockProceduralTrack } from './data/trackTypes.js';
2
+ import type { TireCompound, CustomField, CustomFieldInput, EntryPresentation, SimulatorDriver, DriverRatings, VehicleRatings, DriverBlueprint, VehicleBlueprint, DriverConstructorArgs, VehicleConstructorArgs, DriverData, VehicleData, ChampionshipEntryBlueprint, NormalizedSimulatorDriver } from './data/entryTypes.js';
3
+ export type { PaddockProceduralTrackProfile, PaddockProceduralTrackOptions, PaddockTrackPoint, PaddockDrsZone, PaddockResolvedProceduralTrackOptions, PaddockProceduralTrack } from './data/trackTypes.js';
4
+ export type { TireCompound, CustomField, CustomFieldInput, EntryPresentation, SimulatorDriver, DriverRatings, VehicleRatings, DriverBlueprint, VehicleBlueprint, DriverConstructorArgs, VehicleConstructorArgs, ChampionshipEntryBlueprint, NormalizedSimulatorDriver } from './data/entryTypes.js';
5
+ export { DriverData, VehicleData } from './data/entryTypes.js';
6
+
1
7
  import type {
2
8
  PaddockActionSpec,
3
9
  PaddockDriverControllerLoop,
4
10
  PaddockDriverControllerLoopOptions,
11
+ PaddockEnvironmentResult,
5
12
  PaddockEnvironmentOptions,
6
13
  PaddockObservationSpec,
7
14
  PaddockParticipantInteraction as EnvPaddockParticipantInteraction,
@@ -12,6 +19,11 @@ import type {
12
19
  PaddockReplayGhostSnapshot as EnvPaddockReplayGhostSnapshot,
13
20
  PaddockReplayGhostTrajectorySample as EnvPaddockReplayGhostTrajectorySample,
14
21
  } from './environment/index.js';
22
+ import type {
23
+ PaddockPitCrewStats,
24
+ PaddockThemeSelector,
25
+ TeamData,
26
+ } from './data/teamTypes.js';
15
27
 
16
28
  export type {
17
29
  PaddockActionSpec,
@@ -20,13 +32,18 @@ export type {
20
32
  PaddockDriverControllerContext,
21
33
  PaddockDriverControllerLoop,
22
34
  PaddockDriverControllerLoopOptions,
35
+ PaddockEnvironmentResult,
36
+ PaddockEnvironmentSnapshotResult,
23
37
  PaddockObservationSpec,
24
38
  } from './environment/index.js';
39
+ export type {
40
+ PaddockPitCrewStats,
41
+ PaddockThemeSelector,
42
+ TeamData,
43
+ } from './data/teamTypes.js';
25
44
 
26
- export type TireCompound = 'S' | 'M' | 'H';
27
45
  export type PaddockPhysicsMode = 'arcade' | 'advanced';
28
46
  export type PaddockWarmupPolicy = 'config-change' | 'always' | 'never';
29
- export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
30
47
 
31
48
  export interface PaddockWarmupOptions {
32
49
  enabled?: boolean;
@@ -34,43 +51,6 @@ export interface PaddockWarmupOptions {
34
51
  steps?: number;
35
52
  }
36
53
 
37
- export interface PaddockProceduralTrackOptions {
38
- profile?: PaddockProceduralTrackProfile;
39
- minLengthMeters?: number;
40
- maxLengthMeters?: number;
41
- startStraightMeters?: number;
42
- includePitLane?: boolean;
43
- length?: {
44
- minMeters?: number;
45
- maxMeters?: number;
46
- };
47
- startStraight?: {
48
- gridMeters?: number;
49
- exitMeters?: number;
50
- blendMeters?: number;
51
- lockExtraMeters?: number;
52
- };
53
- pitLane?: {
54
- enabled?: boolean;
55
- };
56
- shape?: {
57
- scale?: number;
58
- cornerDensity?: number;
59
- variation?: number;
60
- };
61
- validation?: {
62
- minClearanceMultiplier?: number;
63
- minShapeVariation?: number;
64
- minNonAdjacentArcMeters?: number;
65
- maxLocalTurnRadians?: number;
66
- maxSampleHeadingDeltaRadians?: number;
67
- };
68
- attempts?: {
69
- primary?: number;
70
- fallback?: number;
71
- };
72
- }
73
-
74
54
  export type PaddockStabilityState = 'stable' | 'understeer' | 'oversteer' | 'spin-risk' | 'destroyed';
75
55
  export type PaddockPitIntent = 0 | 1 | 2;
76
56
  export type PaddockPitIntentRequest = PaddockPitIntent | {
@@ -90,6 +70,7 @@ export type TimingTowerVerticalFit = 'expand-race-view' | 'scroll';
90
70
  export type TimingGapMode = 'interval' | 'leader';
91
71
  export type LayoutPreset = 'standard' | 'left-tower-overlay';
92
72
  export type CameraControlsMode = 'embedded' | 'external' | false;
73
+ export type ViewControlsMode = 'switch' | 'toggle';
93
74
  export type PaddockPresetName = 'dashboard' | 'timing-overlay' | 'compact-race' | 'full-dashboard';
94
75
  export type TelemetryModuleName = 'core' | 'sectors' | 'lapTimes' | 'sectorTimes';
95
76
  export type SectorPerformanceStatus = 'overall-best' | 'personal-best' | 'slower';
@@ -101,164 +82,7 @@ export type PaddockReplayGhostTrajectorySample = EnvPaddockReplayGhostTrajectory
101
82
  export type PaddockReplayGhostOptions = EnvPaddockReplayGhostOptions;
102
83
  export type PaddockReplayGhostSnapshot = EnvPaddockReplayGhostSnapshot;
103
84
 
104
- export interface CustomField {
105
- label: string;
106
- value: string;
107
- }
108
-
109
- export type CustomFieldInput = CustomField[] | Record<string, string>;
110
-
111
- export interface TeamData {
112
- id?: string;
113
- name?: string;
114
- color?: string;
115
- icon?: string;
116
- theme?: PaddockThemeSelector;
117
- pitCrew?: PaddockPitCrewStats;
118
- pitCrewStats?: PaddockPitCrewStats;
119
- }
120
-
121
- export interface PaddockPitCrewStats {
122
- speed?: number;
123
- consistency?: number;
124
- reliability?: number;
125
- }
126
-
127
- export interface SimulatorDriver {
128
- id: string;
129
- name: string;
130
- color: string;
131
- link?: string;
132
- icon?: string;
133
- code?: string;
134
- timingName?: string;
135
- tire?: TireCompound;
136
- raceData?: string[];
137
- customFields?: CustomFieldInput;
138
- team?: TeamData | null;
139
- driverNumber?: number;
140
- }
141
-
142
- export interface DriverRatings {
143
- pace: number;
144
- racecraft: number;
145
- aggression: number;
146
- riskTolerance: number;
147
- patience: number;
148
- consistency: number;
149
- }
150
-
151
- export interface VehicleRatings {
152
- power: number;
153
- braking: number;
154
- aero: number;
155
- dragEfficiency: number;
156
- mechanicalGrip: number;
157
- weightControl: number;
158
- tireCare: number;
159
- }
160
-
161
- export interface DriverBlueprint extends Partial<DriverRatings> {
162
- customFields?: CustomFieldInput;
163
- }
164
-
165
- export interface VehicleBlueprint extends Partial<VehicleRatings> {
166
- id?: string | null;
167
- name?: string | null;
168
- customFields?: CustomFieldInput;
169
- }
170
-
171
- export interface DriverConstructorArgs {
172
- ratings: DriverRatings;
173
- customFields: CustomField[];
174
- pace: number;
175
- racecraft: number;
176
- consistency: number;
177
- personality: {
178
- aggression: number;
179
- riskTolerance: number;
180
- patience: number;
181
- };
182
- }
183
-
184
- export interface VehicleConstructorArgs {
185
- id: string | null;
186
- name: string | null;
187
- ratings: VehicleRatings;
188
- customFields: CustomField[];
189
- powerNewtons: number;
190
- brakeNewtons: number;
191
- downforceCoefficient: number;
192
- dragCoefficient: number;
193
- tireGrip: number;
194
- mass: number;
195
- tireCare: number;
196
- }
197
-
198
- export class DriverData {
199
- constructor(input?: DriverBlueprint);
200
- readonly pace: number;
201
- readonly racecraft: number;
202
- readonly aggression: number;
203
- readonly riskTolerance: number;
204
- readonly patience: number;
205
- readonly consistency: number;
206
- readonly customFields: CustomField[];
207
- ratings(): DriverRatings;
208
- toConstructorArgs(): DriverConstructorArgs;
209
- }
210
-
211
- export class VehicleData {
212
- constructor(input?: VehicleBlueprint);
213
- readonly id: string | null;
214
- readonly name: string | null;
215
- readonly power: number;
216
- readonly braking: number;
217
- readonly aero: number;
218
- readonly dragEfficiency: number;
219
- readonly mechanicalGrip: number;
220
- readonly weightControl: number;
221
- readonly tireCare: number;
222
- readonly customFields: CustomField[];
223
- ratings(): VehicleRatings;
224
- toConstructorArgs(): VehicleConstructorArgs;
225
- }
226
-
227
- export interface ChampionshipEntryBlueprint {
228
- driverId: string;
229
- driverNumber?: number;
230
- timingName?: string;
231
- driver?: DriverData | DriverBlueprint;
232
- vehicle?: VehicleData | VehicleBlueprint;
233
- team?: TeamData;
234
- }
235
-
236
- export interface NormalizedSimulatorDriver extends SimulatorDriver {
237
- code: string;
238
- timingCode: string;
239
- raceName: string;
240
- driverNumber: number;
241
- team: TeamData | null;
242
- pace: number;
243
- racecraft: number;
244
- consistency: number;
245
- personality: DriverConstructorArgs['personality'];
246
- vehicle: VehicleConstructorArgs;
247
- constructorArgs: {
248
- driver: DriverConstructorArgs;
249
- vehicle: VehicleConstructorArgs;
250
- };
251
- championship: {
252
- id: string;
253
- name: string;
254
- season: number;
255
- entryIndex: number;
256
- vehicleId: string | null;
257
- };
258
- }
259
-
260
85
  export type PaddockThemeMode = 'dark' | 'light' | 'system';
261
- export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
262
86
  export type PaddockThemeTokenValue = string | { light?: string; dark?: string };
263
87
  export type PaddockThemeTokenName =
264
88
  | 'primary'
@@ -366,6 +190,8 @@ export interface PaddockThemeSyncOptions {
366
190
  export interface F1SimulatorUiOptions {
367
191
  layoutPreset?: LayoutPreset;
368
192
  cameraControls?: CameraControlsMode;
193
+ /** Two mode buttons by default; toggle uses one Start race / Stop race action. */
194
+ viewControls?: ViewControlsMode;
369
195
  showFps?: boolean;
370
196
  showTimingTower?: boolean;
371
197
  showTelemetry?: boolean;
@@ -959,14 +785,14 @@ export interface PaddockEnvironmentResultOptions {
959
785
  }
960
786
 
961
787
  export interface F1SimulatorExpertApi {
962
- reset(options?: unknown): unknown;
963
- step(actions: Record<string, F1SimulatorExpertAction>): unknown;
788
+ reset(options?: unknown): PaddockEnvironmentResult;
789
+ step(actions: Record<string, F1SimulatorExpertAction>): PaddockEnvironmentResult;
964
790
  resetDrivers?(
965
791
  placements: Record<string, PaddockScenarioPlacement>,
966
792
  resultOptions?: PaddockEnvironmentResultOptions
967
- ): unknown;
968
- getObservation(): unknown;
969
- getState(): unknown;
793
+ ): PaddockEnvironmentResult;
794
+ getObservation(): PaddockEnvironmentResult['observation'];
795
+ getState(): PaddockEnvironmentResult['state'];
970
796
  getActionSpec(): PaddockActionSpec;
971
797
  getObservationSpec(): PaddockObservationSpec;
972
798
  attachExternalRenderer(source: PaddockExternalRendererSource): void;
@@ -975,7 +801,16 @@ export interface F1SimulatorExpertApi {
975
801
  destroy(): void;
976
802
  }
977
803
 
804
+ export type PaddockView = 'grid-walk' | 'race';
805
+
806
+ export interface GridWalkOptions {
807
+ enabled?: boolean;
808
+ initialView?: PaddockView;
809
+ ambientMotion?: boolean;
810
+ }
811
+
978
812
  export interface F1SimulatorOptions extends F1SimulatorCallbacks {
813
+ gridWalk?: GridWalkOptions;
979
814
  preset?: PaddockPresetName;
980
815
  drivers: SimulatorDriver[];
981
816
  entries?: ChampionshipEntryBlueprint[];
@@ -1031,6 +866,8 @@ export interface F1MountedSimulator {
1031
866
  getTheme(): ResolvedPaddockTheme;
1032
867
  syncThemeFrom(source: Element, options?: PaddockThemeSyncOptions): () => void;
1033
868
  selectDriver(driverId: string): void;
869
+ setView(view: PaddockView): boolean;
870
+ getView(): PaddockView;
1034
871
  setSafetyCarDeployed(deployed: boolean): void;
1035
872
  setRedFlagDeployed(deployed: boolean): void;
1036
873
  setPitLaneOpen(open: boolean): void;
@@ -1053,6 +890,7 @@ export interface PaddockSimulatorController {
1053
890
  readonly expert: F1SimulatorExpertApi | null;
1054
891
  mountRaceControls<T extends Element>(root: T): T;
1055
892
  mountCameraControls<T extends Element>(root: T): T;
893
+ mountViewControls<T extends Element>(root: T): T;
1056
894
  mountSafetyCarControl<T extends Element>(root: T): T;
1057
895
  mountTimingTower<T extends Element>(root: T): T;
1058
896
  mountRaceCanvas<T extends Element>(root: T, options?: MountRaceCanvasOptions): T;
@@ -1075,6 +913,8 @@ export interface PaddockSimulatorController {
1075
913
  getTheme(): ResolvedPaddockTheme;
1076
914
  syncThemeFrom(source: Element, options?: PaddockThemeSyncOptions): () => void;
1077
915
  selectDriver(driverId: string): void;
916
+ setView(view: PaddockView): boolean;
917
+ getView(): PaddockView;
1078
918
  setSafetyCarDeployed(deployed: boolean): void;
1079
919
  setRedFlagDeployed(deployed: boolean): void;
1080
920
  setPitLaneOpen(open: boolean): void;
@@ -1133,11 +973,12 @@ export function simSpeedToMetersPerSecond(simSpeed: number): number;
1133
973
 
1134
974
  export function createPaddockSimulator(options: F1SimulatorOptions): PaddockSimulatorController;
1135
975
  export function mountF1Simulator(root: Element, options: F1SimulatorOptions): Promise<F1MountedSimulator>;
1136
- export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions): unknown;
976
+ export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions): PaddockProceduralTrack;
1137
977
  export function createPaddockDriverControllerLoop(options: PaddockDriverControllerLoopOptions): PaddockDriverControllerLoop;
1138
978
 
1139
979
  export function mountRaceControls<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
1140
980
  export function mountCameraControls<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
981
+ export function mountViewControls<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
1141
982
  export function mountSafetyCarControl<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
1142
983
  export function mountTimingTower<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
1143
984
  export function mountRaceCanvas<T extends Element>(
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  createPaddockSimulator,
9
9
  mountCarDriverOverview,
10
10
  mountCameraControls,
11
+ mountViewControls,
11
12
  mountRaceCanvas,
12
13
  mountRaceControls,
13
14
  mountRaceDataPanel,
@@ -60,14 +61,30 @@ function assertMountRoot(root) {
60
61
  }
61
62
  }
62
63
 
64
+ const activeSimulatorRoots = new WeakMap();
65
+
63
66
  export async function mountF1Simulator(root, options = {}) {
64
67
  assertMountRoot(root);
68
+ if (activeSimulatorRoots.has(root)) {
69
+ throw new Error('mountF1Simulator root already has an active simulator. Destroy it before mounting again.');
70
+ }
65
71
 
66
- let resolvedOptions = resolveF1SimulatorOptions(options);
67
- root.innerHTML = createF1SimulatorShell(resolvedOptions);
68
- const shell = root.querySelector('[data-f1-simulator-shell]');
69
- const app = new F1SimulatorApp(shell, resolvedOptions);
70
- await app.init();
72
+ const mountToken = {};
73
+ activeSimulatorRoots.set(root, mountToken);
74
+ let app = null;
75
+ let resolvedOptions;
76
+ try {
77
+ resolvedOptions = resolveF1SimulatorOptions(options);
78
+ root.innerHTML = createF1SimulatorShell(resolvedOptions);
79
+ const shell = root.querySelector('[data-f1-simulator-shell]');
80
+ app = new F1SimulatorApp(shell, resolvedOptions);
81
+ await app.init();
82
+ } catch (error) {
83
+ app?.destroy();
84
+ if (activeSimulatorRoots.get(root) === mountToken) activeSimulatorRoots.delete(root);
85
+ root.innerHTML = '';
86
+ throw error;
87
+ }
71
88
 
72
89
  const syncResolvedTimingGapMode = (timingGapMode) => {
73
90
  resolvedOptions = {
@@ -90,7 +107,10 @@ export async function mountF1Simulator(root, options = {}) {
90
107
  },
91
108
  destroy() {
92
109
  app.destroy();
93
- root.innerHTML = '';
110
+ if (activeSimulatorRoots.get(root) === mountToken) {
111
+ activeSimulatorRoots.delete(root);
112
+ root.innerHTML = '';
113
+ }
94
114
  },
95
115
  restart(nextOptions = {}) {
96
116
  const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(syncResolvedRuntimeOptions(), nextOptions));
@@ -116,6 +136,8 @@ export async function mountF1Simulator(root, options = {}) {
116
136
  selectDriver(driverId) {
117
137
  app.selectCar(driverId, { focus: true });
118
138
  },
139
+ setView(view) { return app.setView(view); },
140
+ getView() { return app.getView(); },
119
141
  setSafetyCarDeployed(deployed) {
120
142
  app.setSafetyCarDeployed(deployed);
121
143
  },
@@ -1,11 +1,4 @@
1
- function escapeHtml(value) {
2
- return String(value ?? '')
3
- .replaceAll('&', '&amp;')
4
- .replaceAll('<', '&lt;')
5
- .replaceAll('>', '&gt;')
6
- .replaceAll('"', '&quot;')
7
- .replaceAll("'", '&#39;');
8
- }
1
+ import { escapeHtml } from '../ui/htmlEscaping.js';
9
2
 
10
3
  function normalizeVariant(value) {
11
4
  return value === 'custom' ? 'custom' : 'lights';
@@ -0,0 +1,6 @@
1
+ // Display objects own their children, while simulator textures remain shared.
2
+ export function destroyDisplayChildren(container) {
3
+ container?.removeChildren?.().forEach((child) => {
4
+ child.destroy?.({ children: true, texture: false, textureSource: false });
5
+ });
6
+ }
@@ -1,6 +1,6 @@
1
1
  import { Container } from 'pixi.js';
2
2
  import { WORLD } from '../simulation/track/trackModel.js';
3
- import { destroyDisplayChildren } from './track/trackRenderGeometry.js';
3
+ import { destroyDisplayChildren } from './displayLifecycle.js';
4
4
  import { addAsphalt, addBarriers, addBorders, addBoundaryUnderlay, addGrass, addGravelRunoff, addKerbs } from './track/trackMaterialRenderer.js';
5
5
  import { addPitLane, addPitLaneRunoff } from './track/pitLaneRenderer.js';
6
6
  import { addStartingGrid } from './track/gridRenderer.js';
@@ -11,6 +11,9 @@ function interpolateAngle(previous, current, amount) {
11
11
 
12
12
  function interpolateMovingEntityInto(target, entity, alpha) {
13
13
  Object.assign(target, entity);
14
+ // Retirement stops pose integration, so its previous endpoint may be from
15
+ // the last moving step. A stationary wreck must not replay that movement.
16
+ if (entity.destroyed || entity.outOfRace) return target;
14
17
  target.x = lerp(entity.previousX ?? entity.x, entity.x, alpha);
15
18
  target.y = lerp(entity.previousY ?? entity.y, entity.y, alpha);
16
19
  target.heading = interpolateAngle(entity.previousHeading ?? entity.heading, entity.heading, alpha);
@@ -1,5 +1,5 @@
1
1
  import { Graphics } from 'pixi.js';
2
- import { offsetTrackPoint, pointAt } from '../../simulation/trackModel.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/track/trackModel.js';
3
3
  import { FINISH_LINE_COLUMNS, FINISH_LINE_DEPTH } from './trackRenderConstants.js';
4
4
 
5
5
  export function addFinishLine(asset, track) {
@@ -1,5 +1,5 @@
1
1
  import { Graphics } from 'pixi.js';
2
- import { offsetTrackPoint, pointAt } from '../../simulation/trackModel.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/track/trackModel.js';
3
3
  import { START_GRID_BOX_LENGTH, START_GRID_BOX_WIDTH, START_GRID_FIRST_DISTANCE, START_GRID_LATERAL_OFFSET, START_GRID_SLOT_COUNT, START_GRID_SLOT_SPACING } from './trackRenderConstants.js';
4
4
 
5
5
  export function addStartingGrid(asset, track) {
@@ -1,7 +1,7 @@
1
- import { nearestTrackState, offsetTrackPoint } from '../../simulation/trackModel.js';
1
+ import { nearestTrackState, offsetTrackPoint } from '../../simulation/track/trackModel.js';
2
2
  import { queryNearbyTrackProjections } from '../../simulation/track/trackQueryIndex.js';
3
3
  import { metersToSimUnits } from '../../simulation/units.js';
4
- import { NON_LOCAL_SAMPLE_STEP, OFFSET_GAP_SAMPLE_COUNT, OFFSET_SEGMENT_SAMPLE_COUNT } from './trackRenderConstants.js';
4
+ import { NON_LOCAL_SAMPLE_STEP, OFFSET_GAP_SAMPLE_COUNT, OFFSET_SEGMENT_SAMPLE_COUNT, SEGMENTED_STROKE_STEP } from './trackRenderConstants.js';
5
5
  import { arcDistance, interpolatedSegmentPoint, pointDistance } from './trackRenderGeometry.js';
6
6
 
7
7
  export function offsetPointIsLocal(track, source, point, offset) {
@@ -1,5 +1,5 @@
1
1
  import { Graphics } from 'pixi.js';
2
- import { nearestTrackState } from '../../simulation/trackModel.js';
2
+ import { nearestTrackState } from '../../simulation/track/trackModel.js';
3
3
  import { ASPHALT_COLOR, GRAVEL_COLOR, PIT_ASPHALT_COLOR, PIT_BOX_COLOR, PIT_CONNECTOR_WIDTH, PIT_LINE_COLOR, PIT_SPEED_LINE_COLOR } from './trackRenderConstants.js';
4
4
  import { colorToNumber, createTeamPitGroupCorners, drawPolyline, offsetVectorPoint } from './trackRenderGeometry.js';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { Graphics, TilingSprite } from 'pixi.js';
2
- import { offsetTrackPoint } from '../../simulation/trackModel.js';
2
+ import { offsetTrackPoint } from '../../simulation/track/trackModel.js';
3
3
  import { ASPHALT_COLOR, BARRIER_WALL_WIDTH, EDGE_REVEAL_OFFSET, EDGE_REVEAL_WIDTH, GRASS_COLOR, GRAVEL_COLOR, KERB_CURVATURE_THRESHOLD, KERB_OFFSET, KERB_STEP, KERB_WIDTH, MATERIAL_TILE_SCALE, OUTER_BOUNDARY_OFFSET, OUTER_BOUNDARY_WIDTH, RUNOFF_GRASS_COLOR, SEGMENTED_STROKE_STEP, WORLD_BACKGROUND_PADDING_MULTIPLIER } from './trackRenderConstants.js';
4
4
  import { makeTrackPath, textureOrWhite } from './trackRenderGeometry.js';
5
5
  import { getOffsetGapBridges, getOffsetStrokeSegments, offsetSegmentIsSafe } from './offsetStrokeSafety.js';
@@ -149,7 +149,7 @@ export function addBoundaryUnderlay(asset, track) {
149
149
  });
150
150
  });
151
151
 
152
- drawKerbStripes(asset, boundary, track, { clipUnsafeSegments: false });
152
+ drawKerbStripes(boundary, track, { clipUnsafeSegments: false });
153
153
 
154
154
  [-1, 1].forEach((side) => {
155
155
  drawContinuousOffsetStroke(boundary, track, {
@@ -214,7 +214,7 @@ export function getTrackMaterialBands(track) {
214
214
  };
215
215
  }
216
216
 
217
- export function drawKerbStripes(asset, kerbs, track, { clipUnsafeSegments }) {
217
+ export function drawKerbStripes(kerbs, track, { clipUnsafeSegments }) {
218
218
  const samples = track.samples.slice(0, -1);
219
219
 
220
220
  for (let index = 0; index < samples.length; index += KERB_STEP) {
@@ -320,7 +320,7 @@ export function addBorders(asset, track) {
320
320
  export function addKerbs(asset, track) {
321
321
  const kerbs = new Graphics();
322
322
  kerbs.label = 'track-kerbs';
323
- drawKerbStripes(asset, kerbs, track, { clipUnsafeSegments: true });
323
+ drawKerbStripes(kerbs, track, { clipUnsafeSegments: true });
324
324
  asset.container.addChild(kerbs);
325
325
 
326
326
  }
@@ -14,7 +14,6 @@ export const PIT_BOX_COLOR = 0x242831;
14
14
  export const PIT_LINE_COLOR = 0xf8fafc;
15
15
  export const PIT_SPEED_LINE_COLOR = 0xffd166;
16
16
  export const PIT_CONNECTOR_WIDTH = metersToSimUnits(12);
17
- export const PIT_EDGE_WIDTH = metersToSimUnits(0.35);
18
17
  export const EDGE_REVEAL_OFFSET = metersToSimUnits(0.35);
19
18
  export const EDGE_REVEAL_WIDTH = metersToSimUnits(0.5);
20
19
  export const OUTER_BOUNDARY_OFFSET = metersToSimUnits(1.1);
@@ -1,5 +1,5 @@
1
1
  import { Graphics, Texture } from 'pixi.js';
2
- import { offsetTrackPoint } from '../../simulation/trackModel.js';
2
+ import { offsetTrackPoint } from '../../simulation/track/trackModel.js';
3
3
  import { PIT_LINE_COLOR } from './trackRenderConstants.js';
4
4
 
5
5
  export function colorToNumber(color, fallback = PIT_LINE_COLOR) {
@@ -29,12 +29,6 @@ export function textureOrWhite(texture) {
29
29
  return texture ?? Texture.WHITE;
30
30
  }
31
31
 
32
- export function destroyDisplayChildren(container) {
33
- container.removeChildren().forEach((child) => {
34
- child.destroy?.({ children: true, texture: false, textureSource: false });
35
- });
36
- }
37
-
38
32
  export function arcDistance(track, first, second) {
39
33
  const delta = Math.abs(first - second);
40
34
  return Math.min(delta, track.length - delta);
@@ -1,7 +1,4 @@
1
- import {
2
- getVehicleGeometryState,
3
- vehicleAxes,
4
- } from './vehicle/vehicleGeometry.js';
1
+ import { getVehicleGeometryState } from './vehicle/vehicleGeometry.js';
5
2
  import { normalizeAngle } from './simMath.js';
6
3
 
7
4
  const DEFAULT_SWEEP_STEPS = 16;
@@ -10,10 +7,6 @@ const DEFAULT_DISTANCE_WINDOW = 150;
10
7
  const COLLISION_PAIR_FIRST_INDEX = Symbol('collisionPairFirstIndex');
11
8
  const COLLISION_PAIR_SECOND_INDEX = Symbol('collisionPairSecondIndex');
12
9
 
13
- function dot(a, b) {
14
- return a.x * b.x + a.y * b.y;
15
- }
16
-
17
10
  function projectShapeInto(target, shape, axis) {
18
11
  let min = Infinity;
19
12
  let max = -Infinity;
@@ -91,10 +84,6 @@ function aabbsOverlap(first, second) {
91
84
  );
92
85
  }
93
86
 
94
- function createSweptAabb(car) {
95
- return getVehicleGeometryState(car).sweptBodyAabb;
96
- }
97
-
98
87
  function writeShapeCollisionResult(target, firstShape, secondShape, axisX, axisY, depth) {
99
88
  target.axis ??= {};
100
89
  target.axis.x = axisX;
@@ -123,10 +112,12 @@ function withCollisionMetadata(collision, timeOfImpact, swept, scratch) {
123
112
  function detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch) {
124
113
  const firstState = getVehicleGeometryState(first);
125
114
  const secondState = getVehicleGeometryState(second);
126
- const current = detectShapeCollision(firstState.current.body, secondState.current.body, scratch);
127
- if (current) return withCollisionMetadata(current, 1, false, scratch);
115
+ if (aabbsOverlap(firstState.bodyAabb, secondState.bodyAabb)) {
116
+ const current = detectShapeCollision(firstState.current.body, secondState.current.body, scratch);
117
+ if (current) return withCollisionMetadata(current, 1, false, scratch);
118
+ }
128
119
 
129
- if (!aabbsOverlap(createSweptAabb(first), createSweptAabb(second))) return null;
120
+ if (!aabbsOverlap(firstState.sweptBodyAabb, secondState.sweptBodyAabb)) return null;
130
121
 
131
122
  const sweepShapes = ensureSweepShapes(scratch, firstState.current.body, secondState.current.body);
132
123
  for (let step = 1; step < sweepSteps; step += 1) {
@@ -340,17 +331,18 @@ function writeInterpolatedBodyShape(target, geometryState, amount) {
340
331
  const x = pose.previousX + (pose.x - pose.previousX) * amount;
341
332
  const y = pose.previousY + (pose.y - pose.previousY) * amount;
342
333
  const heading = normalizeAngle(pose.previousHeading + headingDelta * amount);
343
- const axes = vehicleAxes(heading);
334
+ const cos = Math.cos(heading);
335
+ const sin = Math.sin(heading);
344
336
  const halfLength = target.halfLength;
345
337
  const halfWidth = target.halfWidth;
346
338
 
347
339
  target.center.x = x;
348
340
  target.center.y = y;
349
341
  target.heading = heading;
350
- target.forward.x = axes.forward.x;
351
- target.forward.y = axes.forward.y;
352
- target.right.x = axes.right.x;
353
- target.right.y = axes.right.y;
342
+ target.forward.x = cos;
343
+ target.forward.y = sin;
344
+ target.right.x = -sin;
345
+ target.right.y = cos;
354
346
 
355
347
  writeOffsetCorner(target.corners[0], target.center, target.forward, target.right, halfLength, halfWidth);
356
348
  writeOffsetCorner(target.corners[1], target.center, target.forward, target.right, halfLength, -halfWidth);
@@ -0,0 +1,53 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { simUnitsToMeters } from '../units.js';
3
+ import { VEHICLE_LIMITS, tirePerformanceFactor } from '../vehicle/vehiclePhysics.js';
4
+ import { ADVANCED_VEHICLE_MODEL, advancedDriveForce } from '../vehicle/advancedVehicleModel.js';
5
+ import { advancedLateralAccelerationLimit, advancedRearForceCapacity, advancedRollingResistanceForce } from '../vehicle/advancedTireForces.js';
6
+
7
+ export function advancedSteeringToPoint(car, target) {
8
+ const speed = simUnitsToMeters(car.speed);
9
+ const dx = simUnitsToMeters(target.x - car.x);
10
+ const dy = simUnitsToMeters(target.y - car.y);
11
+ const angle = normalizeAngle(Math.atan2(dy, dx) - car.heading);
12
+ // Pure pursuit gives a geometric curvature in 1/m, then a physical wheel
13
+ // angle. Yaw feedback damps the inertial response without moving the car.
14
+ const curvature = 2 * Math.sin(angle) / Math.max(Math.hypot(dx, dy), 1);
15
+ const desiredYaw = speed * curvature;
16
+ const slip = car.slipAngleRadians ?? 0;
17
+ const countersteer = Math.sign(slip) * Math.max(0, Math.abs(slip) - 0.08) * 0.35;
18
+ return {
19
+ curvature,
20
+ steering: Math.atan(simUnitsToMeters(VEHICLE_LIMITS.wheelbase) * curvature) +
21
+ (desiredYaw - (car.yawRate ?? 0)) * 0.04 + countersteer,
22
+ };
23
+ }
24
+
25
+ export function advancedLongitudinalLimits(car) {
26
+ const speed = simUnitsToMeters(car.speed);
27
+ const tireFactor = tirePerformanceFactor(car.tireEnergy ?? 100);
28
+ const capacity = advancedLateralAccelerationLimit(car, speed, tireFactor);
29
+ const lateralUsage = clamp(Math.abs(speed * (car.yawRate ?? 0)) / capacity, 0, 0.95);
30
+ // Static axle capacity is an estimate: keep a margin for load transfer and
31
+ // reserve the lateral share before requesting rear drive or brake force.
32
+ const longitudinalFraction = Math.sqrt(1 - lateralUsage ** 2) * 0.65;
33
+ const force = capacity * car.mass * longitudinalFraction;
34
+ let driveForce = force * (1 - ADVANCED_VEHICLE_MODEL.frontWeightFraction);
35
+ if (speed < 5 && !car.trackState.onTrack) {
36
+ // A crawling car must overcome all four wheels' rolling resistance. Use
37
+ // the rear tires' remaining adhesion, not an artificial minimum throttle.
38
+ const rearCapacity = advancedRearForceCapacity(car, speed, tireFactor) * Math.sqrt(1 - lateralUsage ** 2);
39
+ const rollingCompensation = advancedRollingResistanceForce(car, speed) * (1 - speed / 5);
40
+ driveForce = Math.min(rearCapacity, driveForce + rollingCompensation);
41
+ }
42
+ const throttleLimit = driveForce / advancedDriveForce(car, speed);
43
+ const brakeLimit = force / car.brakeNewtons;
44
+ return { speed, throttleLimit, brakeLimit };
45
+ }
46
+
47
+ export function advancedSpeedControls(car, targetSpeed) {
48
+ const { speed, throttleLimit, brakeLimit } = advancedLongitudinalLimits(car);
49
+ const error = targetSpeed - speed;
50
+ const brake = clamp(-error / 8, 0, Math.min(0.85, brakeLimit));
51
+ const throttle = brake > 0.03 ? 0 : clamp(error / 8, 0, Math.min(1, throttleLimit));
52
+ return { throttle, brake };
53
+ }