@inventure71/paddockjs 2.0.0 → 3.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 (110) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +92 -17
  4. package/docs/architecture.md +26 -16
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +7 -5
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +121 -33
  9. package/docs/data_model.md +1 -1
  10. package/docs/index.md +10 -0
  11. package/docs/learnings.md +6 -6
  12. package/docs/rules.md +39 -9
  13. package/docs/sense_contract.md +59 -0
  14. package/docs/system_specs.md +76 -20
  15. package/docs/training.md +27 -20
  16. package/package.json +1 -1
  17. package/src/api/PaddockSimulatorController.js +75 -36
  18. package/src/app/BrowserExpertAdapter.js +9 -300
  19. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  20. package/src/app/F1SimulatorApp.js +190 -68
  21. package/src/app/banners/raceDataBannerController.js +71 -36
  22. package/src/app/banners/stewardMessageController.js +5 -4
  23. package/src/app/camera/cameraConstants.js +2 -0
  24. package/src/app/camera/cameraController.js +86 -29
  25. package/src/app/camera/cameraGeometry.js +12 -1
  26. package/src/app/domBindings.js +89 -9
  27. package/src/app/layoutScheduler.js +27 -0
  28. package/src/app/layoutSupport.js +165 -0
  29. package/src/app/raceOverlayClearanceSupport.js +158 -0
  30. package/src/app/readouts/carOverviewRenderer.js +16 -8
  31. package/src/app/readouts/controlStateRenderer.js +1 -1
  32. package/src/app/readouts/raceStatusRenderer.js +30 -21
  33. package/src/app/readouts/readoutFormatters.js +2 -0
  34. package/src/app/readouts/telemetryRenderer.js +17 -8
  35. package/src/app/readouts/timingTowerRenderer.js +62 -10
  36. package/src/app/runtime/runtimeVisibility.js +66 -5
  37. package/src/config/cssValues.js +16 -0
  38. package/src/config/defaultOptions.js +51 -38
  39. package/src/config/restartOptions.js +34 -0
  40. package/src/config/themeOptions.js +825 -0
  41. package/src/config/timingGapMode.js +13 -0
  42. package/src/config/urlOptions.js +25 -0
  43. package/src/data/championship.js +14 -9
  44. package/src/environment/controllerLoop.js +34 -5
  45. package/src/environment/episode.js +14 -2
  46. package/src/environment/index.d.ts +17 -4
  47. package/src/environment/observationVector.js +200 -0
  48. package/src/environment/observations.js +58 -285
  49. package/src/environment/options.js +1 -1
  50. package/src/environment/recorder.js +13 -6
  51. package/src/environment/rewards.js +13 -2
  52. package/src/environment/runtime.js +65 -25
  53. package/src/environment/scenarios.js +22 -3
  54. package/src/environment/sensors/index.js +3 -3
  55. package/src/environment/sensors/opponentRadar.js +13 -3
  56. package/src/environment/sensors/sensorTargets.js +8 -2
  57. package/src/environment/sensors/surfaceRays.js +84 -12
  58. package/src/environment/sensors/trackRays.js +173 -15
  59. package/src/environment/specs.js +78 -40
  60. package/src/index.d.ts +112 -9
  61. package/src/index.js +26 -33
  62. package/src/simulation/driver/driverController.js +1 -1
  63. package/src/simulation/driver/edgeRecovery.js +1 -1
  64. package/src/simulation/driver/racingControls.js +2 -2
  65. package/src/simulation/driver/rejoinControls.js +2 -2
  66. package/src/simulation/pit/pitFlow.js +5 -0
  67. package/src/simulation/pit/pitKinematics.js +24 -0
  68. package/src/simulation/pit/pitQueue.js +5 -0
  69. package/src/simulation/pit/pitRouteMovement.js +7 -0
  70. package/src/simulation/pit/pitRouting.js +2 -4
  71. package/src/simulation/pit/pitService.js +2 -0
  72. package/src/simulation/pit/pitTireService.js +2 -0
  73. package/src/simulation/race/raceLifecycle.js +2 -6
  74. package/src/simulation/race/raceProgress.js +6 -1
  75. package/src/simulation/race/raceSetup.js +2 -6
  76. package/src/simulation/race/raceStep.js +2 -2
  77. package/src/simulation/race/redFlag.js +3 -3
  78. package/src/simulation/race/safetyCar.js +10 -4
  79. package/src/simulation/race/stalledDnf.js +8 -17
  80. package/src/simulation/raceSimulation.js +1 -0
  81. package/src/simulation/rules/rulesReview.js +3 -0
  82. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  83. package/src/simulation/timing/lapTelemetry.js +28 -9
  84. package/src/simulation/track/pitLaneLayout.js +7 -3
  85. package/src/simulation/track/spatialQueries.js +5 -28
  86. package/src/simulation/track/trackQueryGrid.js +94 -0
  87. package/src/simulation/track/trackQueryIndex.js +193 -434
  88. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  89. package/src/simulation/track/trackQueryScratch.js +46 -0
  90. package/src/simulation/track/trackQueryStats.js +28 -0
  91. package/src/simulation/track/trackStatePolicy.js +0 -1
  92. package/src/simulation/vehicle/contactResolution.js +67 -2
  93. package/src/simulation/vehicle/runoffResponse.js +11 -12
  94. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  95. package/src/simulation/vehicle/vehiclePhysics.js +5 -4
  96. package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
  97. package/src/simulation/vehicle/vehicleState.js +36 -2
  98. package/src/simulation/vehicle/wheelSurface.js +3 -1
  99. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  100. package/src/styles.css +1185 -136
  101. package/src/ui/bannerTemplates.js +38 -28
  102. package/src/ui/cameraControlsTemplate.js +16 -5
  103. package/src/ui/carOverviewTemplate.js +12 -5
  104. package/src/ui/raceCanvasTemplate.js +31 -8
  105. package/src/ui/raceControlsTemplate.js +27 -10
  106. package/src/ui/shellTemplate.js +3 -1
  107. package/src/ui/telemetryDrawerTemplate.js +23 -10
  108. package/src/ui/telemetryTemplates.js +58 -32
  109. package/src/ui/templateUtils.js +48 -0
  110. package/src/ui/timingTowerTemplate.js +29 -12
@@ -1,7 +1,8 @@
1
1
  import { normalizeLookaheadMeters } from './observationOptions.js';
2
+ import { effectiveSensorOptions } from './observations.js';
2
3
  import { normalizeRayOptions } from './sensors.js';
3
4
 
4
- const PHYSICAL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
5
+ const MODEL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
5
6
 
6
7
  export function buildActionSpec(options) {
7
8
  const compounds = Array.isArray(options.rules?.modules?.tireStrategy?.compounds) &&
@@ -32,7 +33,7 @@ export function buildObservationSpec(options) {
32
33
  : normalizeLookaheadMeters(options.observation?.lookaheadMeters);
33
34
  const profile = options.observation?.profile ?? 'default';
34
35
  return {
35
- version: profile === 'physical-driver' ? 4 : 2,
36
+ version: profile === 'physical-driver' ? 6 : 3,
36
37
  controlledDrivers: [...options.controlledDrivers],
37
38
  object: {
38
39
  profile,
@@ -45,12 +46,15 @@ export function buildObservationSpec(options) {
45
46
  { name: 'steeringAngleRadians', unit: 'rad' },
46
47
  { name: 'throttle', unit: 'normalized' },
47
48
  { name: 'brake', unit: 'normalized' },
49
+ { name: 'appliedControls', unit: 'nullable:normalized-controls' },
48
50
  { name: 'lateralG', unit: 'g' },
49
51
  { name: 'longitudinalG', unit: 'g' },
50
52
  { name: 'gripUsage', unit: 'ratio' },
51
53
  { name: 'slipAngleRadians', unit: 'rad' },
52
54
  { name: 'tractionLimited', unit: 'boolean' },
53
55
  { name: 'stabilityState', unit: 'label' },
56
+ { name: 'destroyed', unit: 'boolean' },
57
+ { name: 'destroyReason', unit: 'nullable:label' },
54
58
  { name: 'lap', unit: 'count' },
55
59
  { name: 'completedLaps', unit: 'count' },
56
60
  { name: 'lapProgressMeters', unit: 'm' },
@@ -99,40 +103,8 @@ export function buildObservationSpec(options) {
99
103
  { name: 'redFlag', unit: 'boolean' },
100
104
  { name: 'totalLaps', unit: 'count' },
101
105
  ],
102
- rays: {
103
- enabled: Boolean(rayOptions.enabled),
104
- anglesDegrees: [...rayOptions.anglesDegrees],
105
- lengthMeters: rayOptions.lengthMeters,
106
- defaultLengthMeters: rayOptions.defaultLengthMeters,
107
- rays: rayOptions.rays.map((ray) => ({ ...ray })),
108
- channels: [...rayOptions.channels],
109
- precision: rayOptions.precision,
110
- track: {
111
- distanceMeters: { unit: 'm', noHitValue: rayOptions.lengthMeters },
112
- hit: { unit: 'boolean' },
113
- kind: { values: ['exit', 'entry', null] },
114
- },
115
- roadEdge: {
116
- distanceMeters: { unit: 'm', noHitValue: rayOptions.lengthMeters },
117
- hit: { unit: 'boolean' },
118
- kind: { values: ['exit', 'entry', null] },
119
- },
120
- kerb: surfaceRaySpec(rayOptions.lengthMeters),
121
- illegalSurface: surfaceRaySpec(rayOptions.lengthMeters),
122
- car: {
123
- distanceMeters: { unit: 'm', noHitValue: rayOptions.lengthMeters },
124
- hit: { unit: 'boolean' },
125
- driverId: { nullable: true },
126
- targetId: { nullable: true },
127
- targetType: { values: ['car', 'replayGhost', null] },
128
- relativeSpeedKph: { unit: 'kph' },
129
- },
130
- },
131
- nearbyCars: {
132
- enabled: Boolean(nearbyOptions.enabled),
133
- maxCars: nearbyOptions.maxCars,
134
- radiusMeters: nearbyOptions.radiusMeters,
135
- },
106
+ rays: buildRayObjectSpec(rayOptions),
107
+ nearbyCars: buildNearbyObjectSpec(nearbyOptions),
136
108
  track: {
137
109
  lengthMeters: { unit: 'm' },
138
110
  widthMeters: { unit: 'm' },
@@ -149,6 +121,69 @@ export function buildObservationSpec(options) {
149
121
  vector: {
150
122
  schema: buildVectorSchema({ rayOptions, nearbyOptions, lookaheadMeters, profile }),
151
123
  },
124
+ perDriver: buildPerDriverObservationSpecs(options, { rayOptions, nearbyOptions, lookaheadMeters, profile }),
125
+ };
126
+ }
127
+
128
+ function buildPerDriverObservationSpecs(options, { rayOptions, nearbyOptions, lookaheadMeters, profile }) {
129
+ if (Object.keys(options.sensorsByDriver ?? {}).length === 0) return {};
130
+ const defaultSensors = { rays: rayOptions, nearbyCars: nearbyOptions };
131
+ return Object.fromEntries(options.controlledDrivers.map((driverId) => {
132
+ const sensors = effectiveSensorOptions(options, driverId, defaultSensors, true);
133
+ return [driverId, {
134
+ object: {
135
+ rays: buildRayObjectSpec(sensors.rays),
136
+ nearbyCars: buildNearbyObjectSpec(sensors.nearbyCars),
137
+ },
138
+ vector: {
139
+ schema: buildVectorSchema({
140
+ rayOptions: sensors.rays,
141
+ nearbyOptions: sensors.nearbyCars,
142
+ lookaheadMeters,
143
+ profile,
144
+ }),
145
+ },
146
+ }];
147
+ }));
148
+ }
149
+
150
+ function buildRayObjectSpec(rayOptions) {
151
+ return {
152
+ enabled: Boolean(rayOptions.enabled),
153
+ anglesDegrees: [...rayOptions.anglesDegrees],
154
+ lengthMeters: rayOptions.lengthMeters,
155
+ defaultLengthMeters: rayOptions.defaultLengthMeters,
156
+ rays: rayOptions.rays.map((ray) => ({ ...ray })),
157
+ channels: [...rayOptions.channels],
158
+ precision: rayOptions.precision,
159
+ track: {
160
+ distanceMeters: { unit: 'm', noHitValue: rayOptions.lengthMeters },
161
+ hit: { unit: 'boolean' },
162
+ kind: { values: ['exit', 'entry', null] },
163
+ },
164
+ roadEdge: {
165
+ distanceMeters: { unit: 'm', noHitValue: rayOptions.lengthMeters },
166
+ hit: { unit: 'boolean' },
167
+ kind: { values: ['exit', 'entry', null] },
168
+ },
169
+ kerb: surfaceRaySpec(rayOptions.lengthMeters),
170
+ illegalSurface: surfaceRaySpec(rayOptions.lengthMeters),
171
+ car: {
172
+ distanceMeters: { unit: 'm', noHitValue: rayOptions.lengthMeters },
173
+ hit: { unit: 'boolean' },
174
+ driverId: { nullable: true },
175
+ targetId: { nullable: true },
176
+ targetType: { values: ['car', 'replayGhost', null] },
177
+ relativeSpeedKph: { unit: 'kph' },
178
+ },
179
+ };
180
+ }
181
+
182
+ function buildNearbyObjectSpec(nearbyOptions) {
183
+ return {
184
+ enabled: Boolean(nearbyOptions.enabled),
185
+ maxCars: nearbyOptions.maxCars,
186
+ radiusMeters: nearbyOptions.radiusMeters,
152
187
  };
153
188
  }
154
189
 
@@ -193,6 +228,7 @@ function buildVectorSchema({ rayOptions, nearbyOptions, lookaheadMeters, profile
193
228
  { name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
194
229
  { name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
195
230
  { name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
231
+ { name: `contactPatches[${index}].crossTrackErrorMeters`, unit: 'm', scale: 'fixed:meters' },
196
232
  );
197
233
  }
198
234
  }
@@ -212,15 +248,16 @@ function buildVectorSchema({ rayOptions, nearbyOptions, lookaheadMeters, profile
212
248
  { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
213
249
  { name: `rays[${index}].car.hit`, scale: 'boolean' },
214
250
  { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
251
+ { name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
215
252
  );
216
- if (includePhysicalDriverSenses) {
217
- PHYSICAL_RAY_SURFACE_CHANNELS.forEach((channel) => {
253
+ MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
254
+ if (rayOptions.channels?.includes?.(channel)) {
218
255
  schema.push(
219
256
  { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
220
257
  { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
221
258
  );
222
- });
223
- }
259
+ }
260
+ });
224
261
  });
225
262
  }
226
263
  if (nearbyOptions.enabled) {
@@ -234,6 +271,7 @@ function buildVectorSchema({ rayOptions, nearbyOptions, lookaheadMeters, profile
234
271
  { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
235
272
  { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
236
273
  { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
274
+ { name: `nearbyCars[${index}].entityTypeReplayGhost`, scale: 'boolean' },
237
275
  );
238
276
  if (includePhysicalDriverSenses) {
239
277
  schema.push(
package/src/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  PaddockActionSpec,
3
3
  PaddockDriverControllerLoop,
4
4
  PaddockDriverControllerLoopOptions,
5
+ PaddockEnvironmentOptions,
5
6
  PaddockObservationSpec,
6
7
  PaddockParticipantInteraction as EnvPaddockParticipantInteraction,
7
8
  PaddockParticipantInteractionsOptions as EnvPaddockParticipantInteractionsOptions,
@@ -23,7 +24,7 @@ export type {
23
24
  } from './environment/index.js';
24
25
 
25
26
  export type TireCompound = 'S' | 'M' | 'H';
26
- export type PaddockPhysicsMode = 'arcade' | 'simulator';
27
+ export type PaddockPhysicsMode = 'arcade' | 'advanced';
27
28
  export type PaddockWarmupPolicy = 'config-change' | 'always' | 'never';
28
29
  export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
29
30
 
@@ -81,11 +82,12 @@ export type PaddockPitIntentRequest = PaddockPitIntent | {
81
82
  pitTargetCompound?: TireCompound | string;
82
83
  targetTire?: TireCompound | string;
83
84
  };
84
- export type CameraMode = 'overview' | 'leader' | 'selected' | 'show-all' | 'pit';
85
+ export type CameraMode = 'overview' | 'leader' | 'selected' | 'driver' | 'show-all' | 'pit';
85
86
  export type RaceBannerMode = 'project' | 'radio' | 'hidden';
86
87
  export type RaceBannerEnabledMode = 'project' | 'radio';
87
88
  export type RaceDataBannerSize = 'auto' | 'custom';
88
89
  export type TimingTowerVerticalFit = 'expand-race-view' | 'scroll';
90
+ export type TimingGapMode = 'interval' | 'leader';
89
91
  export type LayoutPreset = 'standard' | 'left-tower-overlay';
90
92
  export type CameraControlsMode = 'embedded' | 'external' | false;
91
93
  export type PaddockPresetName = 'dashboard' | 'timing-overlay' | 'compact-race' | 'full-dashboard';
@@ -111,6 +113,7 @@ export interface TeamData {
111
113
  name?: string;
112
114
  color?: string;
113
115
  icon?: string;
116
+ theme?: PaddockThemeSelector;
114
117
  pitCrew?: PaddockPitCrewStats;
115
118
  pitCrewStats?: PaddockPitCrewStats;
116
119
  }
@@ -254,12 +257,84 @@ export interface NormalizedSimulatorDriver extends SimulatorDriver {
254
257
  };
255
258
  }
256
259
 
257
- export interface F1SimulatorTheme {
258
- accentColor?: string;
259
- greenColor?: string;
260
- yellowColor?: string;
261
- timingTowerMaxWidth?: string;
262
- raceViewMinHeight?: string;
260
+ export type PaddockThemeMode = 'dark' | 'light' | 'system';
261
+ export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
262
+ export type PaddockThemeTokenValue = string | { light?: string; dark?: string };
263
+ export type PaddockThemeTokenName =
264
+ | 'primary'
265
+ | 'primaryText'
266
+ | 'secondary'
267
+ | 'secondaryText'
268
+ | 'surface'
269
+ | 'surfaceRaised'
270
+ | 'surfacePanel'
271
+ | 'text'
272
+ | 'mutedText'
273
+ | 'border'
274
+ | 'success'
275
+ | 'warning'
276
+ | 'danger'
277
+ | 'info'
278
+ | 'yellowFlag'
279
+ | 'greenFlag'
280
+ | 'redFlag'
281
+ | 'safetyCar'
282
+ | 'drsActive'
283
+ | 'pitLane'
284
+ | 'track'
285
+ | 'trackEdge'
286
+ | 'timingTowerMaxWidth'
287
+ | 'raceViewMinHeight';
288
+
289
+ export interface F1SimulatorThemeTokens extends Partial<Record<PaddockThemeTokenName, PaddockThemeTokenValue>> {
290
+ /** Compatibility alias for `primary`. Prefer `tokens.primary` in new 3.0.0 code. */
291
+ accentColor?: PaddockThemeTokenValue;
292
+ /** Compatibility alias for `greenFlag`. Prefer `tokens.greenFlag` or `tokens.success`. */
293
+ greenColor?: PaddockThemeTokenValue;
294
+ /** Compatibility alias for `yellowFlag`. Prefer `tokens.yellowFlag` or `tokens.warning`. */
295
+ yellowColor?: PaddockThemeTokenValue;
296
+ /** Compatibility alias for `info`. */
297
+ blueColor?: PaddockThemeTokenValue;
298
+ /** Compatibility alias for `redFlag`. Prefer `tokens.redFlag` or `tokens.danger`. */
299
+ raceControlRedColor?: PaddockThemeTokenValue;
300
+ /** Compatibility alias for `surface`. */
301
+ surfaceColor?: PaddockThemeTokenValue;
302
+ /** Compatibility alias for `surfaceRaised`. */
303
+ surfaceRaisedColor?: PaddockThemeTokenValue;
304
+ /** Compatibility alias for `surfacePanel`. */
305
+ surfacePanelColor?: PaddockThemeTokenValue;
306
+ /** Compatibility alias for `border`. */
307
+ lineColor?: PaddockThemeTokenValue;
308
+ /** Compatibility alias for `border`. */
309
+ lineStrongColor?: PaddockThemeTokenValue;
310
+ /** Compatibility alias for `text`. */
311
+ textColor?: PaddockThemeTokenValue;
312
+ /** Compatibility alias for `mutedText`. */
313
+ mutedTextColor?: PaddockThemeTokenValue;
314
+ /** Compatibility alias for `track`. */
315
+ trackColor?: PaddockThemeTokenValue;
316
+ /** Compatibility alias for `trackEdge`. */
317
+ trackEdgeColor?: PaddockThemeTokenValue;
318
+ }
319
+
320
+ export interface F1SimulatorThemePackage extends F1SimulatorThemeTokens {
321
+ extends?: string;
322
+ tokens?: F1SimulatorThemeTokens;
323
+ components?: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
324
+ light?: F1SimulatorThemeTokens;
325
+ dark?: F1SimulatorThemeTokens;
326
+ }
327
+
328
+ export interface F1SimulatorTheme extends Omit<F1SimulatorThemePackage, 'components'> {
329
+ mode?: PaddockThemeMode;
330
+ use?: string;
331
+ themes?: Record<string, F1SimulatorThemePackage>;
332
+ componentThemes?: Record<string, PaddockThemeSelector | { theme: PaddockThemeSelector }>;
333
+ teamThemes?: Record<string, PaddockThemeSelector | { theme: PaddockThemeSelector }>;
334
+ components?: Record<
335
+ string,
336
+ PaddockThemeSelector | { theme: PaddockThemeSelector } | Partial<Record<string, PaddockThemeTokenName>>
337
+ >;
263
338
  }
264
339
 
265
340
  export interface F1SimulatorUiOptions {
@@ -277,10 +352,15 @@ export interface F1SimulatorUiOptions {
277
352
  };
278
353
  raceDataBannerSize?: RaceDataBannerSize;
279
354
  raceDataTelemetryDetail?: boolean;
355
+ driverCamera?: boolean;
280
356
  penaltyBanners?: boolean;
281
357
  timingPenaltyBadges?: boolean;
282
358
  simulationSpeedControl?: boolean;
283
359
  timingTowerVerticalFit?: TimingTowerVerticalFit;
360
+ timingGapMode?: TimingGapMode;
361
+ timingGapModeToggle?: boolean;
362
+ timingEntryVerticalPadding?: number;
363
+ responsiveNarrowLayout?: boolean;
284
364
  }
285
365
 
286
366
  export interface F1SimulatorDebugOptions {
@@ -638,6 +718,18 @@ export interface WheelSurfaceSnapshot {
638
718
  fullyOutsideWhiteLine: boolean;
639
719
  }
640
720
 
721
+ export interface CarTrackStateSnapshot {
722
+ distance: number;
723
+ signedOffset: number;
724
+ crossTrackError: number;
725
+ surface: string;
726
+ inPitLane: boolean;
727
+ pitLanePart: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
728
+ pitBoxId: string | null;
729
+ curvature: number;
730
+ heading: number;
731
+ }
732
+
641
733
  export interface CarSnapshot {
642
734
  id: string;
643
735
  rank: number | null;
@@ -648,6 +740,8 @@ export interface CarSnapshot {
648
740
  tire: TireCompound;
649
741
  interaction?: PaddockParticipantInteraction;
650
742
  lap: number;
743
+ velocityX?: number | null;
744
+ velocityY?: number | null;
651
745
  speedKph: number;
652
746
  finishRank?: number | null;
653
747
  status?: 'racing' | 'waved-flag' | 'destroyed' | string;
@@ -681,6 +775,7 @@ export interface CarSnapshot {
681
775
  stabilityState?: PaddockStabilityState;
682
776
  signedOffset?: number;
683
777
  crossTrackError?: number;
778
+ trackState?: CarTrackStateSnapshot | null;
684
779
  inPitLane?: boolean;
685
780
  wheels?: WheelSurfaceSnapshot[];
686
781
  pitLanePart?: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
@@ -789,6 +884,7 @@ export interface F1SimulatorExpertOptions {
789
884
  enabled: boolean;
790
885
  controlledDrivers: string[];
791
886
  frameSkip?: number;
887
+ episode?: PaddockEnvironmentOptions['episode'];
792
888
  visualizeSensors?: boolean | {
793
889
  rays?: boolean;
794
890
  drivers?: 'selected' | 'all' | string[];
@@ -860,7 +956,6 @@ export interface F1SimulatorOptions extends F1SimulatorCallbacks {
860
956
  seed?: number;
861
957
  trackSeed?: number;
862
958
  trackGeneration?: PaddockProceduralTrackOptions;
863
- trackQueryIndex?: boolean;
864
959
  warmup?: PaddockWarmupOptions | boolean;
865
960
  totalLaps?: number;
866
961
  physicsMode?: PaddockPhysicsMode;
@@ -887,6 +982,7 @@ export interface MountRaceCanvasOptions {
887
982
  includeTimingTower?: boolean;
888
983
  includeTelemetrySectorBanner?: boolean;
889
984
  timingTowerVerticalFit?: TimingTowerVerticalFit;
985
+ responsiveNarrowLayout?: boolean;
890
986
  }
891
987
 
892
988
  export interface MountTelemetryPanelOptions {
@@ -897,6 +993,7 @@ export interface MountRaceTelemetryDrawerOptions {
897
993
  timingTowerVerticalFit?: TimingTowerVerticalFit;
898
994
  drawerInitiallyOpen?: boolean;
899
995
  raceDataTelemetryDetail?: boolean;
996
+ responsiveNarrowLayout?: boolean;
900
997
  }
901
998
 
902
999
  export interface F1MountedSimulator {
@@ -914,6 +1011,9 @@ export interface F1MountedSimulator {
914
1011
  getPitIntent(driverId: string): PaddockPitIntent;
915
1012
  getPitTargetCompound(driverId: string): TireCompound | string | null;
916
1013
  getSimulationSpeed(): number;
1014
+ setTimingGapMode(mode: TimingGapMode): TimingGapMode;
1015
+ getTimingGapMode(): TimingGapMode;
1016
+ toggleTimingGapMode(): TimingGapMode;
917
1017
  servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
918
1018
  cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
919
1019
  getSnapshot(): RaceSnapshot | null;
@@ -951,6 +1051,9 @@ export interface PaddockSimulatorController {
951
1051
  getPitIntent(driverId: string): PaddockPitIntent;
952
1052
  getPitTargetCompound(driverId: string): TireCompound | string | null;
953
1053
  getSimulationSpeed(): number;
1054
+ setTimingGapMode(mode: TimingGapMode): TimingGapMode;
1055
+ getTimingGapMode(): TimingGapMode;
1056
+ toggleTimingGapMode(): TimingGapMode;
954
1057
  servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
955
1058
  cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
956
1059
  getSnapshot(): RaceSnapshot | null;
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import './styles.css';
2
2
  import { F1SimulatorApp } from './app/F1SimulatorApp.js';
3
3
  import { resolveF1SimulatorOptions } from './config/defaultOptions.js';
4
+ import { mergeRestartOptions } from './config/restartOptions.js';
4
5
  import { createF1SimulatorShell } from './ui/shellTemplate.js';
5
6
  export {
6
7
  createPaddockSimulator,
@@ -51,38 +52,6 @@ function assertMountRoot(root) {
51
52
  }
52
53
  }
53
54
 
54
- function mergeRestartOptions(previousOptions, nextOptions) {
55
- const resetFromPreset = Object.hasOwn(nextOptions, 'preset');
56
- const previousUi = resetFromPreset ? {} : previousOptions.ui;
57
- const previousTheme = resetFromPreset ? {} : previousOptions.theme;
58
- return {
59
- ...previousOptions,
60
- ...nextOptions,
61
- ui: {
62
- ...previousUi,
63
- ...(nextOptions.ui ?? {}),
64
- raceDataBanners: {
65
- ...(previousUi.raceDataBanners ?? {}),
66
- ...(nextOptions.ui?.raceDataBanners ?? {}),
67
- },
68
- },
69
- theme: {
70
- ...previousTheme,
71
- ...(nextOptions.theme ?? {}),
72
- },
73
- assets: {
74
- ...previousOptions.assets,
75
- ...(nextOptions.assets ?? {}),
76
- trackTextures: {
77
- ...previousOptions.assets.trackTextures,
78
- ...(nextOptions.assets?.trackTextures ?? {}),
79
- },
80
- },
81
- drivers: nextOptions.drivers ?? previousOptions.drivers,
82
- entries: nextOptions.entries ?? previousOptions.entries,
83
- };
84
- }
85
-
86
55
  export async function mountF1Simulator(root, options = {}) {
87
56
  assertMountRoot(root);
88
57
 
@@ -92,6 +61,21 @@ export async function mountF1Simulator(root, options = {}) {
92
61
  const app = new F1SimulatorApp(shell, resolvedOptions);
93
62
  await app.init();
94
63
 
64
+ const syncResolvedTimingGapMode = (timingGapMode) => {
65
+ resolvedOptions = {
66
+ ...resolvedOptions,
67
+ ui: {
68
+ ...resolvedOptions.ui,
69
+ timingGapMode,
70
+ },
71
+ };
72
+ return timingGapMode;
73
+ };
74
+ const syncResolvedRuntimeOptions = () => {
75
+ syncResolvedTimingGapMode(app.getTimingGapMode());
76
+ return resolvedOptions;
77
+ };
78
+
95
79
  return {
96
80
  get expert() {
97
81
  return app.expert ?? null;
@@ -101,7 +85,7 @@ export async function mountF1Simulator(root, options = {}) {
101
85
  root.innerHTML = '';
102
86
  },
103
87
  restart(nextOptions = {}) {
104
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(resolvedOptions, nextOptions));
88
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(syncResolvedRuntimeOptions(), nextOptions));
105
89
  app.restart(nextResolvedOptions);
106
90
  resolvedOptions = nextResolvedOptions;
107
91
  },
@@ -139,6 +123,15 @@ export async function mountF1Simulator(root, options = {}) {
139
123
  getSimulationSpeed() {
140
124
  return app.simulationSpeed;
141
125
  },
126
+ setTimingGapMode(mode) {
127
+ return syncResolvedTimingGapMode(app.setTimingGapMode(mode));
128
+ },
129
+ getTimingGapMode() {
130
+ return app.getTimingGapMode();
131
+ },
132
+ toggleTimingGapMode() {
133
+ return syncResolvedTimingGapMode(app.toggleTimingGapMode());
134
+ },
142
135
  servePenalty(penaltyId) {
143
136
  return app.servePenalty(penaltyId);
144
137
  },
@@ -37,7 +37,7 @@ export function decideDriverControls({ car, orderIndex, race }) {
37
37
  }
38
38
 
39
39
  function shouldStabilizeSimulatorCar(car, race) {
40
- if (race.physicsMode !== 'simulator') return false;
40
+ if (race.physicsMode !== 'advanced') return false;
41
41
  const gripUsage = car.gripUsage ?? 0;
42
42
  const slipAngle = Math.abs(car.slipAngleRadians ?? 0);
43
43
  const edgeDistance = race.track.width / 2 - VEHICLE_LIMITS.carWidth * 2.25;
@@ -5,7 +5,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
5
5
  import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
6
6
 
7
7
  export function calculateTrackEdgeGuard(car, race) {
8
- const simulatorMode = race.physicsMode === 'simulator';
8
+ const simulatorMode = race.physicsMode === 'advanced';
9
9
  const trackLimit = race.track.width / 2;
10
10
  const halfCarWidth = VEHICLE_LIMITS.carWidth / 2;
11
11
  const wholeCarOutsideLimit = trackLimit + halfCarWidth;
@@ -10,7 +10,7 @@ import { calculateActualOverlapPenalty, calculatePlannedTrafficPenalty, planRaci
10
10
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
11
11
 
12
12
  export function decideRacingControls(car, orderIndex, race) {
13
- if (race.physicsMode === 'simulator') {
13
+ if (race.physicsMode === 'advanced') {
14
14
  return decideSimulatorRacingControls(car, orderIndex, race);
15
15
  }
16
16
  return decideArcadeRacingControls(car, orderIndex, race);
@@ -389,7 +389,7 @@ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeG
389
389
  if (turnDirection === 0) return 0;
390
390
 
391
391
  const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
392
- const simulatorMode = race.physicsMode === 'simulator';
392
+ const simulatorMode = race.physicsMode === 'advanced';
393
393
  const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
394
394
  (simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
395
395
  const apexOffset = turnDirection * safeEdge *
@@ -8,7 +8,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
8
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
9
9
 
10
10
  export function decideRejoinControls(car, race) {
11
- if (race.physicsMode === 'simulator') {
11
+ if (race.physicsMode === 'advanced') {
12
12
  return decideSimulatorRejoinControls(car, race);
13
13
  }
14
14
  return decideArcadeRejoinControls(car, race);
@@ -103,7 +103,7 @@ export function decideSimulatorRejoinControls(car, race) {
103
103
  }
104
104
 
105
105
  function decideRejoinControlsForMode(car, race, profile) {
106
- const simulatorMode = race.physicsMode === 'simulator';
106
+ const simulatorMode = race.physicsMode === 'advanced';
107
107
  const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
108
108
  const edgeMotion = analyzeTrackEdgeMotion(car, race);
109
109
  const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
@@ -17,6 +17,7 @@ import {
17
17
  isPitServiceQueueOccupied,
18
18
  releasePitQueue,
19
19
  } from './pitService.js';
20
+ import { syncPitCarKinematics } from './pitKinematics.js';
20
21
  import {
21
22
  createPitApproachPoints,
22
23
  createRoute,
@@ -209,6 +210,10 @@ export function advancePitStopCar(sim, car, delta) {
209
210
  car.speed = 0;
210
211
  car.throttle = 0;
211
212
  car.brake = 1;
213
+ car.steeringAngle = 0;
214
+ car.yawRate = 0;
215
+ car.turnRadius = Infinity;
216
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
212
217
  applyWheelSurfaceState(car, sim.track);
213
218
  car.progress = car.trackState.distance;
214
219
  car.raceDistance = getPitBoxRaceDistance(sim, stop, {
@@ -0,0 +1,24 @@
1
+ export function syncPitCarKinematics(car, { physicsMode = 'arcade' } = {}) {
2
+ const speed = Number.isFinite(car.speed) ? car.speed : 0;
3
+ const heading = Number.isFinite(car.heading) ? car.heading : 0;
4
+ if (physicsMode === 'advanced') {
5
+ car.velocityX = Math.cos(heading) * speed;
6
+ car.velocityY = Math.sin(heading) * speed;
7
+ } else {
8
+ car.velocityX = null;
9
+ car.velocityY = null;
10
+ }
11
+ car.appliedControls = {
12
+ steering: car.steeringAngle ?? 0,
13
+ throttle: car.throttle ?? 0,
14
+ brake: car.brake ?? 0,
15
+ };
16
+ car.lateralAcceleration = 0;
17
+ car.longitudinalAcceleration = 0;
18
+ car.lateralG = 0;
19
+ car.longitudinalG = 0;
20
+ car.gripUsage = 0;
21
+ car.slipAngleRadians = 0;
22
+ car.tractionLimited = false;
23
+ car.stabilityState = 'stable';
24
+ }
@@ -1,5 +1,6 @@
1
1
  import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
2
2
  import { createRoute, routePoint } from './pitRouting.js';
3
+ import { syncPitCarKinematics } from './pitKinematics.js';
3
4
  import { PIT_QUEUE_RELEASE_SPEED } from './pitServiceConstants.js';
4
5
  import { getPitBoxRaceDistance } from './pitOccupancy.js';
5
6
 
@@ -17,6 +18,10 @@ export function beginPitQueue(sim, car, box) {
17
18
  car.speed = 0;
18
19
  car.throttle = 0;
19
20
  car.brake = 1;
21
+ car.steeringAngle = 0;
22
+ car.yawRate = 0;
23
+ car.turnRadius = Infinity;
24
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
20
25
  applyWheelSurfaceState(car, sim.track);
21
26
  car.progress = car.trackState.distance;
22
27
  car.raceDistance = getPitBoxRaceDistance(sim, stop, {
@@ -8,6 +8,7 @@ import {
8
8
  routeLimiterActiveAt,
9
9
  sampleRoute,
10
10
  } from './pitRouting.js';
11
+ import { syncPitCarKinematics } from './pitKinematics.js';
11
12
  import { completePitPenaltyService } from './pitPenaltyService.js';
12
13
  import { completePitService } from './pitTireService.js';
13
14
  import { getPitBoxRaceDistance, getPitStopBox } from './pitOccupancy.js';
@@ -51,6 +52,7 @@ export function applyPitRoutePosition(sim, car, delta) {
51
52
  VEHICLE_LIMITS.maxSteer,
52
53
  );
53
54
  car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
55
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
54
56
  stop.routeProgress = nextProgress;
55
57
  const routeAmount = route.length > 0 ? stop.routeProgress / route.length : 1;
56
58
  car.raceDistance = stop.routeStartRaceDistance +
@@ -111,6 +113,7 @@ export function applyPitRoutePosition(sim, car, delta) {
111
113
  VEHICLE_LIMITS.maxSteer,
112
114
  );
113
115
  car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
116
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
114
117
  applyWheelSurfaceState(car, sim.track);
115
118
  stop.routeProgress = nextProgress;
116
119
  const routeAmount = route.length > 0 ? stop.routeProgress / route.length : 1;
@@ -148,6 +151,10 @@ export function advancePitService(sim, car, delta) {
148
151
  car.speed = 0;
149
152
  car.throttle = 0;
150
153
  car.brake = 1;
154
+ car.steeringAngle = 0;
155
+ car.yawRate = 0;
156
+ car.turnRadius = Infinity;
157
+ syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
151
158
  applyWheelSurfaceState(car, sim.track);
152
159
  car.progress = car.trackState.distance;
153
160
  car.raceDistance = getPitBoxRaceDistance(sim, stop, box);
@@ -285,11 +285,9 @@ export function nearestDistanceOnRoute(route, position, previousDistance = 0) {
285
285
  export function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
286
286
  const currentDistance = car.raceDistance ?? entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
287
287
  const remaining = Math.max(0, entryRaceDistance - currentDistance);
288
- const currentState = nearestTrackState(track, car, car.progress, { indexMode: 'legacy' });
288
+ const currentState = nearestTrackState(track, car, car.progress);
289
289
  const startOffset = currentState.inPitLane ? 0 : currentState.signedOffset ?? 0;
290
- const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance, {
291
- indexMode: 'legacy',
292
- });
290
+ const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance);
293
291
  const targetOffset = entryConnectState.signedOffset ?? 0;
294
292
  const points = [routePoint(car, car.heading)];
295
293
  const straightDistance = remaining > metersToSimUnits(14)