@inventure71/paddockjs 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INSTALL_AND_UPDATE.md +23 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/grid_walk.md +80 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +36 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +23 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
@@ -0,0 +1,100 @@
1
+ import { simUnitsToMeters } from '../simulation/units.js';
2
+ import { isEnvironmentCarOffTrack } from './metrics.js';
3
+ import { isEnvironmentContactEvent, writeEventDriverIds } from './events.js';
4
+
5
+ export function createEvaluationTracker(initialResult, { lapScope = 'since-start' } = {}) {
6
+ if (!initialResult?.state?.snapshot) {
7
+ throw new Error('PaddockJS environment evaluation tracker requires result.state.snapshot; use result.stateOutput "minimal" or "full".');
8
+ }
9
+ const initialCars = carsById(initialResult.state.snapshot);
10
+ const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
11
+ const car = initialCars.get(driverId);
12
+ const offTrack = isEnvironmentCarOffTrack(car);
13
+ return [driverId, {
14
+ distanceMeters: 0,
15
+ lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
16
+ offTrackSteps: offTrack ? 1 : 0,
17
+ contactCount: 0,
18
+ recoverySuccess: false,
19
+ passCount: 0,
20
+ lapTimeSeconds: lapScope === 'include-history' && completedLaps(car) > 0 ? recordedLapTime(car) : null,
21
+ previousCompletedLaps: completedLaps(car),
22
+ startedOffTrack: offTrack,
23
+ startPosition: car?.rank ?? null,
24
+ bestPosition: car?.rank ?? null,
25
+ startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
26
+ }];
27
+ }));
28
+
29
+ return {
30
+ update(result) {
31
+ const currentCars = carsById(result.state.snapshot);
32
+ result.info.controlledDrivers.forEach((driverId) => {
33
+ const car = currentCars.get(driverId);
34
+ const entry = metrics[driverId];
35
+ if (!car || !entry) return;
36
+ entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
37
+ entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
38
+ const offTrack = isEnvironmentCarOffTrack(car);
39
+ if (offTrack) entry.offTrackSteps += 1;
40
+ if (entry.startedOffTrack && !offTrack) entry.recoverySuccess = true;
41
+ if (Number.isFinite(car.rank)) {
42
+ entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
43
+ entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
44
+ }
45
+ const currentCompletedLaps = completedLaps(car);
46
+ const newCompletion = currentCompletedLaps > entry.previousCompletedLaps;
47
+ if (newCompletion && entry.lapTimeSeconds == null) {
48
+ entry.lapTimeSeconds = recordedLapTime(car);
49
+ }
50
+ entry.previousCompletedLaps = currentCompletedLaps;
51
+ });
52
+ const eventDriverIds = [];
53
+ result.events.forEach((event) => {
54
+ if (!isEnvironmentContactEvent(event)) return;
55
+ writeEventDriverIds(eventDriverIds, event).forEach((driverId) => {
56
+ if (metrics[driverId]) metrics[driverId].contactCount += 1;
57
+ });
58
+ });
59
+ },
60
+ finish() {
61
+ return Object.fromEntries(Object.entries(metrics).map(([driverId, entry]) => [
62
+ driverId,
63
+ {
64
+ distanceMeters: entry.distanceMeters,
65
+ lapProgressMeters: entry.lapProgressMeters,
66
+ offTrackSteps: entry.offTrackSteps,
67
+ contactCount: entry.contactCount,
68
+ recoverySuccess: entry.recoverySuccess,
69
+ passCount: entry.passCount,
70
+ lapTimeSeconds: entry.lapTimeSeconds,
71
+ },
72
+ ]));
73
+ },
74
+ };
75
+ }
76
+
77
+ function cumulativeDistanceMetersFromSnapshotCar(car) {
78
+ const distance = Number(car?.distanceMeters ?? car?.raceDistanceMeters);
79
+ if (Number.isFinite(distance)) return distance;
80
+ return simUnitsToMeters(car?.raceDistance ?? 0);
81
+ }
82
+
83
+ function lapProgressMetersFromSnapshotCar(car) {
84
+ const progressMeters = Number(car?.lapProgressMeters ?? car?.progressMeters);
85
+ if (Number.isFinite(progressMeters)) return progressMeters;
86
+ return simUnitsToMeters(car?.progress ?? 0);
87
+ }
88
+
89
+ function carsById(snapshot) {
90
+ return new Map(snapshot.cars.map((car) => [car.id, car]));
91
+ }
92
+
93
+ function completedLaps(car) {
94
+ return car?.lapTelemetry?.completedLaps ?? 0;
95
+ }
96
+
97
+ function recordedLapTime(car) {
98
+ const duration = car?.lapTelemetry?.lastLapTime;
99
+ return Number.isFinite(duration) && duration >= 0 ? duration : null;
100
+ }
@@ -1,7 +1,30 @@
1
+ export function isEnvironmentContactEvent(event) {
2
+ return ['collision', 'contact', 'car-contact'].includes(event?.type);
3
+ }
4
+
1
5
  export function collectStepEvents(events = []) {
2
6
  return events.map((event) => normalizeEnvironmentEvent(event));
3
7
  }
4
8
 
9
+ export function writeEventDriverIds(target, event) {
10
+ target.length = 0;
11
+ pushEventDriverId(target, event.driverId);
12
+ pushEventDriverId(target, event.carId);
13
+ pushEventDriverId(target, event.otherCarId);
14
+ pushEventDriverId(target, event.primaryDriverId);
15
+ pushEventDriverId(target, event.otherDriverId);
16
+ const extraDriverIds = event.driverIds ?? [];
17
+ for (let index = 0; index < extraDriverIds.length; index += 1) {
18
+ pushEventDriverId(target, extraDriverIds[index]);
19
+ }
20
+ return target;
21
+ }
22
+
23
+ function pushEventDriverId(target, driverId) {
24
+ if (!driverId || target.includes(driverId)) return;
25
+ target.push(driverId);
26
+ }
27
+
5
28
  function normalizeEnvironmentEvent(event) {
6
29
  if (event.type === 'contact') {
7
30
  return {
@@ -1,106 +1,17 @@
1
- export type TireCompound = 'S' | 'M' | 'H';
2
- export type PaddockPhysicsMode = 'arcade' | 'advanced';
3
- export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
4
-
5
- export interface PaddockProceduralTrackOptions {
6
- profile?: PaddockProceduralTrackProfile;
7
- minLengthMeters?: number;
8
- maxLengthMeters?: number;
9
- startStraightMeters?: number;
10
- includePitLane?: boolean;
11
- length?: {
12
- minMeters?: number;
13
- maxMeters?: number;
14
- };
15
- startStraight?: {
16
- gridMeters?: number;
17
- exitMeters?: number;
18
- blendMeters?: number;
19
- lockExtraMeters?: number;
20
- };
21
- pitLane?: {
22
- enabled?: boolean;
23
- };
24
- shape?: {
25
- scale?: number;
26
- cornerDensity?: number;
27
- variation?: number;
28
- };
29
- validation?: {
30
- minClearanceMultiplier?: number;
31
- minShapeVariation?: number;
32
- minNonAdjacentArcMeters?: number;
33
- maxLocalTurnRadians?: number;
34
- maxSampleHeadingDeltaRadians?: number;
35
- };
36
- attempts?: {
37
- primary?: number;
38
- fallback?: number;
39
- };
40
- }
1
+ import type { PaddockProceduralTrackProfile, PaddockProceduralTrackOptions, PaddockTrackPoint, PaddockDrsZone, PaddockResolvedProceduralTrackOptions, PaddockProceduralTrack } from '../data/trackTypes.js';
2
+ import type { TireCompound, SimulatorDriver, ChampionshipEntryBlueprint } from '../data/entryTypes.js';
3
+ export type { PaddockProceduralTrackProfile, PaddockProceduralTrackOptions, PaddockTrackPoint, PaddockDrsZone, PaddockResolvedProceduralTrackOptions, PaddockProceduralTrack } from '../data/trackTypes.js';
4
+ export type { TireCompound, SimulatorDriver, ChampionshipEntryBlueprint } from '../data/entryTypes.js';
41
5
 
42
- export interface PaddockTrackPoint {
43
- x: number;
44
- y: number;
45
- }
6
+ import type { TeamData } from '../data/teamTypes.js';
46
7
 
47
- export interface PaddockDrsZone {
48
- id: string;
49
- startRatio: number;
50
- endRatio: number;
51
- }
52
-
53
- export interface PaddockResolvedProceduralTrackOptions {
54
- profile: PaddockProceduralTrackProfile;
55
- length: {
56
- min: number;
57
- max: number;
58
- };
59
- startStraight: {
60
- grid: number;
61
- exit: number;
62
- blend: number;
63
- lockExtra: number;
64
- };
65
- pitLane: {
66
- enabled: boolean;
67
- };
68
- shape: {
69
- scale: number;
70
- cornerDensity: number;
71
- variation: number;
72
- };
73
- validation: {
74
- minClearanceMultiplier: number;
75
- minShapeVariation: number;
76
- minNonAdjacentArcDistance: number;
77
- maxLocalTurnRadians: number;
78
- maxSampleHeadingDeltaRadians: number;
79
- };
80
- attempts: {
81
- primary: number;
82
- fallback: number;
83
- };
84
- cacheKey: string;
85
- [key: string]: unknown;
86
- }
8
+ export type {
9
+ PaddockPitCrewStats,
10
+ PaddockThemeSelector,
11
+ TeamData,
12
+ } from '../data/teamTypes.js';
87
13
 
88
- export interface PaddockProceduralTrack {
89
- name: string;
90
- width: number;
91
- kerbWidth: number;
92
- gravelWidth: number;
93
- runoffWidth: number;
94
- barrierWidth: number;
95
- sampleCount: number;
96
- seed?: number;
97
- curveInterpolation?: string;
98
- centerlineControls?: readonly PaddockTrackPoint[];
99
- drsZones?: readonly PaddockDrsZone[] | null;
100
- pitLane?: { enabled?: boolean } | null;
101
- generationOptions?: PaddockResolvedProceduralTrackOptions;
102
- [key: string]: unknown;
103
- }
14
+ export type PaddockPhysicsMode = 'arcade' | 'advanced';
104
15
 
105
16
  export type PaddockStabilityState = 'stable' | 'understeer' | 'oversteer' | 'spin-risk' | 'destroyed';
106
17
  export type PaddockPitIntent = 0 | 1 | 2;
@@ -172,45 +83,6 @@ export interface PaddockReplayGhostSnapshot {
172
83
  };
173
84
  }
174
85
 
175
- export interface TeamData {
176
- id?: string;
177
- name?: string;
178
- color?: string;
179
- icon?: string;
180
- pitCrew?: PaddockPitCrewStats;
181
- pitCrewStats?: PaddockPitCrewStats;
182
- }
183
-
184
- export interface PaddockPitCrewStats {
185
- speed?: number;
186
- consistency?: number;
187
- reliability?: number;
188
- }
189
-
190
- export interface SimulatorDriver {
191
- id: string;
192
- name: string;
193
- color: string;
194
- link?: string;
195
- icon?: string;
196
- code?: string;
197
- timingName?: string;
198
- tire?: TireCompound;
199
- raceData?: string[];
200
- customFields?: Array<{ label: string; value: string }> | Record<string, string>;
201
- team?: TeamData | null;
202
- driverNumber?: number;
203
- }
204
-
205
- export interface ChampionshipEntryBlueprint {
206
- driverId: string;
207
- driverNumber?: number;
208
- timingName?: string;
209
- driver?: unknown;
210
- vehicle?: unknown;
211
- team?: TeamData;
212
- }
213
-
214
86
  export interface RaceEvent {
215
87
  type: string;
216
88
  at?: number;
@@ -731,6 +603,8 @@ export type PaddockPolicyLike =
731
603
  | { predict(observation: PaddockDriverObservation, context: Record<string, unknown>): PaddockAction };
732
604
 
733
605
  export interface PaddockDriverControllerContext {
606
+ /** Generation cancellation signal; Paddock-owned loops always supply it. */
607
+ signal?: AbortSignal;
734
608
  runtime: PaddockControllerRuntime;
735
609
  mode: string;
736
610
  actionRepeat: number;
@@ -932,7 +806,13 @@ export function createRolloutTransition(
932
806
  ): PaddockRolloutTransition;
933
807
  export const ENVIRONMENT_SCENARIO_PRESETS: readonly PaddockScenarioPreset[];
934
808
  export const DEFAULT_EVALUATION_CASES: readonly PaddockEvaluationCase[];
935
- export function createEvaluationTracker(initialResult: PaddockEnvironmentSnapshotResult): {
809
+ export type PaddockEvaluationLapScope = 'since-start' | 'include-history';
810
+
811
+ export interface PaddockEvaluationTrackerOptions {
812
+ lapScope?: PaddockEvaluationLapScope;
813
+ }
814
+
815
+ export function createEvaluationTracker(initialResult: PaddockEnvironmentSnapshotResult, options?: PaddockEvaluationTrackerOptions): {
936
816
  update(result: PaddockEnvironmentSnapshotResult): void;
937
817
  finish(): Record<string, PaddockEvaluationDriverMetrics>;
938
818
  };
@@ -940,6 +820,7 @@ export function runEnvironmentEvaluation(options: {
940
820
  baseOptions: PaddockEnvironmentOptions;
941
821
  policy: PaddockPolicyLike;
942
822
  cases?: PaddockEvaluationCase[];
823
+ lapScope?: PaddockEvaluationLapScope;
943
824
  createEnvironment?: (options: PaddockEnvironmentOptions) => PaddockEnvironment;
944
825
  }): PaddockEvaluationReport;
945
826
  export function createEnvironmentWorkerProtocol(env: PaddockEnvironment): PaddockEnvironmentWorkerProtocol;
@@ -1,3 +1,5 @@
1
+ import { isEnvironmentContactEvent, writeEventDriverIds } from './events.js';
2
+ export { isEnvironmentContactEvent } from './events.js';
1
3
  import { normalizeAngle } from '../simulation/simMath.js';
2
4
  import { pointAt } from '../simulation/track/trackModel.js';
3
5
  import { simUnitsToMeters } from '../simulation/units.js';
@@ -26,10 +28,6 @@ export function isEnvironmentCarOffTrack(car) {
26
28
  return isOffTrack(car, classifyWheels(car));
27
29
  }
28
30
 
29
- export function isEnvironmentContactEvent(event) {
30
- return ['collision', 'contact', 'car-contact'].includes(event?.type);
31
- }
32
-
33
31
  export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [], scratch = null }) {
34
32
  const previousById = carsByIdForMetrics(previousSnapshot, scratch, 'previousCarsById');
35
33
  const currentById = carsByIdForMetrics(snapshot, scratch, 'currentCarsById');
@@ -179,14 +177,7 @@ function contactCountsByDriver(events, scratch = null) {
179
177
  counts.clear();
180
178
  events.forEach((event) => {
181
179
  if (!isEnvironmentContactEvent(event)) return;
182
- eventDriverIds.length = 0;
183
- pushEventDriverId(eventDriverIds, event.driverId);
184
- pushEventDriverId(eventDriverIds, event.carId);
185
- pushEventDriverId(eventDriverIds, event.otherCarId);
186
- const extraDriverIds = event.driverIds ?? [];
187
- for (let index = 0; index < extraDriverIds.length; index += 1) {
188
- pushEventDriverId(eventDriverIds, extraDriverIds[index]);
189
- }
180
+ writeEventDriverIds(eventDriverIds, event);
190
181
  for (let index = 0; index < eventDriverIds.length; index += 1) {
191
182
  const driverId = eventDriverIds[index];
192
183
  counts.set(driverId, (counts.get(driverId) ?? 0) + 1);
@@ -195,8 +186,3 @@ function contactCountsByDriver(events, scratch = null) {
195
186
  eventDriverIds.length = 0;
196
187
  return counts;
197
188
  }
198
-
199
- function pushEventDriverId(target, driverId) {
200
- if (!driverId || target.includes(driverId)) return;
201
- target.push(driverId);
202
- }
@@ -0,0 +1,104 @@
1
+ export const MODEL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
2
+
3
+ export function buildObservationVectorSchema({
4
+ profile = 'default',
5
+ contactPatchCount = 4,
6
+ lookaheadCount = 0,
7
+ rayCount = 0,
8
+ rayChannels = [],
9
+ nearbyCount = 0,
10
+ }) {
11
+ const includePhysicalDriverSenses = profile === 'physical-driver';
12
+ const schema = [
13
+ { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
14
+ { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
15
+ { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
16
+ { name: 'self.throttle', scale: '0..1' },
17
+ { name: 'self.brake', scale: '0..1' },
18
+ { name: 'self.lateralG', scale: 'fixed:8g' },
19
+ { name: 'self.longitudinalG', scale: 'fixed:6g' },
20
+ { name: 'self.gripUsage', scale: '0..2' },
21
+ { name: 'self.slipAngleRadians', unit: 'rad', scale: 'fixed:pi' },
22
+ { name: 'self.tractionLimited', scale: 'boolean' },
23
+ { name: 'self.lapProgressRatio', scale: '0..1' },
24
+ { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
25
+ { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
26
+ { name: 'self.onTrack', scale: 'boolean' },
27
+ { name: 'self.inPitLane', scale: 'boolean' },
28
+ { name: 'self.tireEnergy', scale: '0..100' },
29
+ { name: 'self.pitIntent', scale: '0..2' },
30
+ { name: 'self.pitStopActive', scale: 'boolean' },
31
+ { name: 'race.positionNormalized', scale: '0..1' },
32
+ { name: 'race.raceModeGreen', scale: 'boolean' },
33
+ { name: 'race.raceModeSafetyCar', scale: 'boolean' },
34
+ { name: 'race.redFlag', scale: 'boolean' },
35
+ { name: 'race.pitLaneOpen', scale: 'boolean' },
36
+ { name: 'track.curvature', scale: 'track-curvature' },
37
+ ];
38
+ if (includePhysicalDriverSenses) {
39
+ schema.push(
40
+ { name: 'self.yawRateRadiansPerSecond', unit: 'rad/s', scale: 'fixed:pi' },
41
+ { name: 'trackRelation.leftBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
42
+ { name: 'trackRelation.rightBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
43
+ { name: 'trackRelation.legalWidthMeters', unit: 'm', scale: 'fixed:meters' },
44
+ );
45
+ for (let index = 0; index < contactPatchCount; index += 1) {
46
+ schema.push(
47
+ { name: `contactPatches[${index}].present`, scale: 'boolean' },
48
+ { name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
49
+ { name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
50
+ { name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
51
+ { name: `contactPatches[${index}].crossTrackErrorMeters`, unit: 'm', scale: 'fixed:meters' },
52
+ );
53
+ }
54
+ }
55
+ for (let index = 0; index < lookaheadCount; index += 1) {
56
+ schema.push(
57
+ { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
58
+ { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
59
+ );
60
+ }
61
+ for (let index = 0; index < rayCount; index += 1) {
62
+ schema.push(
63
+ { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
64
+ { name: `rays[${index}].track.hit`, scale: 'boolean' },
65
+ { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
66
+ { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
67
+ { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
68
+ { name: `rays[${index}].car.hit`, scale: 'boolean' },
69
+ { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
70
+ { name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
71
+ );
72
+ MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
73
+ if (rayChannels?.includes?.(channel)) {
74
+ schema.push(
75
+ { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
76
+ { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
77
+ );
78
+ }
79
+ });
80
+ }
81
+ for (let index = 0; index < nearbyCount; index += 1) {
82
+ schema.push(
83
+ { name: `nearbyCars[${index}].present`, scale: 'boolean' },
84
+ { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
85
+ { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
86
+ { name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
87
+ { name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
88
+ { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
89
+ { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
90
+ { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
91
+ { name: `nearbyCars[${index}].entityTypeReplayGhost`, scale: 'boolean' },
92
+ );
93
+ if (includePhysicalDriverSenses) {
94
+ schema.push(
95
+ { name: `nearbyCars[${index}].behind`, scale: 'boolean' },
96
+ { name: `nearbyCars[${index}].closingRateMetersPerSecond`, unit: 'm/s', scale: 'fixed:100' },
97
+ { name: `nearbyCars[${index}].timeToContactSeconds`, unit: 's', scale: 'fixed:10' },
98
+ { name: `nearbyCars[${index}].leftOverlap`, scale: 'boolean' },
99
+ { name: `nearbyCars[${index}].rightOverlap`, scale: 'boolean' },
100
+ );
101
+ }
102
+ }
103
+ return schema;
104
+ }
@@ -1,33 +1,8 @@
1
- const MODEL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
1
+ import { appendRayVectorValues } from './sensors/rayOutputs.js';
2
+ import { buildObservationVectorSchema, MODEL_RAY_SURFACE_CHANNELS } from './observationSchema.js';
2
3
 
3
4
  export function buildObservationVector(source, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
4
5
  const includePhysicalDriverSenses = source.profile === 'physical-driver';
5
- const schema = includeSchema ? [
6
- { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
7
- { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
8
- { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
9
- { name: 'self.throttle', scale: '0..1' },
10
- { name: 'self.brake', scale: '0..1' },
11
- { name: 'self.lateralG', scale: 'fixed:8g' },
12
- { name: 'self.longitudinalG', scale: 'fixed:6g' },
13
- { name: 'self.gripUsage', scale: '0..2' },
14
- { name: 'self.slipAngleRadians', unit: 'rad', scale: 'fixed:pi' },
15
- { name: 'self.tractionLimited', scale: 'boolean' },
16
- { name: 'self.lapProgressRatio', scale: '0..1' },
17
- { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
18
- { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
19
- { name: 'self.onTrack', scale: 'boolean' },
20
- { name: 'self.inPitLane', scale: 'boolean' },
21
- { name: 'self.tireEnergy', scale: '0..100' },
22
- { name: 'self.pitIntent', scale: '0..2' },
23
- { name: 'self.pitStopActive', scale: 'boolean' },
24
- { name: 'race.positionNormalized', scale: '0..1' },
25
- { name: 'race.raceModeGreen', scale: 'boolean' },
26
- { name: 'race.raceModeSafetyCar', scale: 'boolean' },
27
- { name: 'race.redFlag', scale: 'boolean' },
28
- { name: 'race.pitLaneOpen', scale: 'boolean' },
29
- { name: 'track.curvature', scale: 'track-curvature' },
30
- ] : null;
31
6
  const vector = [
32
7
  source.self.speedKph / 400,
33
8
  source.self.speedMetersPerSecond / 120,
@@ -55,26 +30,13 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
55
30
  source.track.curvature ?? 0,
56
31
  ];
57
32
  if (includePhysicalDriverSenses) {
58
- pushSchema(schema,
59
- { name: 'self.yawRateRadiansPerSecond', unit: 'rad/s', scale: 'fixed:pi' },
60
- { name: 'trackRelation.leftBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
61
- { name: 'trackRelation.rightBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
62
- { name: 'trackRelation.legalWidthMeters', unit: 'm', scale: 'fixed:meters' },
63
- );
64
33
  vector.push(
65
34
  source.self.yawRateRadiansPerSecond / Math.PI,
66
35
  source.trackRelation.leftBoundaryMeters,
67
36
  source.trackRelation.rightBoundaryMeters,
68
37
  source.trackRelation.legalWidthMeters,
69
38
  );
70
- source.contactPatches.forEach((patch, index) => {
71
- pushSchema(schema,
72
- { name: `contactPatches[${index}].present`, scale: 'boolean' },
73
- { name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
74
- { name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
75
- { name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
76
- { name: `contactPatches[${index}].crossTrackErrorMeters`, unit: 'm', scale: 'fixed:meters' },
77
- );
39
+ source.contactPatches.forEach((patch) => {
78
40
  vector.push(
79
41
  patch.present ? 1 : 0,
80
42
  patch.surfaceCode / 5,
@@ -84,58 +46,21 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
84
46
  );
85
47
  });
86
48
  }
87
- source.track.lookahead.forEach((sample, index) => {
88
- pushSchema(schema,
89
- { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
90
- { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
91
- );
49
+ source.track.lookahead.forEach((sample) => {
92
50
  vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
93
51
  });
94
52
  if (typeof source.appendRayVectorValues === 'function') {
95
53
  source.appendRayVectorValues(vector);
96
- } else if (Array.isArray(source.rayVectorValues)) {
97
- source.rayVectorValues.forEach((values, index) => {
98
- pushRaySchema(schema, index, sensors);
99
- vector.push(...values);
100
- });
101
54
  } else {
102
- source.rays.forEach((ray, index) => {
103
- pushRaySchema(schema, index, sensors);
104
- vector.push(
105
- ratio(ray.track.distanceMeters, ray.lengthMeters),
106
- ray.track.hit ? 1 : 0,
107
- ray.track.kind === 'exit' ? 1 : 0,
108
- ray.track.kind === 'entry' ? 1 : 0,
109
- ratio(ray.car.distanceMeters, ray.lengthMeters),
110
- ray.car.hit ? 1 : 0,
111
- ray.car.relativeSpeedKph / 200,
112
- ray.car.targetType === 'replayGhost' ? 1 : 0,
113
- );
114
- MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
115
- if (sensors.rays.channels?.includes?.(channel)) {
116
- vector.push(
117
- ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
118
- ray[channel]?.hit ? 1 : 0,
119
- );
120
- }
121
- });
55
+ const surfaceChannels = MODEL_RAY_SURFACE_CHANNELS.filter((channel) => sensors.rays.channels?.includes?.(channel));
56
+ source.rays.forEach((ray) => {
57
+ appendRayVectorValues(vector, ray.lengthMeters, ray.track, ray.car, ray, surfaceChannels);
122
58
  });
123
59
  }
124
60
  const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
125
61
  const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
126
62
  for (let index = 0; index < nearbyLimit; index += 1) {
127
63
  const nearby = source.nearbyCars[index] ?? null;
128
- pushSchema(schema,
129
- { name: `nearbyCars[${index}].present`, scale: 'boolean' },
130
- { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
131
- { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
132
- { name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
133
- { name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
134
- { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
135
- { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
136
- { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
137
- { name: `nearbyCars[${index}].entityTypeReplayGhost`, scale: 'boolean' },
138
- );
139
64
  vector.push(
140
65
  nearby ? 1 : 0,
141
66
  clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
@@ -148,13 +73,6 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
148
73
  nearby?.entityType === 'replayGhost' ? 1 : 0,
149
74
  );
150
75
  if (includePhysicalDriverSenses) {
151
- pushSchema(schema,
152
- { name: `nearbyCars[${index}].behind`, scale: 'boolean' },
153
- { name: `nearbyCars[${index}].closingRateMetersPerSecond`, unit: 'm/s', scale: 'fixed:100' },
154
- { name: `nearbyCars[${index}].timeToContactSeconds`, unit: 's', scale: 'fixed:10' },
155
- { name: `nearbyCars[${index}].leftOverlap`, scale: 'boolean' },
156
- { name: `nearbyCars[${index}].rightOverlap`, scale: 'boolean' },
157
- );
158
76
  vector.push(
159
77
  nearby?.behind ? 1 : 0,
160
78
  (nearby?.closingRateMetersPerSecond ?? 0) / 100,
@@ -164,38 +82,21 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
164
82
  );
165
83
  }
166
84
  }
167
- return { vector: finalizeVector(vector, vectorType), schema: schema ?? [] };
85
+ const schema = includeSchema ? buildObservationVectorSchema({
86
+ profile: source.profile,
87
+ contactPatchCount: source.contactPatches?.length ?? 0,
88
+ lookaheadCount: source.track.lookahead.length,
89
+ rayCount: typeof source.appendRayVectorValues === 'function' ? 0 : source.rays.length,
90
+ rayChannels: sensors.rays.channels,
91
+ nearbyCount: nearbyLimit,
92
+ }) : [];
93
+ return { vector: finalizeVector(vector, vectorType), schema };
168
94
  }
169
95
 
170
96
  function finalizeVector(vector, vectorType = 'array') {
171
97
  return vectorType === 'float32' ? Float32Array.from(vector) : vector;
172
98
  }
173
99
 
174
- function pushSchema(schema, ...entries) {
175
- if (schema) schema.push(...entries);
176
- }
177
-
178
- function pushRaySchema(schema, index, sensors) {
179
- pushSchema(schema,
180
- { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
181
- { name: `rays[${index}].track.hit`, scale: 'boolean' },
182
- { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
183
- { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
184
- { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
185
- { name: `rays[${index}].car.hit`, scale: 'boolean' },
186
- { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
187
- { name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
188
- );
189
- MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
190
- if (sensors.rays.channels?.includes?.(channel)) {
191
- pushSchema(schema,
192
- { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
193
- { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
194
- );
195
- }
196
- });
197
- }
198
-
199
100
  function normalizeLapProgress(source) {
200
101
  return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
201
102
  }