@inventure71/paddockjs 3.0.0 → 4.1.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 (97) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/INSTALL_AND_UPDATE.md +21 -4
  3. package/README.md +103 -551
  4. package/docs/architecture.md +28 -18
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +79 -0
  7. package/docs/custom_model_controller.md +4 -4
  8. package/docs/data-helpers.md +50 -0
  9. package/docs/data_contract.md +13 -7
  10. package/docs/getting-started.md +105 -0
  11. package/docs/headless-environment.md +68 -0
  12. package/docs/index.md +17 -4
  13. package/docs/rules.md +6 -2
  14. package/docs/system_specs.md +60 -7
  15. package/docs/theming.md +64 -0
  16. package/docs/training.md +20 -14
  17. package/docs/troubleshooting.md +76 -0
  18. package/docs/upgrading-hosts-to-3.md +42 -0
  19. package/examples/python/README.md +6 -0
  20. package/examples/python/base_policy_server.py +19 -0
  21. package/package.json +21 -5
  22. package/src/api/PaddockSimulatorController.js +78 -22
  23. package/src/api/runtimeTheme.js +59 -0
  24. package/src/app/F1SimulatorApp.js +24 -3
  25. package/src/app/readouts/timingTowerRenderer.js +203 -42
  26. package/src/app/runtime/frameLoop.js +5 -1
  27. package/src/config/defaultOptions.js +8 -1
  28. package/src/config/themeOptions.js +5 -0
  29. package/src/data/index.d.ts +212 -0
  30. package/src/data/index.js +14 -0
  31. package/src/environment/episode.js +11 -1
  32. package/src/environment/metrics.js +59 -16
  33. package/src/environment/observationVector.js +50 -33
  34. package/src/environment/observations.js +89 -20
  35. package/src/environment/runtime.js +41 -0
  36. package/src/environment/sensors/carRays.js +48 -37
  37. package/src/environment/sensors/index.js +501 -97
  38. package/src/environment/sensors/indexedRayBands.js +134 -46
  39. package/src/environment/sensors/rayBandTrace.js +1171 -0
  40. package/src/environment/sensors/rayChannels.js +108 -0
  41. package/src/environment/sensors/rayConfig.js +71 -25
  42. package/src/environment/sensors/rayGeometry.js +23 -17
  43. package/src/environment/sensors/sensorTargets.js +67 -1
  44. package/src/environment/sensors/surfaceRays.js +130 -89
  45. package/src/environment/sensors/trackRays.js +115 -1
  46. package/src/index.d.ts +43 -0
  47. package/src/index.js +27 -2
  48. package/src/placeholder/index.d.ts +11 -0
  49. package/src/placeholder/index.js +61 -0
  50. package/src/placeholder.css +72 -0
  51. package/src/simulation/collisionGeometry.js +260 -96
  52. package/src/simulation/pit/pitFlow.js +21 -9
  53. package/src/simulation/pit/pitQueue.js +5 -3
  54. package/src/simulation/pit/pitRouteMovement.js +9 -4
  55. package/src/simulation/pit/pitRouting.js +77 -42
  56. package/src/simulation/pit/pitState.js +1 -0
  57. package/src/simulation/pit/pitTireService.js +7 -3
  58. package/src/simulation/race/raceFinish.js +25 -8
  59. package/src/simulation/race/raceOrder.js +189 -32
  60. package/src/simulation/race/raceProgress.js +129 -37
  61. package/src/simulation/race/raceSetup.js +2 -0
  62. package/src/simulation/race/raceStep.js +25 -13
  63. package/src/simulation/raceSimulation.js +20 -17
  64. package/src/simulation/rules/collisionSteward.js +40 -23
  65. package/src/simulation/rules/rulesReview.js +77 -56
  66. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  67. package/src/simulation/timing/drsTiming.js +143 -30
  68. package/src/simulation/timing/gapEstimation.js +25 -3
  69. package/src/simulation/timing/lapTelemetry.js +115 -36
  70. package/src/simulation/timing/raceTiming.js +5 -1
  71. package/src/simulation/timing/sectorPerformance.js +104 -26
  72. package/src/simulation/timing/timingHistory.js +229 -14
  73. package/src/simulation/timing/timingLines.js +65 -7
  74. package/src/simulation/track/pitLaneLayout.js +23 -0
  75. package/src/simulation/track/pitLaneState.js +218 -19
  76. package/src/simulation/track/spatialQueries.js +67 -11
  77. package/src/simulation/track/trackMath.js +91 -24
  78. package/src/simulation/track/trackModel.js +2 -0
  79. package/src/simulation/track/trackQueryIndex.js +1334 -100
  80. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  81. package/src/simulation/track/trackQueryScratch.js +7 -0
  82. package/src/simulation/track/trackQueryStats.js +25 -0
  83. package/src/simulation/track/trackSnapshotJson.js +139 -0
  84. package/src/simulation/track/trackStatePolicy.js +249 -1
  85. package/src/simulation/vehicle/contactResolution.js +96 -59
  86. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  87. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  88. package/src/simulation/vehicle/runoffResponse.js +54 -29
  89. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  90. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  91. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  92. package/src/simulation/vehicle/vehicleState.js +6 -0
  93. package/src/simulation/vehicle/wheelSurface.js +807 -92
  94. package/src/styles.css +497 -497
  95. package/src/ui/raceControlsTemplate.js +1 -0
  96. package/src/ui/telemetryDrawerTemplate.js +1 -0
  97. package/src/ui/telemetryTemplates.js +1 -0
@@ -91,40 +91,36 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
91
91
  );
92
92
  vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
93
93
  });
94
- source.rays.forEach((ray, index) => {
95
- pushSchema(schema,
96
- { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
97
- { name: `rays[${index}].track.hit`, scale: 'boolean' },
98
- { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
99
- { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
100
- { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
101
- { name: `rays[${index}].car.hit`, scale: 'boolean' },
102
- { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
103
- { name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
104
- );
105
- vector.push(
106
- ratio(ray.track.distanceMeters, ray.lengthMeters),
107
- ray.track.hit ? 1 : 0,
108
- ray.track.kind === 'exit' ? 1 : 0,
109
- ray.track.kind === 'entry' ? 1 : 0,
110
- ratio(ray.car.distanceMeters, ray.lengthMeters),
111
- ray.car.hit ? 1 : 0,
112
- ray.car.relativeSpeedKph / 200,
113
- ray.car.targetType === 'replayGhost' ? 1 : 0,
114
- );
115
- MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
116
- if (sensors.rays.channels?.includes?.(channel)) {
117
- pushSchema(schema,
118
- { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
119
- { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
120
- );
121
- vector.push(
122
- ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
123
- ray[channel]?.hit ? 1 : 0,
124
- );
125
- }
94
+ if (typeof source.appendRayVectorValues === 'function') {
95
+ 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);
126
100
  });
127
- });
101
+ } 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
+ });
122
+ });
123
+ }
128
124
  const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
129
125
  const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
130
126
  for (let index = 0; index < nearbyLimit; index += 1) {
@@ -179,6 +175,27 @@ function pushSchema(schema, ...entries) {
179
175
  if (schema) schema.push(...entries);
180
176
  }
181
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
+
182
199
  function normalizeLapProgress(source) {
183
200
  return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
184
201
  }
@@ -6,18 +6,37 @@ import { buildBodySenses } from './sensors/bodySenses.js';
6
6
  import { buildBoundarySenses } from './sensors/boundarySenses.js';
7
7
  import { buildContactPatchSenses } from './sensors/contactSenses.js';
8
8
  import { enrichOpponentRadar } from './sensors/opponentRadar.js';
9
- import { buildNearbyCars, buildRaySensors, createRayBatchContext, normalizeRayOptions } from './sensors.js';
9
+ import {
10
+ buildNearbyCars,
11
+ appendRaySensorVectorValues,
12
+ buildRaySensors,
13
+ createRayBatchContext,
14
+ normalizeRayOptions,
15
+ } from './sensors.js';
10
16
  import { buildObservationVector } from './observationVector.js';
11
17
 
12
18
  const EMPTY_EVENTS = Object.freeze([]);
13
19
 
14
- export function buildEnvironmentObservation({ snapshot, options, events = [], controlledDrivers = options.controlledDrivers }) {
15
- const carsById = new Map(snapshot.cars.map((entry) => [entry.id, entry]));
16
- const eventsByDriver = groupEventsByDriver(events, controlledDrivers);
20
+ export function buildEnvironmentObservation({
21
+ snapshot,
22
+ options,
23
+ events = [],
24
+ controlledDrivers = options.controlledDrivers,
25
+ scratch = null,
26
+ }) {
27
+ const carsById = carsByIdForObservation(snapshot, scratch);
28
+ const eventsByDriver = groupEventsByDriver(events, controlledDrivers, scratch);
17
29
  const defaultSensors = defaultSensorOptions(options);
18
30
  const hasSensorOverrides = Object.keys(options.sensorsByDriver ?? {}).length > 0;
19
31
  let rayBatchContext = null;
20
- const getRayBatchContext = () => {
32
+ let rayScratchBatchContext = null;
33
+ const getRayBatchContext = ({ scratch: useScratch = false } = {}) => {
34
+ if (useScratch) {
35
+ rayScratchBatchContext ??= createRayBatchContext(snapshot, {
36
+ scratch: rayBatchContextScratchForObservation(scratch),
37
+ });
38
+ return rayScratchBatchContext;
39
+ }
21
40
  rayBatchContext ??= createRayBatchContext(snapshot);
22
41
  return rayBatchContext;
23
42
  };
@@ -66,24 +85,65 @@ function formatObservation(observation, options = {}) {
66
85
  return formatted;
67
86
  }
68
87
 
69
- function groupEventsByDriver(events, controlledDrivers) {
88
+ function carsByIdForObservation(snapshot, scratch) {
89
+ if (!scratch) return new Map(snapshot.cars.map((entry) => [entry.id, entry]));
90
+ const carsById = scratch.carsById ?? new Map();
91
+ scratch.carsById = carsById;
92
+ carsById.clear();
93
+ for (let index = 0; index < snapshot.cars.length; index += 1) {
94
+ const entry = snapshot.cars[index];
95
+ carsById.set(entry.id, entry);
96
+ }
97
+ return carsById;
98
+ }
99
+
100
+ function groupEventsByDriver(events, controlledDrivers, scratch = null) {
70
101
  if (!events.length) return null;
71
- const byDriver = new Map(controlledDrivers.map((driverId) => [driverId, []]));
72
- const controlledSet = new Set(controlledDrivers);
102
+ const byDriver = scratch ? (scratch.eventsByDriver ?? new Map()) : new Map();
103
+ const controlledSet = scratch ? (scratch.controlledEventDrivers ?? new Set()) : new Set();
104
+ const eventDriverIds = scratch ? (scratch.eventDriverIds ?? []) : [];
105
+ if (scratch) {
106
+ scratch.eventsByDriver = byDriver;
107
+ scratch.controlledEventDrivers = controlledSet;
108
+ scratch.eventDriverIds = eventDriverIds;
109
+ }
110
+ byDriver.clear();
111
+ controlledSet.clear();
112
+ for (let index = 0; index < controlledDrivers.length; index += 1) {
113
+ const driverId = controlledDrivers[index];
114
+ byDriver.set(driverId, []);
115
+ controlledSet.add(driverId);
116
+ }
73
117
  events.forEach((event) => {
74
- const driverIds = new Set([
75
- event.driverId,
76
- event.carId,
77
- event.otherCarId,
78
- ...(event.driverIds ?? []),
79
- ].filter(Boolean));
80
- driverIds.forEach((driverId) => {
118
+ eventDriverIds.length = 0;
119
+ pushEventDriverId(eventDriverIds, event.driverId);
120
+ pushEventDriverId(eventDriverIds, event.carId);
121
+ pushEventDriverId(eventDriverIds, event.otherCarId);
122
+ const extraDriverIds = event.driverIds ?? [];
123
+ for (let index = 0; index < extraDriverIds.length; index += 1) {
124
+ pushEventDriverId(eventDriverIds, extraDriverIds[index]);
125
+ }
126
+ for (let index = 0; index < eventDriverIds.length; index += 1) {
127
+ const driverId = eventDriverIds[index];
81
128
  if (controlledSet.has(driverId)) byDriver.get(driverId)?.push(event);
82
- });
129
+ }
83
130
  });
131
+ eventDriverIds.length = 0;
84
132
  return byDriver;
85
133
  }
86
134
 
135
+ function pushEventDriverId(target, driverId) {
136
+ if (!driverId || target.includes(driverId)) return;
137
+ target.push(driverId);
138
+ }
139
+
140
+ function rayBatchContextScratchForObservation(scratch) {
141
+ if (!scratch) return true;
142
+ const rayScratch = scratch.rayBatchContextScratch ?? {};
143
+ scratch.rayBatchContextScratch = rayScratch;
144
+ return rayScratch;
145
+ }
146
+
87
147
  function buildDriverObservationObject(car, snapshot, options, events, sensors, getRayBatchContext = null) {
88
148
  const sensorCar = withEnvironmentControlFlag(car, options);
89
149
  const onTrack = isCarLegallyOnTrack(car);
@@ -186,7 +246,15 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
186
246
  headingErrorRadians: trackHeadingError,
187
247
  };
188
248
  const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
189
- const rays = sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [];
249
+ const appendRayVectorValues = sensors.rays.enabled
250
+ ? (vector) => appendRaySensorVectorValues(
251
+ vector,
252
+ sensorCar,
253
+ snapshot,
254
+ sensors.rays,
255
+ rayBatchContextForSensors(sensors, getRayBatchContext, { scratch: true }),
256
+ )
257
+ : null;
190
258
  const nearbyCars = sensors.nearbyCars.enabled
191
259
  ? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
192
260
  : [];
@@ -219,7 +287,8 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
219
287
  curvature: trackCurvature,
220
288
  lookahead: buildTrackLookahead(car, snapshot, options),
221
289
  },
222
- rays,
290
+ rays: [],
291
+ appendRayVectorValues,
223
292
  nearbyCars,
224
293
  };
225
294
  return buildObservationVector(source, sensors, {
@@ -228,9 +297,9 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
228
297
  });
229
298
  }
230
299
 
231
- function rayBatchContextForSensors(sensors, getRayBatchContext) {
300
+ function rayBatchContextForSensors(sensors, getRayBatchContext, { scratch = false } = {}) {
232
301
  if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
233
- return getRayBatchContext();
302
+ return getRayBatchContext({ scratch });
234
303
  }
235
304
 
236
305
  export function defaultSensorOptions(options) {
@@ -195,6 +195,9 @@ export function createEnvironmentRuntime(host) {
195
195
  episodeState.lastObservationSnapshot = null;
196
196
  episodeState.lastRewardSnapshot = null;
197
197
  episodeState.drivers?.clear?.();
198
+ episodeState.destroyedCarsById?.clear?.();
199
+ clearObservationScratch(episodeState.observationScratch);
200
+ clearMetricScratch(episodeState.metricScratch);
198
201
  }
199
202
 
200
203
  return { reset, step, resetDrivers, getObservation, getState, getActionSpec, getObservationSpec, destroy };
@@ -292,6 +295,7 @@ function buildResult({
292
295
  options,
293
296
  events,
294
297
  controlledDrivers: resultDrivers,
298
+ scratch: episodeState.observationScratch,
295
299
  });
296
300
  const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
297
301
  const metrics = buildDriverMetrics({
@@ -299,6 +303,7 @@ function buildResult({
299
303
  previousSnapshot: episodeState.previousSnapshot,
300
304
  options: { ...options, controlledDrivers: resultDrivers },
301
305
  events,
306
+ scratch: episodeState.metricScratch,
302
307
  });
303
308
  const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
304
309
  ...options,
@@ -345,6 +350,42 @@ function buildResult({
345
350
  };
346
351
  }
347
352
 
353
+ function clearObservationScratch(scratch) {
354
+ if (!scratch) return;
355
+ scratch.carsById?.clear?.();
356
+ scratch.eventsByDriver?.clear?.();
357
+ scratch.controlledEventDrivers?.clear?.();
358
+ clearRayBatchContextScratch(scratch.rayBatchContextScratch);
359
+ if (Array.isArray(scratch.eventDriverIds)) scratch.eventDriverIds.length = 0;
360
+ }
361
+
362
+ function clearRayBatchContextScratch(scratch) {
363
+ if (!scratch) return;
364
+ const arrayKeys = [
365
+ 'rayTargets',
366
+ 'rayTargetPool',
367
+ 'carTargets',
368
+ 'rayVectors',
369
+ 'sharedRayQueries',
370
+ 'rays',
371
+ 'rayPool',
372
+ 'rayVectorValues',
373
+ 'rayVectorValuePool',
374
+ ];
375
+ for (let index = 0; index < arrayKeys.length; index += 1) {
376
+ const value = scratch[arrayKeys[index]];
377
+ if (Array.isArray(value)) value.length = 0;
378
+ }
379
+ }
380
+
381
+ function clearMetricScratch(scratch) {
382
+ if (!scratch) return;
383
+ scratch.previousCarsById?.clear?.();
384
+ scratch.currentCarsById?.clear?.();
385
+ scratch.contactCounts?.clear?.();
386
+ if (Array.isArray(scratch.eventDriverIds)) scratch.eventDriverIds.length = 0;
387
+ }
388
+
348
389
  function applyControlledRunoffResponse(sim, options) {
349
390
  options?.controlledDrivers?.forEach?.((driverId) => {
350
391
  const car = sim?.cars?.find?.((item) => item.id === driverId);
@@ -1,6 +1,6 @@
1
1
  import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
2
2
  import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
3
- import { dot, getCarRayOrigin, getCarRayVector, intersectAxisAlignedBoxRay } from './rayGeometry.js';
3
+ import { getCarRayOrigin, getCarRayVector, intersectAxisAlignedBoxRayScalars } from './rayGeometry.js';
4
4
  import { rayDetectableTargets } from './sensorTargets.js';
5
5
 
6
6
  export function estimateCarHit(
@@ -10,41 +10,53 @@ export function estimateCarHit(
10
10
  lengthMeters,
11
11
  origin = getCarRayOrigin(car),
12
12
  targets = rayDetectableTargets(car, snapshot),
13
+ options = {},
13
14
  ) {
14
- const ray = getCarRayVector(car, angleDegrees);
15
+ const ray = options.rayVector ?? getCarRayVector(car, angleDegrees);
16
+ if (options.stats) {
17
+ if (options.rayVector) options.stats.callerVectorCount += 1;
18
+ else options.stats.computedVectorCount += 1;
19
+ }
15
20
  const maxDistance = metersToSimUnits(lengthMeters);
16
- let closest = null;
21
+ let closestTarget = null;
22
+ let closestDistance = Infinity;
17
23
  targets.forEach((other) => {
18
24
  if (!carRayBroadphaseHit(origin, ray, maxDistance, other)) return;
19
25
  const hitDistance = intersectCarFootprint(origin, ray, other);
20
26
  if (hitDistance == null || hitDistance > maxDistance) return;
21
- const distanceMeters = simUnitsToMeters(hitDistance);
22
- if (!closest || hitDistance < closest.distanceSimUnits) {
23
- closest = {
24
- hit: true,
25
- distanceSimUnits: hitDistance,
26
- distanceMeters,
27
- driverId: other.id,
28
- targetId: other.id,
29
- targetType: other.entityType,
30
- relativeSpeedKph: other.speedKph - car.speedKph,
31
- };
27
+ if (hitDistance < closestDistance) {
28
+ closestDistance = hitDistance;
29
+ closestTarget = other;
32
30
  }
33
31
  });
34
- if (!closest) return createCarRayMiss(lengthMeters);
35
- const { distanceSimUnits, ...publicHit } = closest;
36
- return publicHit;
32
+ const resultTarget = options.resultTarget ?? null;
33
+ if (resultTarget && options.stats) options.stats.resultTargetCount += 1;
34
+ if (!closestTarget) return writeCarRayMiss(resultTarget ?? {}, lengthMeters);
35
+ return writeCarRayResult(resultTarget ?? {}, car, closestTarget, closestDistance);
37
36
  }
38
37
 
39
38
  export function createCarRayMiss(lengthMeters) {
40
- return {
41
- hit: false,
42
- distanceMeters: lengthMeters,
43
- driverId: null,
44
- targetId: null,
45
- targetType: null,
46
- relativeSpeedKph: 0,
47
- };
39
+ return writeCarRayMiss({}, lengthMeters);
40
+ }
41
+
42
+ function writeCarRayMiss(target, lengthMeters) {
43
+ target.hit = false;
44
+ target.distanceMeters = lengthMeters;
45
+ target.driverId = null;
46
+ target.targetId = null;
47
+ target.targetType = null;
48
+ target.relativeSpeedKph = 0;
49
+ return target;
50
+ }
51
+
52
+ function writeCarRayResult(target, car, other, distanceSimUnits) {
53
+ target.hit = true;
54
+ target.distanceMeters = simUnitsToMeters(distanceSimUnits);
55
+ target.driverId = other.id;
56
+ target.targetId = other.id;
57
+ target.targetType = other.entityType;
58
+ target.relativeSpeedKph = other.speedKph - car.speedKph;
59
+ return target;
48
60
  }
49
61
 
50
62
  function carRayBroadphaseHit(origin, ray, maxDistance, other) {
@@ -58,18 +70,17 @@ function carRayBroadphaseHit(origin, ray, maxDistance, other) {
58
70
  }
59
71
 
60
72
  function intersectCarFootprint(origin, ray, other) {
61
- const forward = { x: Math.cos(other.heading), y: Math.sin(other.heading) };
62
- const right = { x: -Math.sin(other.heading), y: Math.cos(other.heading) };
63
- const delta = { x: origin.x - other.x, y: origin.y - other.y };
64
- const localOrigin = {
65
- x: dot(delta, forward),
66
- y: dot(delta, right),
67
- };
68
- const localRay = {
69
- x: dot(ray, forward),
70
- y: dot(ray, right),
71
- };
73
+ const forwardX = Math.cos(other.heading);
74
+ const forwardY = Math.sin(other.heading);
75
+ const rightX = -forwardY;
76
+ const rightY = forwardX;
77
+ const deltaX = origin.x - other.x;
78
+ const deltaY = origin.y - other.y;
79
+ const localOriginX = deltaX * forwardX + deltaY * forwardY;
80
+ const localOriginY = deltaX * rightX + deltaY * rightY;
81
+ const localRayX = ray.x * forwardX + ray.y * forwardY;
82
+ const localRayY = ray.x * rightX + ray.y * rightY;
72
83
  const halfLength = VEHICLE_GEOMETRY.bodyLength / 2;
73
84
  const halfWidth = VEHICLE_GEOMETRY.bodyWidth / 2;
74
- return intersectAxisAlignedBoxRay(localOrigin, localRay, halfLength, halfWidth);
85
+ return intersectAxisAlignedBoxRayScalars(localOriginX, localOriginY, localRayX, localRayY, halfLength, halfWidth);
75
86
  }