@inventure71/paddockjs 1.0.0 → 2.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,126 @@
1
+ import { getCarRayOrigin, getCarRayVector } from '../../environment/sensors.js';
2
+ import { metersToSimUnits } from '../../simulation/units.js';
3
+
4
+ const SENSOR_RAY_TRACK_COLOR = 0xf1c65b;
5
+ const SENSOR_RAY_TRACK_ENTRY_COLOR = 0x68d8ff;
6
+ const SENSOR_RAY_CAR_COLOR = 0xff4d5f;
7
+ const SENSOR_RAY_KERB_COLOR = 0x49d17d;
8
+ const SENSOR_RAY_ILLEGAL_COLOR = 0xd946ef;
9
+ const SENSOR_RAY_MARKER_STROKE = 0x10131a;
10
+
11
+ const SENSOR_HIT_CHANNELS = Object.freeze([
12
+ { key: 'track', radius: 5.8, alpha: 0.94 },
13
+ { key: 'kerb', color: SENSOR_RAY_KERB_COLOR, radius: 4.6, alpha: 0.9 },
14
+ { key: 'illegalSurface', color: SENSOR_RAY_ILLEGAL_COLOR, radius: 5.2, alpha: 0.92 },
15
+ { key: 'car', color: SENSOR_RAY_CAR_COLOR, radius: 6.6, alpha: 0.96 },
16
+ ]);
17
+
18
+ function expertVisualizesRays(expertOptions) {
19
+ const setting = expertOptions?.visualizeSensors;
20
+ if (setting === true) return true;
21
+ if (!setting || setting === false) return false;
22
+ return Boolean(setting.rays);
23
+ }
24
+
25
+ function pointFromRay(origin, ray, distance) {
26
+ return {
27
+ x: origin.x + ray.x * distance,
28
+ y: origin.y + ray.y * distance,
29
+ };
30
+ }
31
+
32
+ function clearSensorLayerIfRendered(sensorLayer) {
33
+ if (!sensorLayer?.__paddockHasExpertSensorRays) return;
34
+ sensorLayer.clear();
35
+ sensorLayer.__paddockHasExpertSensorRays = false;
36
+ }
37
+
38
+ export function renderExpertSensorRays({ snapshot, observation, sensorLayer, expertMode, expertOptions }) {
39
+ if (!sensorLayer) return;
40
+ if (!expertMode || !expertVisualizesRays(expertOptions)) {
41
+ clearSensorLayerIfRendered(sensorLayer);
42
+ return;
43
+ }
44
+
45
+ const controlledDrivers = visualizedSensorDrivers(expertOptions);
46
+ if (!controlledDrivers.length || !observation) {
47
+ clearSensorLayerIfRendered(sensorLayer);
48
+ return;
49
+ }
50
+
51
+ const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
52
+ const drawableDrivers = controlledDrivers.filter((driverId) => {
53
+ const car = carsById.get(driverId);
54
+ const rays = observation?.[driverId]?.object?.rays;
55
+ return Boolean(car && Array.isArray(rays) && rays.length > 0);
56
+ });
57
+ if (!drawableDrivers.length) {
58
+ clearSensorLayerIfRendered(sensorLayer);
59
+ return;
60
+ }
61
+
62
+ sensorLayer.clear();
63
+ sensorLayer.__paddockHasExpertSensorRays = true;
64
+ drawableDrivers.forEach((driverId) => {
65
+ const car = carsById.get(driverId);
66
+ const rays = observation?.[driverId]?.object?.rays;
67
+ if (!car || !Array.isArray(rays) || rays.length === 0) return;
68
+
69
+ const origin = getCarRayOrigin(car);
70
+
71
+ rays.forEach((ray) => {
72
+ const rayVector = getCarRayVector(car, Number(ray.angleDegrees) || 0);
73
+ const totalDistanceMeters = Math.max(0, Number(ray.lengthMeters) || 0);
74
+ const fullEnd = pointFromRay(origin, rayVector, metersToSimUnits(totalDistanceMeters));
75
+
76
+ sensorLayer
77
+ .moveTo(origin.x, origin.y)
78
+ .lineTo(fullEnd.x, fullEnd.y)
79
+ .stroke({ width: 2, color: 0xffffff, alpha: 0.18, cap: 'round' });
80
+
81
+ rayHitMarkers(ray, totalDistanceMeters).forEach((marker) => {
82
+ const hitEnd = pointFromRay(origin, rayVector, metersToSimUnits(marker.distanceMeters));
83
+ sensorLayer
84
+ .circle(hitEnd.x, hitEnd.y, marker.radius)
85
+ .fill({ color: marker.color, alpha: marker.alpha })
86
+ .stroke({ width: 1.4, color: SENSOR_RAY_MARKER_STROKE, alpha: 0.82 });
87
+ });
88
+ });
89
+ });
90
+ }
91
+
92
+ function rayHitMarkers(ray, totalDistanceMeters) {
93
+ return SENSOR_HIT_CHANNELS
94
+ .map((channel) => {
95
+ const hit = ray[channel.key];
96
+ const distanceMeters = Math.max(0, Number(hit?.distanceMeters));
97
+ if (!hit?.hit || !Number.isFinite(distanceMeters) || distanceMeters > totalDistanceMeters) return null;
98
+ return {
99
+ ...channel,
100
+ distanceMeters,
101
+ color: markerColor(channel, hit),
102
+ };
103
+ })
104
+ .filter(Boolean);
105
+ }
106
+
107
+ function markerColor(channel, hit) {
108
+ if (channel.key === 'track') {
109
+ return hit.kind === 'entry' ? SENSOR_RAY_TRACK_ENTRY_COLOR : SENSOR_RAY_TRACK_COLOR;
110
+ }
111
+ return channel.color;
112
+ }
113
+
114
+ function visualizedSensorDrivers(expertOptions) {
115
+ const controlledDrivers = expertOptions?.controlledDrivers ?? [];
116
+ if (!controlledDrivers.length) return [];
117
+ const setting = expertOptions?.visualizeSensors;
118
+ if (setting && typeof setting === 'object') {
119
+ if (Array.isArray(setting.drivers)) {
120
+ return setting.drivers.filter((driverId) => controlledDrivers.includes(driverId));
121
+ }
122
+ if (setting.drivers === 'all') return controlledDrivers;
123
+ if (controlledDrivers.includes(setting.selectedDriverId)) return [setting.selectedDriverId];
124
+ }
125
+ return [controlledDrivers[0]];
126
+ }
@@ -0,0 +1,50 @@
1
+ import { colorToTint } from './displayUtils.js';
2
+
3
+ export class PitLaneStatusRenderer {
4
+ constructor() {
5
+ this.lastRenderKey = null;
6
+ }
7
+
8
+ reset() {
9
+ this.lastRenderKey = null;
10
+ }
11
+
12
+ render(snapshot, pitLaneStatusLayer) {
13
+ if (!pitLaneStatusLayer) return;
14
+ const pitLane = snapshot.track?.pitLane;
15
+ if (!pitLane?.enabled || !pitLane.mainLane?.start) {
16
+ if (this.lastRenderKey !== 'none') {
17
+ pitLaneStatusLayer.clear();
18
+ this.lastRenderKey = 'none';
19
+ }
20
+ return;
21
+ }
22
+ const status = snapshot.pitLaneStatus ?? snapshot.raceControl?.pitLaneStatus;
23
+ const color = colorToTint(status?.light ?? (status?.open ? '#22c55e' : '#ef4444'));
24
+ const heading = pitLane.mainLane.heading ?? 0;
25
+ const normal = pitLane.serviceNormal ?? { x: -Math.sin(heading), y: Math.cos(heading) };
26
+ const sign = pitLane.workingLane?.offset != null && pitLane.workingLane.offset < 0 ? 1 : -1;
27
+ const base = pitLane.mainLane.start;
28
+ const x = base.x + normal.x * sign * (pitLane.width * 0.72);
29
+ const y = base.y + normal.y * sign * (pitLane.width * 0.72);
30
+ const renderKey = [
31
+ color,
32
+ x.toFixed(3),
33
+ y.toFixed(3),
34
+ pitLane.width,
35
+ status?.open ? 1 : 0,
36
+ status?.reason ?? '',
37
+ ].join(':');
38
+ if (renderKey === this.lastRenderKey) return;
39
+ this.lastRenderKey = renderKey;
40
+
41
+ pitLaneStatusLayer.clear();
42
+ pitLaneStatusLayer
43
+ .circle(x, y, 17)
44
+ .fill({ color: 0x0b0f14, alpha: 0.92 })
45
+ .circle(x, y, 11)
46
+ .fill({ color, alpha: 0.94 })
47
+ .circle(x, y, 18)
48
+ .stroke({ width: 3, color: 0xf8fafc, alpha: 0.82 });
49
+ }
50
+ }
@@ -0,0 +1,124 @@
1
+ import { Graphics, Sprite, Text, Texture } from 'pixi.js';
2
+ import { CAR_WORLD_LENGTH } from '../camera/cameraConstants.js';
3
+ import { VEHICLE_GEOMETRY } from '../../simulation/vehicleGeometry.js';
4
+ import { colorToTint, smoothAngle } from './displayUtils.js';
5
+
6
+ const CAR_WORLD_WIDTH = VEHICLE_GEOMETRY.visualWidth;
7
+
8
+ export class ReplayGhostRenderer {
9
+ constructor() {
10
+ this.sprites = new Map();
11
+ this.labels = new Map();
12
+ this.halos = new Map();
13
+ }
14
+
15
+ render(snapshot, { textures, replayGhostLayer }) {
16
+ if (!replayGhostLayer) return;
17
+ const visibleGhosts = (snapshot.replayGhosts ?? []).filter((ghost) => ghost.visible !== false);
18
+ const visibleIds = new Set(visibleGhosts.map((ghost) => ghost.id));
19
+ [...this.sprites.keys()].forEach((id) => {
20
+ if (visibleIds.has(id)) return;
21
+ this.destroyGhost(id);
22
+ });
23
+
24
+ visibleGhosts.forEach((ghost) => {
25
+ const halo = this.ensureHalo(ghost, replayGhostLayer);
26
+ halo.x = ghost.x;
27
+ halo.y = ghost.y;
28
+ halo.rotation = ghost.heading;
29
+ this.drawHalo(halo, ghost);
30
+
31
+ const sprite = this.ensureSprite(ghost, textures, replayGhostLayer);
32
+ sprite.x = ghost.x;
33
+ sprite.y = ghost.y;
34
+ sprite.currentRotation = smoothAngle(sprite.currentRotation, ghost.heading, 0.24);
35
+ sprite.rotation = sprite.currentRotation;
36
+ sprite.alpha = ghost.opacity;
37
+ sprite.tint = colorToTint(ghost.color);
38
+
39
+ const label = this.ensureLabel(ghost, replayGhostLayer);
40
+ label.text = ghost.label;
41
+ label.x = ghost.x;
42
+ label.y = ghost.y - CAR_WORLD_WIDTH * 1.15;
43
+ label.alpha = Math.min(1, ghost.opacity + 0.2);
44
+ label.visible = Boolean(ghost.label);
45
+ });
46
+ }
47
+
48
+ destroy() {
49
+ [...this.sprites.keys()].forEach((id) => this.destroyGhost(id));
50
+ }
51
+
52
+ ensureHalo(ghost, replayGhostLayer) {
53
+ let halo = this.halos.get(ghost.id);
54
+ if (halo) return halo;
55
+ halo = new Graphics();
56
+ halo.eventMode = 'none';
57
+ halo.ghostVisualRole = 'replay-halo';
58
+ this.halos.set(ghost.id, halo);
59
+ replayGhostLayer.addChild(halo);
60
+ return halo;
61
+ }
62
+
63
+ ensureSprite(ghost, textures, replayGhostLayer) {
64
+ let sprite = this.sprites.get(ghost.id);
65
+ if (sprite) return sprite;
66
+ const texture = textures.car ?? Texture.WHITE;
67
+ sprite = new Sprite(texture);
68
+ sprite.anchor.set(0.5);
69
+ sprite.baseScale = Math.min(
70
+ CAR_WORLD_LENGTH / Math.max(texture.width, 1),
71
+ CAR_WORLD_WIDTH / Math.max(texture.height, 1),
72
+ );
73
+ sprite.scale.set(sprite.baseScale);
74
+ sprite.eventMode = 'none';
75
+ sprite.currentRotation = ghost.heading;
76
+ this.sprites.set(ghost.id, sprite);
77
+ replayGhostLayer.addChild(sprite);
78
+ return sprite;
79
+ }
80
+
81
+ ensureLabel(ghost, replayGhostLayer) {
82
+ let label = this.labels.get(ghost.id);
83
+ if (label) return label;
84
+ label = new Text({
85
+ text: ghost.label,
86
+ style: {
87
+ fill: 0xffffff,
88
+ fontFamily: 'Arial, sans-serif',
89
+ fontSize: 18,
90
+ fontWeight: '800',
91
+ align: 'center',
92
+ stroke: { color: 0x000000, width: 3 },
93
+ },
94
+ });
95
+ label.anchor.set(0.5);
96
+ label.resolution = 2;
97
+ this.labels.set(ghost.id, label);
98
+ replayGhostLayer.addChild(label);
99
+ return label;
100
+ }
101
+
102
+ destroyGhost(id) {
103
+ this.halos.get(id)?.destroy?.();
104
+ this.sprites.get(id)?.destroy?.();
105
+ this.labels.get(id)?.destroy?.();
106
+ this.halos.delete(id);
107
+ this.sprites.delete(id);
108
+ this.labels.delete(id);
109
+ }
110
+
111
+ drawHalo(halo, ghost) {
112
+ const tint = colorToTint(ghost.color);
113
+ const alpha = Math.max(0.18, Math.min(0.55, ghost.opacity + 0.16));
114
+ halo.clear();
115
+ halo
116
+ .ellipse(0, 0, CAR_WORLD_LENGTH * 0.72, CAR_WORLD_WIDTH * 1.25)
117
+ .fill({ color: tint, alpha: Math.min(0.14, alpha * 0.28) })
118
+ .stroke({ width: 5, color: tint, alpha });
119
+ halo
120
+ .moveTo(CAR_WORLD_LENGTH * 0.55, 0)
121
+ .lineTo(CAR_WORLD_LENGTH * 0.92, 0)
122
+ .stroke({ width: 3, color: 0xffffff, alpha: Math.min(0.72, alpha + 0.14) });
123
+ }
124
+ }
@@ -0,0 +1,25 @@
1
+ import { Graphics } from 'pixi.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/trackModel.js';
3
+ import { destroyDisplayChildren } from './displayUtils.js';
4
+
5
+ export function renderTrackSurface({ drsLayer, sensorLayer, pitLaneStatusLayer, trackAsset, snapshot }) {
6
+ destroyDisplayChildren(drsLayer);
7
+ sensorLayer?.clear();
8
+ pitLaneStatusLayer?.clear();
9
+ const track = snapshot.track;
10
+
11
+ trackAsset.render(track);
12
+
13
+ track.drsZones.forEach((zone) => {
14
+ const zoneLine = new Graphics();
15
+ const steps = 44;
16
+ for (let index = 0; index <= steps; index += 1) {
17
+ const basePoint = pointAt(track, zone.start + ((zone.end - zone.start) * index) / steps);
18
+ const point = offsetTrackPoint(basePoint, track.width / 2 - 22);
19
+ if (index === 0) zoneLine.moveTo(point.x, point.y);
20
+ else zoneLine.lineTo(point.x, point.y);
21
+ }
22
+ zoneLine.stroke({ width: 8, color: 0x14c784, alpha: 0.5, join: 'round', cap: 'round' });
23
+ drsLayer.addChild(zoneLine);
24
+ });
25
+ }
@@ -0,0 +1,66 @@
1
+ import { interpolateRenderSnapshotInto } from '../../rendering/renderSnapshot.js';
2
+ import { FIXED_STEP } from '../../simulation/raceSimulation.js';
3
+ import { clamp } from '../../simulation/simMath.js';
4
+ import {
5
+ domUpdateIntervalForSpeed,
6
+ FRAME_PACING_EPSILON_MS,
7
+ maxSimulationStepsForFrame,
8
+ MAX_FRAME_CATCHUP_COUNT,
9
+ TARGET_FRAME_MS,
10
+ } from './runtimeTiming.js';
11
+
12
+ export function runFrameLoopTick(app) {
13
+ if (app.expertMode) return;
14
+ const now = performance.now();
15
+ if (now < app.nextGameFrameTime - FRAME_PACING_EPSILON_MS) return;
16
+
17
+ const elapsedFrameCount = clamp(
18
+ Math.floor((now - app.nextGameFrameTime + FRAME_PACING_EPSILON_MS) / TARGET_FRAME_MS) + 1,
19
+ 1,
20
+ MAX_FRAME_CATCHUP_COUNT,
21
+ );
22
+ const frameSeconds = (TARGET_FRAME_MS * elapsedFrameCount) / 1000;
23
+ app.nextGameFrameTime += TARGET_FRAME_MS * elapsedFrameCount;
24
+ if (now - app.nextGameFrameTime > TARGET_FRAME_MS * MAX_FRAME_CATCHUP_COUNT) {
25
+ app.nextGameFrameTime = now + TARGET_FRAME_MS;
26
+ }
27
+ app.lastTime = now;
28
+ app.sampleFps(now);
29
+ app.accumulator += frameSeconds * app.simulationSpeed;
30
+
31
+ let simulationSteps = 0;
32
+ const stepEvents = [];
33
+ const maxSimulationSteps = maxSimulationStepsForFrame(app.simulationSpeed, elapsedFrameCount);
34
+ while (app.accumulator >= FIXED_STEP && simulationSteps < maxSimulationSteps) {
35
+ app.sim.step(FIXED_STEP);
36
+ if (Array.isArray(app.sim.events) && app.sim.events.length > 0) {
37
+ stepEvents.push(...app.sim.events);
38
+ }
39
+ app.accumulator -= FIXED_STEP;
40
+ simulationSteps += 1;
41
+ }
42
+
43
+ if (app.accumulator >= FIXED_STEP) {
44
+ app.accumulator %= FIXED_STEP;
45
+ app.nextGameFrameTime = now + TARGET_FRAME_MS;
46
+ }
47
+
48
+ const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
49
+ const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
50
+ if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
51
+ const renderSource = fullSnapshot ?? app.sim.snapshotRender?.() ?? app.sim.snapshot();
52
+ const renderSnapshot = interpolateRenderSnapshotInto(
53
+ app.renderSnapshotBuffer ?? {},
54
+ renderSource,
55
+ clamp(app.accumulator / FIXED_STEP, 0, 1),
56
+ );
57
+ app.renderSnapshotBuffer = renderSnapshot;
58
+ app.applyCamera(renderSnapshot);
59
+ app.renderDrsTrails(renderSnapshot);
60
+ app.renderPitLaneStatus(renderSnapshot);
61
+ app.renderCars(renderSnapshot);
62
+ if (shouldUpdateDom) {
63
+ app.updateDom(fullSnapshot, { emitLifecycle: false });
64
+ app.lastDomUpdateTime = now;
65
+ }
66
+ }
@@ -0,0 +1,34 @@
1
+ import { clamp } from '../../simulation/simMath.js';
2
+
3
+ export const TARGET_RENDER_FPS = 60;
4
+ export const TARGET_FRAME_MS = 1000 / TARGET_RENDER_FPS;
5
+ export const FRAME_PACING_EPSILON_MS = 0.75;
6
+ export const MAX_FRAME_CATCHUP_COUNT = 4;
7
+ export const BASE_MAX_SIMULATION_STEPS_PER_RENDER = 5;
8
+ export const HARD_MAX_SIMULATION_STEPS_PER_RENDER = 60;
9
+ export const DOM_UPDATE_INTERVAL_MS = 100;
10
+ export const TIMING_UPDATE_INTERVAL_MS = 250;
11
+
12
+ export function maxSimulationStepsForFrame(simulationSpeed, elapsedFrameCount) {
13
+ const speed = Math.max(1, Number(simulationSpeed) || 1);
14
+ const frameCount = Math.max(1, Number(elapsedFrameCount) || 1);
15
+ return clamp(
16
+ Math.ceil(speed * frameCount) + 1,
17
+ BASE_MAX_SIMULATION_STEPS_PER_RENDER,
18
+ HARD_MAX_SIMULATION_STEPS_PER_RENDER,
19
+ );
20
+ }
21
+
22
+ export function domUpdateIntervalForSpeed(simulationSpeed) {
23
+ const speed = Math.max(1, Number(simulationSpeed) || 1);
24
+ if (speed >= 10) return 250;
25
+ if (speed >= 5) return 180;
26
+ return DOM_UPDATE_INTERVAL_MS;
27
+ }
28
+
29
+ export function timingUpdateIntervalForSpeed(simulationSpeed) {
30
+ const speed = Math.max(1, Number(simulationSpeed) || 1);
31
+ if (speed >= 10) return 600;
32
+ if (speed >= 5) return 400;
33
+ return TIMING_UPDATE_INTERVAL_MS;
34
+ }
@@ -0,0 +1,45 @@
1
+ export function observeRuntimeVisibility(app) {
2
+ if (!app.canvasHost) return;
3
+
4
+ if (typeof IntersectionObserver === 'function') {
5
+ app.visibilityObserver?.disconnect?.();
6
+ app.visibilityObserver = new IntersectionObserver((entries) => {
7
+ const entry = entries.find((item) => item.target === app.canvasHost) ?? entries[0];
8
+ app.runtimeViewportVisible = Boolean(entry?.isIntersecting || entry?.intersectionRatio > 0);
9
+ app.syncRuntimeTicker();
10
+ }, {
11
+ root: null,
12
+ rootMargin: '240px 0px',
13
+ threshold: 0,
14
+ });
15
+ app.visibilityObserver.observe(app.canvasHost);
16
+ }
17
+
18
+ if (typeof document !== 'undefined') {
19
+ app.visibilityChangeHandler = () => {
20
+ app.runtimeDocumentVisible = document.visibilityState !== 'hidden';
21
+ app.syncRuntimeTicker();
22
+ };
23
+ document.addEventListener('visibilitychange', app.visibilityChangeHandler, {
24
+ signal: app.abortController.signal,
25
+ });
26
+ }
27
+
28
+ app.syncRuntimeTicker();
29
+ }
30
+
31
+ export function syncRuntimeTicker(app) {
32
+ if (app.expertMode) return;
33
+ if (!app.app?.ticker) return;
34
+
35
+ const shouldRun = app.runtimeViewportVisible && app.runtimeDocumentVisible;
36
+ if (shouldRun === app.runtimeTickerRunning) return;
37
+
38
+ app.runtimeTickerRunning = shouldRun;
39
+ if (shouldRun) {
40
+ app.resetFrameClock();
41
+ app.app.ticker.start?.();
42
+ } else {
43
+ app.app.ticker.stop?.();
44
+ }
45
+ }
@@ -1,9 +1,17 @@
1
1
  import { resolveF1SimulatorAssets } from './defaultAssets.js';
2
2
  import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
3
3
  import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
4
+ import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
5
+ import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
4
6
 
5
7
  export const DEFAULT_F1_SIMULATOR_OPTIONS = {
6
8
  seed: 1971,
9
+ physicsMode: 'arcade',
10
+ trackQueryIndex: true,
11
+ warmup: {
12
+ enabled: true,
13
+ policy: 'config-change',
14
+ },
7
15
  totalLaps: 10,
8
16
  initialCameraMode: 'leader',
9
17
  title: 'F1 Simulator Lab',
@@ -33,6 +41,9 @@ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
33
41
  raceDataBannerSize: 'custom',
34
42
  timingTowerVerticalFit: 'expand-race-view',
35
43
  },
44
+ debug: {
45
+ physicsModeIndicator: false,
46
+ },
36
47
  theme: {
37
48
  accentColor: '#e10600',
38
49
  greenColor: '#14c784',
@@ -144,6 +155,12 @@ export function resolveF1SimulatorOptions(options = {}) {
144
155
  ui.raceDataBannerSize = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBannerSize;
145
156
  }
146
157
  ui.raceDataTelemetryDetail = Boolean(ui.raceDataTelemetryDetail);
158
+ const debug = {
159
+ ...DEFAULT_F1_SIMULATOR_OPTIONS.debug,
160
+ ...(preset.debug ?? {}),
161
+ ...(options.debug ?? {}),
162
+ };
163
+ debug.physicsModeIndicator = Boolean(debug.physicsModeIndicator);
147
164
  const initialCameraMode = SUPPORTED_CAMERA_MODES.has(options.initialCameraMode)
148
165
  ? options.initialCameraMode
149
166
  : DEFAULT_F1_SIMULATOR_OPTIONS.initialCameraMode;
@@ -166,8 +183,11 @@ export function resolveF1SimulatorOptions(options = {}) {
166
183
  ...preset,
167
184
  ...options,
168
185
  preset: presetName ?? options.preset,
186
+ physicsMode: normalizePhysicsMode(options.physicsMode ?? preset.physicsMode),
187
+ warmup: normalizeWarmupOptions(mergedOptions.warmup, 'browser'),
169
188
  initialCameraMode,
170
189
  ui,
190
+ debug,
171
191
  theme,
172
192
  drivers,
173
193
  assets: resolveF1SimulatorAssets(options.assets),
@@ -1,5 +1,5 @@
1
1
  import { clamp } from '../simulation/simMath.js';
2
- import { VEHICLE_LIMITS } from '../simulation/vehiclePhysics.js';
2
+ import { VEHICLE_LIMITS } from '../simulation/vehicle/vehiclePhysics.js';
3
3
 
4
4
  export function resolveActionMap(actions = {}, controlledDrivers = [], { policy = 'strict' } = {}) {
5
5
  const errors = [];
@@ -22,9 +22,9 @@ export function resolveActionMap(actions = {}, controlledDrivers = [], { policy
22
22
  }
23
23
 
24
24
  export function normalizeAction(action, driverId = 'unknown', policy = 'strict', errors = []) {
25
- const steering = finiteActionValue(action.steering, `Invalid steering action for controlled driver: ${driverId}`, policy, errors);
26
- const throttle = finiteActionValue(action.throttle, `Invalid throttle action for controlled driver: ${driverId}`, policy, errors);
27
- const brake = finiteActionValue(action.brake, `Invalid brake action for controlled driver: ${driverId}`, policy, errors);
25
+ const steering = requiredActionValue(action, 'steering', driverId, policy, errors);
26
+ const throttle = requiredActionValue(action, 'throttle', driverId, policy, errors);
27
+ const brake = requiredActionValue(action, 'brake', driverId, policy, errors);
28
28
 
29
29
  if (steering == null || throttle == null || brake == null) return null;
30
30
 
@@ -35,8 +35,16 @@ export function normalizeAction(action, driverId = 'unknown', policy = 'strict',
35
35
  };
36
36
  }
37
37
 
38
+ function requiredActionValue(action, field, driverId, policy, errors) {
39
+ if (!Object.hasOwn(action, field)) {
40
+ handleActionError(`Missing ${field} action for controlled driver: ${driverId}`, policy, errors);
41
+ return null;
42
+ }
43
+ return finiteActionValue(action[field], `Invalid ${field} action for controlled driver: ${driverId}`, policy, errors);
44
+ }
45
+
38
46
  function finiteActionValue(value, message, policy, errors) {
39
- const number = Number(value ?? 0);
47
+ const number = Number(value);
40
48
  if (Number.isFinite(number)) return number;
41
49
  handleActionError(message, policy, errors);
42
50
  return null;