@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.
- package/CHANGELOG.md +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +230 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- 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 =
|
|
26
|
-
const throttle =
|
|
27
|
-
const brake =
|
|
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
|
|
47
|
+
const number = Number(value);
|
|
40
48
|
if (Number.isFinite(number)) return number;
|
|
41
49
|
handleActionError(message, policy, errors);
|
|
42
50
|
return null;
|