@inventure71/paddockjs 2.0.1 → 4.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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -0,0 +1,108 @@
1
+ const RAY_CHANNEL_FLAGS = Symbol('rayChannelFlags');
2
+
3
+ export const RAY_CHANNELS = Object.freeze([
4
+ 'roadEdge',
5
+ 'kerb',
6
+ 'illegalSurface',
7
+ 'car',
8
+ ]);
9
+
10
+ export function createRayChannelFlags() {
11
+ const flags = {
12
+ hasRoadEdge: false,
13
+ hasKerb: false,
14
+ hasIllegalSurface: false,
15
+ hasCar: false,
16
+ surfaceChannels: [],
17
+ has: channelFlagsHas,
18
+ };
19
+ Object.defineProperty(flags, RAY_CHANNEL_FLAGS, {
20
+ value: true,
21
+ enumerable: false,
22
+ });
23
+ return flags;
24
+ }
25
+
26
+ export function writeRayChannelFlags(target = createRayChannelFlags(), channels = []) {
27
+ ensureRayChannelFlags(target);
28
+ target.hasRoadEdge = false;
29
+ target.hasKerb = false;
30
+ target.hasIllegalSurface = false;
31
+ target.hasCar = false;
32
+ target.surfaceChannels.length = 0;
33
+
34
+ if (isRayChannelFlags(channels)) {
35
+ if (channels.hasRoadEdge) target.hasRoadEdge = true;
36
+ if (channels.hasKerb) {
37
+ target.hasKerb = true;
38
+ target.surfaceChannels.push('kerb');
39
+ }
40
+ if (channels.hasIllegalSurface) {
41
+ target.hasIllegalSurface = true;
42
+ target.surfaceChannels.push('illegalSurface');
43
+ }
44
+ if (channels.hasCar) target.hasCar = true;
45
+ return target;
46
+ }
47
+
48
+ if (Array.isArray(channels)) {
49
+ for (let index = 0; index < channels.length; index += 1) {
50
+ enableRayChannel(target, channels[index]);
51
+ }
52
+ return target;
53
+ }
54
+
55
+ if (channels && typeof channels.has === 'function') {
56
+ for (let index = 0; index < RAY_CHANNELS.length; index += 1) {
57
+ const channel = RAY_CHANNELS[index];
58
+ if (channels.has(channel)) enableRayChannel(target, channel);
59
+ }
60
+ }
61
+ return target;
62
+ }
63
+
64
+ export function isLegalRaySurface(surface) {
65
+ return surface === 'track' ||
66
+ surface === 'kerb' ||
67
+ surface === 'pit-entry' ||
68
+ surface === 'pit-lane' ||
69
+ surface === 'pit-exit' ||
70
+ surface === 'pit-box';
71
+ }
72
+
73
+ function ensureRayChannelFlags(target) {
74
+ if (!target.surfaceChannels) target.surfaceChannels = [];
75
+ target.has = channelFlagsHas;
76
+ if (!isRayChannelFlags(target)) {
77
+ Object.defineProperty(target, RAY_CHANNEL_FLAGS, {
78
+ value: true,
79
+ enumerable: false,
80
+ });
81
+ }
82
+ }
83
+
84
+ function isRayChannelFlags(value) {
85
+ return Boolean(value?.[RAY_CHANNEL_FLAGS]);
86
+ }
87
+
88
+ function enableRayChannel(target, channel) {
89
+ if (channel === 'roadEdge') {
90
+ target.hasRoadEdge = true;
91
+ } else if (channel === 'kerb') {
92
+ if (!target.hasKerb) target.surfaceChannels.push('kerb');
93
+ target.hasKerb = true;
94
+ } else if (channel === 'illegalSurface') {
95
+ if (!target.hasIllegalSurface) target.surfaceChannels.push('illegalSurface');
96
+ target.hasIllegalSurface = true;
97
+ } else if (channel === 'car') {
98
+ target.hasCar = true;
99
+ }
100
+ }
101
+
102
+ function channelFlagsHas(channel) {
103
+ if (channel === 'roadEdge') return this.hasRoadEdge;
104
+ if (channel === 'kerb') return this.hasKerb;
105
+ if (channel === 'illegalSurface') return this.hasIllegalSurface;
106
+ if (channel === 'car') return this.hasCar;
107
+ return false;
108
+ }
@@ -1,13 +1,11 @@
1
1
  import { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
2
+ import { RAY_CHANNELS } from './rayChannels.js';
2
3
 
3
- export const DEFAULT_RAY_LENGTH_METERS = 120;
4
+ const NORMALIZED_RAY_OPTIONS = Symbol('normalizedRayOptions');
5
+
6
+ export { RAY_CHANNELS } from './rayChannels.js';
4
7
 
5
- export const RAY_CHANNELS = Object.freeze([
6
- 'roadEdge',
7
- 'kerb',
8
- 'illegalSurface',
9
- 'car',
10
- ]);
8
+ export const DEFAULT_RAY_LENGTH_METERS = 120;
11
9
 
12
10
  export const RAY_PRECISION_MODES = Object.freeze(['driver', 'debug']);
13
11
 
@@ -26,6 +24,7 @@ export const RAY_LAYOUT_PRESETS = Object.freeze({
26
24
  });
27
25
 
28
26
  export function normalizeRayOptions(rayOptions = {}) {
27
+ if (rayOptions?.[NORMALIZED_RAY_OPTIONS]) return rayOptions;
29
28
  const defaultLengthMeters = positiveNumber(
30
29
  rayOptions.defaultLengthMeters ?? rayOptions.lengthMeters,
31
30
  DEFAULT_RAY_LENGTH_METERS,
@@ -33,10 +32,22 @@ export function normalizeRayOptions(rayOptions = {}) {
33
32
  const rays = normalizeRays(rayOptions, defaultLengthMeters);
34
33
  const channels = normalizeRayChannels(rayOptions);
35
34
  const precision = RAY_PRECISION_MODES.includes(rayOptions.precision) ? rayOptions.precision : 'driver';
36
- const anglesDegrees = rays.map((ray) => ray.angleDegrees);
37
- const lengthMeters = rays.length ? Math.max(...rays.map((ray) => ray.lengthMeters)) : defaultLengthMeters;
35
+ const anglesDegrees = new Array(rays.length);
36
+ let lengthMeters = rays.length ? 0 : defaultLengthMeters;
37
+ for (let index = 0; index < rays.length; index += 1) {
38
+ const ray = rays[index];
39
+ anglesDegrees[index] = ray.angleDegrees;
40
+ if (ray.lengthMeters > lengthMeters) lengthMeters = ray.lengthMeters;
41
+ }
42
+ let detectTrack = false;
43
+ let detectCars = false;
44
+ for (let index = 0; index < channels.length; index += 1) {
45
+ const channel = channels[index];
46
+ if (channel === 'roadEdge') detectTrack = true;
47
+ else if (channel === 'car') detectCars = true;
48
+ }
38
49
 
39
- return {
50
+ const normalized = {
40
51
  ...rayOptions,
41
52
  enabled: rayOptions.enabled !== false,
42
53
  anglesDegrees,
@@ -45,20 +56,24 @@ export function normalizeRayOptions(rayOptions = {}) {
45
56
  rays,
46
57
  channels,
47
58
  precision,
48
- detectTrack: channels.includes('roadEdge'),
49
- detectCars: channels.includes('car'),
59
+ detectTrack,
60
+ detectCars,
50
61
  };
62
+ Object.defineProperty(normalized, NORMALIZED_RAY_OPTIONS, {
63
+ value: true,
64
+ enumerable: false,
65
+ });
66
+ return normalized;
51
67
  }
52
68
 
53
69
  function normalizeRays(rayOptions, defaultLengthMeters) {
54
70
  const configuredRays = Array.isArray(rayOptions.rays)
55
71
  ? rayOptions.rays
56
72
  : resolvePresetRays(rayOptions.layout);
57
- const source = configuredRays.length
58
- ? configuredRays
59
- : normalizeAngles(rayOptions.anglesDegrees).map((angleDegrees) => ({ angleDegrees }));
60
-
61
- return source.map((ray, index) => {
73
+ const source = configuredRays.length ? configuredRays : normalizeAngles(rayOptions.anglesDegrees);
74
+ const rays = new Array(source.length);
75
+ for (let index = 0; index < source.length; index += 1) {
76
+ const ray = source[index];
62
77
  const angleDegrees = finiteNumber(
63
78
  typeof ray === 'number' ? ray : ray?.angleDegrees,
64
79
  DEFAULT_RAY_ANGLES_DEGREES[index % DEFAULT_RAY_ANGLES_DEGREES.length],
@@ -67,12 +82,13 @@ function normalizeRays(rayOptions, defaultLengthMeters) {
67
82
  typeof ray === 'object' ? ray.lengthMeters : null,
68
83
  defaultLengthMeters,
69
84
  );
70
- return {
85
+ rays[index] = {
71
86
  id: typeof ray === 'object' && ray.id ? String(ray.id) : `ray-${index}`,
72
87
  angleDegrees,
73
88
  lengthMeters,
74
89
  };
75
- });
90
+ }
91
+ return rays;
76
92
  }
77
93
 
78
94
  function resolvePresetRays(layout) {
@@ -81,16 +97,24 @@ function resolvePresetRays(layout) {
81
97
  }
82
98
 
83
99
  function normalizeAngles(value) {
84
- if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RAY_ANGLES_DEGREES];
85
- const angles = value.map((angle) => Number(angle)).filter((angle) => Number.isFinite(angle));
86
- return angles.length ? angles : [...DEFAULT_RAY_ANGLES_DEGREES];
100
+ if (!Array.isArray(value) || value.length === 0) return copyDefaultRayAngles();
101
+ const angles = [];
102
+ for (let index = 0; index < value.length; index += 1) {
103
+ const angle = Number(value[index]);
104
+ if (Number.isFinite(angle)) angles.push(angle);
105
+ }
106
+ return angles.length ? angles : copyDefaultRayAngles();
87
107
  }
88
108
 
89
109
  function normalizeRayChannels(rayOptions) {
90
110
  if (Array.isArray(rayOptions.channels) && rayOptions.channels.length) {
91
- const channelSet = new Set(RAY_CHANNELS);
92
- const channels = rayOptions.channels.filter((channel) => channelSet.has(channel));
93
- if (channels.length) return [...new Set(channels)];
111
+ const channels = [];
112
+ for (let index = 0; index < rayOptions.channels.length; index += 1) {
113
+ const channel = rayOptions.channels[index];
114
+ if (!isKnownRayChannel(channel) || hasChannel(channels, channel)) continue;
115
+ channels.push(channel);
116
+ }
117
+ if (channels.length) return channels;
94
118
  }
95
119
 
96
120
  const channels = [];
@@ -99,6 +123,28 @@ function normalizeRayChannels(rayOptions) {
99
123
  return channels;
100
124
  }
101
125
 
126
+ function isKnownRayChannel(channel) {
127
+ for (let index = 0; index < RAY_CHANNELS.length; index += 1) {
128
+ if (RAY_CHANNELS[index] === channel) return true;
129
+ }
130
+ return false;
131
+ }
132
+
133
+ function hasChannel(channels, channel) {
134
+ for (let index = 0; index < channels.length; index += 1) {
135
+ if (channels[index] === channel) return true;
136
+ }
137
+ return false;
138
+ }
139
+
140
+ function copyDefaultRayAngles() {
141
+ const angles = new Array(DEFAULT_RAY_ANGLES_DEGREES.length);
142
+ for (let index = 0; index < DEFAULT_RAY_ANGLES_DEGREES.length; index += 1) {
143
+ angles[index] = DEFAULT_RAY_ANGLES_DEGREES[index];
144
+ }
145
+ return angles;
146
+ }
147
+
102
148
  function positiveNumber(value, fallback) {
103
149
  const number = Number(value);
104
150
  return Number.isFinite(number) && number > 0 ? number : fallback;
@@ -21,13 +21,31 @@ export function pointOnRay(origin, ray, distance) {
21
21
  }
22
22
 
23
23
  export function intersectAxisAlignedBoxRay(origin, ray, halfLength, halfWidth) {
24
+ return intersectAxisAlignedBoxRayScalars(origin.x, origin.y, ray.x, ray.y, halfLength, halfWidth);
25
+ }
26
+
27
+ export function intersectAxisAlignedBoxRayScalars(originX, originY, rayX, rayY, halfLength, halfWidth) {
24
28
  let tMin = -Infinity;
25
29
  let tMax = Infinity;
26
- const xRange = intersectSlab(origin.x, ray.x, -halfLength, halfLength);
27
- const yRange = intersectSlab(origin.y, ray.y, -halfWidth, halfWidth);
28
- if (!xRange || !yRange) return null;
29
- tMin = Math.max(tMin, xRange.min, yRange.min);
30
- tMax = Math.min(tMax, xRange.max, yRange.max);
30
+
31
+ if (Math.abs(rayX) < 1e-9) {
32
+ if (originX < -halfLength || originX > halfLength) return null;
33
+ } else {
34
+ const first = (-halfLength - originX) / rayX;
35
+ const second = (halfLength - originX) / rayX;
36
+ tMin = Math.max(tMin, Math.min(first, second));
37
+ tMax = Math.min(tMax, Math.max(first, second));
38
+ }
39
+
40
+ if (Math.abs(rayY) < 1e-9) {
41
+ if (originY < -halfWidth || originY > halfWidth) return null;
42
+ } else {
43
+ const first = (-halfWidth - originY) / rayY;
44
+ const second = (halfWidth - originY) / rayY;
45
+ tMin = Math.max(tMin, Math.min(first, second));
46
+ tMax = Math.min(tMax, Math.max(first, second));
47
+ }
48
+
31
49
  if (tMax < 0 || tMin > tMax) return null;
32
50
  return Math.max(0, tMin);
33
51
  }
@@ -46,15 +64,3 @@ export function normalizeRelativeHeading(angle) {
46
64
  while (value < -Math.PI) value += Math.PI * 2;
47
65
  return value;
48
66
  }
49
-
50
- function intersectSlab(origin, direction, min, max) {
51
- if (Math.abs(direction) < 1e-9) {
52
- return origin >= min && origin <= max ? { min: -Infinity, max: Infinity } : null;
53
- }
54
- const first = (min - origin) / direction;
55
- const second = (max - origin) / direction;
56
- return {
57
- min: Math.min(first, second),
58
- max: Math.max(first, second),
59
- };
60
- }
@@ -14,7 +14,10 @@ export function nearbyDetectableTargets(self, snapshot) {
14
14
  return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
15
15
  }
16
16
 
17
- export function rayDetectableTargetsForSnapshot(snapshot) {
17
+ export function rayDetectableTargetsForSnapshot(snapshot, scratch = null) {
18
+ if (scratch) {
19
+ return writeRayDetectableTargetsForSnapshot(snapshot, scratch);
20
+ }
18
21
  return [
19
22
  ...detectableCars(snapshot, isRayDetectable),
20
23
  ...detectableReplayGhosts(snapshot, 'detectableByRays'),
@@ -45,6 +48,69 @@ function detectableCars(snapshot, predicate) {
45
48
  }));
46
49
  }
47
50
 
51
+ function writeRayDetectableTargetsForSnapshot(snapshot, scratch) {
52
+ const targets = scratch.rayTargets ?? [];
53
+ const pool = scratch.rayTargetPool ?? [];
54
+ scratch.rayTargets = targets;
55
+ scratch.rayTargetPool = pool;
56
+ let count = 0;
57
+ let carOrder = 0;
58
+ const cars = snapshot.cars ?? [];
59
+ for (let index = 0; index < cars.length; index += 1) {
60
+ const car = cars[index];
61
+ if (!isRayDetectable(car)) continue;
62
+ const target = pool[count] ?? {};
63
+ pool[count] = target;
64
+ writeCarTarget(target, car, carOrder);
65
+ targets[count] = target;
66
+ count += 1;
67
+ carOrder += 1;
68
+ }
69
+ let ghostOrder = 0;
70
+ const carCount = cars.length;
71
+ const replayGhosts = snapshot.replayGhosts ?? [];
72
+ for (let index = 0; index < replayGhosts.length; index += 1) {
73
+ const ghost = replayGhosts[index];
74
+ if (ghost.sensors?.detectableByRays !== true) continue;
75
+ const target = pool[count] ?? {};
76
+ pool[count] = target;
77
+ writeReplayGhostTarget(target, ghost, carCount + ghostOrder);
78
+ targets[count] = target;
79
+ count += 1;
80
+ ghostOrder += 1;
81
+ }
82
+ targets.length = count;
83
+ return targets;
84
+ }
85
+
86
+ function writeCarTarget(target, car, order) {
87
+ target.entityType = SENSOR_TARGET_CAR;
88
+ target.id = car.id;
89
+ target.x = car.x;
90
+ target.y = car.y;
91
+ target.heading = car.heading;
92
+ target.velocityX = car.velocityX;
93
+ target.velocityY = car.velocityY;
94
+ target.speedKph = car.speedKph ?? 0;
95
+ target.lap = car.lap;
96
+ target.order = order;
97
+ return target;
98
+ }
99
+
100
+ function writeReplayGhostTarget(target, ghost, order) {
101
+ target.entityType = SENSOR_TARGET_REPLAY_GHOST;
102
+ target.id = ghost.id;
103
+ target.x = ghost.x;
104
+ target.y = ghost.y;
105
+ target.heading = ghost.heading;
106
+ target.velocityX = undefined;
107
+ target.velocityY = undefined;
108
+ target.speedKph = ghost.speedKph ?? 0;
109
+ target.lap = null;
110
+ target.order = order;
111
+ return target;
112
+ }
113
+
48
114
  function detectableReplayGhosts(snapshot, sensorFlag) {
49
115
  const carCount = snapshot.cars?.length ?? 0;
50
116
  return (snapshot.replayGhosts ?? [])