@inventure71/paddockjs 0.3.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 +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- 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 +14 -1
- 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 +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- 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 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- 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 +214 -0
- 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 -253
- 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 +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -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/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -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/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- 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 -628
- package/src/simulation/units.js +6 -4
- 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 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -1,71 +1,70 @@
|
|
|
1
|
-
import { Application,
|
|
2
|
-
import { DRIVER_STAT_DEFINITIONS, formatDriverNumber, VEHICLE_STAT_DEFINITIONS } from '../data/championship.js';
|
|
1
|
+
import { Application, Container, Graphics } from 'pixi.js';
|
|
3
2
|
import { applyPaddockThemeCssVariables } from '../config/defaultOptions.js';
|
|
4
|
-
import { normalizeCustomFields } from '../data/customFields.js';
|
|
5
|
-
import { getCarRayOrigin, getCarRayVector } from '../environment/sensors.js';
|
|
6
3
|
import { ProceduralTrackAsset } from '../rendering/proceduralTrackAsset.js';
|
|
7
|
-
import {
|
|
4
|
+
import { interpolateRenderSnapshotInto } from '../rendering/renderSnapshot.js';
|
|
8
5
|
import { createRaceSimulation, FIXED_STEP } from '../simulation/raceSimulation.js';
|
|
9
6
|
import { clamp } from '../simulation/simMath.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { WORLD } from '../simulation/track/trackModel.js';
|
|
8
|
+
import { CameraController } from './camera/cameraController.js';
|
|
12
9
|
import { createBrowserExpertAdapter } from './BrowserExpertAdapter.js';
|
|
13
|
-
import { querySimulatorDom
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
10
|
+
import { querySimulatorDom } from './domBindings.js';
|
|
11
|
+
import { loadAppTextures } from './rendering/appAssets.js';
|
|
12
|
+
import { CarRenderer } from './rendering/carRenderer.js';
|
|
13
|
+
import { DrsTrailRenderer } from './rendering/drsTrailRenderer.js';
|
|
14
|
+
import { renderExpertSensorRays } from './rendering/expertSensorRenderer.js';
|
|
15
|
+
import { PitLaneStatusRenderer } from './rendering/pitLaneStatusRenderer.js';
|
|
16
|
+
import { ReplayGhostRenderer } from './rendering/replayGhostRenderer.js';
|
|
17
|
+
import { renderTrackSurface } from './rendering/trackRenderer.js';
|
|
18
|
+
import {
|
|
19
|
+
formatLapGap,
|
|
20
|
+
formatRaceGap,
|
|
21
|
+
getPenaltyByDriver,
|
|
22
|
+
getTimingOrderKey,
|
|
23
|
+
getTimingPenaltyKey,
|
|
24
|
+
isWavedFlagCar,
|
|
25
|
+
renderTimingTower,
|
|
26
|
+
syncTimingGapModeControls,
|
|
27
|
+
} from './readouts/timingTowerRenderer.js';
|
|
28
|
+
import { formatTelemetryGap } from './readouts/readoutFormatters.js';
|
|
29
|
+
import { getOverviewFields, renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
30
|
+
import { renderLapTelemetry, renderTelemetryReadouts } from './readouts/telemetryRenderer.js';
|
|
31
|
+
import {
|
|
32
|
+
renderRaceFinish,
|
|
33
|
+
renderRaceStatusReadouts,
|
|
34
|
+
renderStartLights,
|
|
35
|
+
} from './readouts/raceStatusRenderer.js';
|
|
36
|
+
import {
|
|
37
|
+
updateCameraControlButtons,
|
|
38
|
+
updateModeButtons,
|
|
39
|
+
updateToggleButtons,
|
|
40
|
+
} from './readouts/controlStateRenderer.js';
|
|
41
|
+
import {
|
|
42
|
+
createPenaltyStewardMessage,
|
|
43
|
+
createWarningStewardMessage,
|
|
44
|
+
renderActiveStewardMessage,
|
|
45
|
+
updateStewardMessageState,
|
|
46
|
+
} from './banners/stewardMessageController.js';
|
|
47
|
+
import {
|
|
48
|
+
hideRaceDataPanel,
|
|
49
|
+
getProjectRadioQuote,
|
|
50
|
+
RACE_DATA_SELECTED_VISIBLE_MS,
|
|
51
|
+
RADIO_SCHEDULE_CATCHUP_LIMIT_MS,
|
|
52
|
+
renderProjectRadio,
|
|
53
|
+
renderRaceData,
|
|
54
|
+
resetRaceDataBannerState,
|
|
55
|
+
scheduleRadioBreak,
|
|
56
|
+
scheduleRadioPopup,
|
|
57
|
+
shouldAutoHideActiveRaceData,
|
|
58
|
+
getNextRadioBreakTime,
|
|
59
|
+
} from './banners/raceDataBannerController.js';
|
|
60
|
+
import { runFrameLoopTick } from './runtime/frameLoop.js';
|
|
61
|
+
import { observeRuntimeVisibility, syncRuntimeTicker } from './runtime/runtimeVisibility.js';
|
|
62
|
+
import { TARGET_FRAME_MS, domUpdateIntervalForSpeed, timingUpdateIntervalForSpeed } from './runtime/runtimeTiming.js';
|
|
63
|
+
import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
64
|
+
|
|
65
|
+
const SIMULATION_SPEED_STEPS = [1, 2, 3, 4, 5, 10];
|
|
48
66
|
const DRS_DRAG_REDUCTION_PERCENT = 58;
|
|
49
67
|
|
|
50
|
-
const VEHICLE_OVERVIEW_FIELDS = [
|
|
51
|
-
['power', 'Power'],
|
|
52
|
-
['braking', 'Braking'],
|
|
53
|
-
['aero', 'Aero'],
|
|
54
|
-
['dragEfficiency', 'Drag'],
|
|
55
|
-
['mechanicalGrip', 'Grip'],
|
|
56
|
-
['weightControl', 'Weight'],
|
|
57
|
-
];
|
|
58
|
-
const DRIVER_OVERVIEW_FIELDS = [
|
|
59
|
-
['pace', 'Pace'],
|
|
60
|
-
['racecraft', 'Racecraft'],
|
|
61
|
-
['aggression', 'Aggression'],
|
|
62
|
-
['riskTolerance', 'Risk'],
|
|
63
|
-
['patience', 'Patience'],
|
|
64
|
-
['consistency', 'Consistency'],
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
const TINT_BY_COLOR = new Map();
|
|
68
|
-
|
|
69
68
|
function createMountTrackSeed() {
|
|
70
69
|
const values = new Uint32Array(1);
|
|
71
70
|
try {
|
|
@@ -76,71 +75,6 @@ function createMountTrackSeed() {
|
|
|
76
75
|
return (values[0] || Math.floor(Date.now() + performance.now() * 1000)) >>> 0;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
function escapeHtml(value) {
|
|
80
|
-
return String(value ?? '')
|
|
81
|
-
.replaceAll('&', '&')
|
|
82
|
-
.replaceAll('<', '<')
|
|
83
|
-
.replaceAll('>', '>')
|
|
84
|
-
.replaceAll('"', '"')
|
|
85
|
-
.replaceAll("'", ''');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function getTireClass(tire) {
|
|
89
|
-
return String(tire ?? 'M').toLowerCase();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function formatTelemetryTime(value) {
|
|
93
|
-
if (!Number.isFinite(value)) return '--';
|
|
94
|
-
const clamped = Math.max(0, value);
|
|
95
|
-
if (clamped >= 60) {
|
|
96
|
-
const minutes = Math.floor(clamped / 60);
|
|
97
|
-
const seconds = clamped - minutes * 60;
|
|
98
|
-
return `${minutes}:${seconds.toFixed(3).padStart(6, '0')}`;
|
|
99
|
-
}
|
|
100
|
-
return `${clamped.toFixed(3)}s`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function setPerformanceClass(node, status) {
|
|
104
|
-
['overall-best', 'personal-best', 'slower'].forEach((name) => {
|
|
105
|
-
node?.classList?.toggle?.(`is-${name}`, status === name);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function colorToTint(color) {
|
|
110
|
-
if (TINT_BY_COLOR.has(color)) return TINT_BY_COLOR.get(color);
|
|
111
|
-
const tint = Number.parseInt(String(color ?? '').replace('#', ''), 16);
|
|
112
|
-
const normalizedTint = Number.isFinite(tint) ? tint : 0xffffff;
|
|
113
|
-
TINT_BY_COLOR.set(color, normalizedTint);
|
|
114
|
-
return normalizedTint;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function smoothAngle(current, target, amount) {
|
|
118
|
-
if (!Number.isFinite(current)) return target;
|
|
119
|
-
let diff = ((target - current + Math.PI) % (Math.PI * 2)) - Math.PI;
|
|
120
|
-
if (diff < -Math.PI) diff += Math.PI * 2;
|
|
121
|
-
return current + diff * amount;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function destroyDisplayChildren(container) {
|
|
125
|
-
container?.removeChildren?.().forEach((child) => {
|
|
126
|
-
child.destroy?.({ children: true, texture: false, textureSource: false });
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function expertVisualizesRays(expertOptions) {
|
|
131
|
-
const setting = expertOptions?.visualizeSensors;
|
|
132
|
-
if (setting === true) return true;
|
|
133
|
-
if (!setting || setting === false) return false;
|
|
134
|
-
return Boolean(setting.rays);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function pointFromRay(origin, ray, distance) {
|
|
138
|
-
return {
|
|
139
|
-
x: origin.x + ray.x * distance,
|
|
140
|
-
y: origin.y + ray.y * distance,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
78
|
function hasOwnOption(options, key) {
|
|
145
79
|
return Object.hasOwn(options ?? {}, key);
|
|
146
80
|
}
|
|
@@ -149,6 +83,17 @@ function expertOptionsChanged(nextExpertOptions, currentExpertOptions) {
|
|
|
149
83
|
return nextExpertOptions !== currentExpertOptions;
|
|
150
84
|
}
|
|
151
85
|
|
|
86
|
+
function withSelectedExpertSensorDriver(visualizeSensors, selectedDriverId) {
|
|
87
|
+
if (!visualizeSensors) return visualizeSensors;
|
|
88
|
+
if (visualizeSensors === true) return { rays: true, selectedDriverId };
|
|
89
|
+
if (typeof visualizeSensors !== 'object') return visualizeSensors;
|
|
90
|
+
if (visualizeSensors.drivers === 'all' || Array.isArray(visualizeSensors.drivers)) return visualizeSensors;
|
|
91
|
+
return {
|
|
92
|
+
...visualizeSensors,
|
|
93
|
+
selectedDriverId,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
152
97
|
function assetSetsEqual(first = {}, second = {}) {
|
|
153
98
|
const firstTrackTextures = first.trackTextures ?? {};
|
|
154
99
|
const secondTrackTextures = second.trackTextures ?? {};
|
|
@@ -172,7 +117,7 @@ export class F1SimulatorApp {
|
|
|
172
117
|
this.abortController = new AbortController();
|
|
173
118
|
this.resizeHandler = null;
|
|
174
119
|
this.layoutResizeObserver = null;
|
|
175
|
-
this.
|
|
120
|
+
this.cameraController = null;
|
|
176
121
|
this.visibilityChangeHandler = null;
|
|
177
122
|
this.visibilityObserver = null;
|
|
178
123
|
this.tickerCallback = null;
|
|
@@ -185,15 +130,34 @@ export class F1SimulatorApp {
|
|
|
185
130
|
this.drsLayer = null;
|
|
186
131
|
this.trailLayer = null;
|
|
187
132
|
this.sensorLayer = null;
|
|
133
|
+
this.pitLaneStatusLayer = null;
|
|
134
|
+
this.replayGhostLayer = null;
|
|
188
135
|
this.carLayer = null;
|
|
189
136
|
this.textures = {};
|
|
190
137
|
this.carSprites = new Map();
|
|
191
138
|
this.carHitAreas = new Map();
|
|
139
|
+
this.serviceCountdownLabels = new Map();
|
|
140
|
+
this.carRenderer = new CarRenderer({
|
|
141
|
+
carSprites: this.carSprites,
|
|
142
|
+
carHitAreas: this.carHitAreas,
|
|
143
|
+
serviceCountdownLabels: this.serviceCountdownLabels,
|
|
144
|
+
onSelectCar: (driverId) => this.selectCar(driverId, { focus: true }),
|
|
145
|
+
});
|
|
192
146
|
this.drsTrails = new Map();
|
|
147
|
+
this.drsTrailRenderer = new DrsTrailRenderer({ trails: this.drsTrails });
|
|
148
|
+
this.replayGhostRenderer = new ReplayGhostRenderer();
|
|
149
|
+
this.pitLaneStatusRenderer = new PitLaneStatusRenderer();
|
|
193
150
|
this.trackSeed = options.trackSeed ?? createMountTrackSeed();
|
|
194
151
|
this.selectedId = this.drivers[0]?.id ?? null;
|
|
195
152
|
this.raceDataBannerConfig = options.ui?.raceDataBanners ?? { initial: 'project', enabled: ['project', 'radio'] };
|
|
153
|
+
this.raceDataBannersMuted = false;
|
|
154
|
+
this.penaltyBannerEnabled = Boolean(options.ui?.penaltyBanners);
|
|
155
|
+
this.timingPenaltyBadgesEnabled = Boolean(options.ui?.timingPenaltyBadges);
|
|
196
156
|
this.activeRaceDataId = null;
|
|
157
|
+
this.activePenaltyBanner = null;
|
|
158
|
+
this.lastPenaltyBannerId = null;
|
|
159
|
+
this.activeStewardMessage = null;
|
|
160
|
+
this.lastStewardMessageKey = null;
|
|
197
161
|
this.lastRaceDataInteraction = performance.now();
|
|
198
162
|
this.radioRandomState = (this.trackSeed ^ 0x9e3779b9) >>> 0;
|
|
199
163
|
this.radioState = {
|
|
@@ -203,20 +167,22 @@ export class F1SimulatorApp {
|
|
|
203
167
|
quoteIndex: 0,
|
|
204
168
|
};
|
|
205
169
|
this.resetRaceDataBannerState(this.lastRaceDataInteraction);
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
170
|
+
this.cameraController = new CameraController({
|
|
171
|
+
canvasHost: this.canvasHost,
|
|
172
|
+
readouts: this.readouts,
|
|
173
|
+
initialMode: options.initialCameraMode,
|
|
174
|
+
});
|
|
175
|
+
this.camera = this.cameraController.camera;
|
|
213
176
|
this.overviewMode = 'vehicle';
|
|
177
|
+
this.simulationSpeed = 1;
|
|
214
178
|
this.accumulator = 0;
|
|
215
179
|
this.lastTime = performance.now();
|
|
216
180
|
this.nextGameFrameTime = this.lastTime + TARGET_FRAME_MS;
|
|
217
181
|
this.lastDomUpdateTime = 0;
|
|
218
182
|
this.lastTimingRenderTime = 0;
|
|
219
183
|
this.lastTimingRaceMode = null;
|
|
184
|
+
this.lastTimingPenaltyKey = '';
|
|
185
|
+
this.lastTimingOrderKey = '';
|
|
220
186
|
this.lastTimingMarkup = null;
|
|
221
187
|
this.lastOverviewRenderKey = null;
|
|
222
188
|
this.lastFinishClassificationMarkup = null;
|
|
@@ -228,6 +194,7 @@ export class F1SimulatorApp {
|
|
|
228
194
|
current: 0,
|
|
229
195
|
lastSample: this.lastTime,
|
|
230
196
|
};
|
|
197
|
+
this.renderSnapshotBuffer = {};
|
|
231
198
|
this.lastLeaderLap = null;
|
|
232
199
|
this.emittedRaceEventKeys = new Set();
|
|
233
200
|
this.raceFinishEmitted = false;
|
|
@@ -259,27 +226,42 @@ export class F1SimulatorApp {
|
|
|
259
226
|
this.drsLayer = new Container();
|
|
260
227
|
this.trailLayer = new Graphics();
|
|
261
228
|
this.sensorLayer = new Graphics();
|
|
229
|
+
this.pitLaneStatusLayer = new Graphics();
|
|
230
|
+
this.replayGhostLayer = new Container();
|
|
262
231
|
this.carLayer = new Container();
|
|
263
232
|
this.app.stage.addChild(this.worldLayer);
|
|
264
233
|
|
|
265
234
|
await this.loadAssets();
|
|
266
235
|
this.trackAsset = new ProceduralTrackAsset({ textures: this.textures, world: WORLD });
|
|
267
|
-
this.worldLayer.addChild(
|
|
236
|
+
this.worldLayer.addChild(
|
|
237
|
+
this.trackAsset.container,
|
|
238
|
+
this.drsLayer,
|
|
239
|
+
this.trailLayer,
|
|
240
|
+
this.pitLaneStatusLayer,
|
|
241
|
+
this.sensorLayer,
|
|
242
|
+
this.replayGhostLayer,
|
|
243
|
+
this.carLayer,
|
|
244
|
+
);
|
|
268
245
|
this.createCars();
|
|
269
246
|
this.bindControls();
|
|
270
247
|
this.renderTrack();
|
|
271
248
|
const snapshot = this.sim.snapshot();
|
|
272
249
|
this.updateDom(snapshot);
|
|
250
|
+
this.lastDomUpdateTime = performance.now();
|
|
273
251
|
if (this.expertMode) {
|
|
274
252
|
this.expert = createBrowserExpertAdapter(this, this.options.expert);
|
|
275
253
|
}
|
|
254
|
+
this.renderInitialFrame(snapshot);
|
|
276
255
|
this.completeComponentLoading();
|
|
277
256
|
this.emitHostCallback('onLoadingChange', { loading: false, phase: 'ready' });
|
|
278
257
|
this.emitHostCallback('onReady', { snapshot });
|
|
279
|
-
this.resizeHandler = () => this.applyCamera(this.sim.snapshot());
|
|
258
|
+
this.resizeHandler = () => this.applyCamera(this.sim.snapshotRender?.() ?? this.sim.snapshot());
|
|
280
259
|
window.addEventListener('resize', this.resizeHandler, { signal: this.abortController.signal });
|
|
281
260
|
this.observeLayoutResize();
|
|
282
|
-
if (
|
|
261
|
+
if (this.expertMode) {
|
|
262
|
+
this.app.ticker.stop?.();
|
|
263
|
+
} else {
|
|
264
|
+
this.resetFrameClock();
|
|
283
265
|
this.tickerCallback = () => this.tick();
|
|
284
266
|
this.app.ticker.add(this.tickerCallback);
|
|
285
267
|
this.observeRuntimeVisibility();
|
|
@@ -320,82 +302,41 @@ export class F1SimulatorApp {
|
|
|
320
302
|
return createRaceSimulation({
|
|
321
303
|
seed: options.seed,
|
|
322
304
|
trackSeed: options.trackSeed ?? this.trackSeed,
|
|
305
|
+
trackGeneration: options.trackGeneration,
|
|
306
|
+
physicsMode: options.physicsMode,
|
|
323
307
|
drivers: options.drivers ?? this.drivers,
|
|
324
308
|
totalLaps: options.totalLaps,
|
|
309
|
+
rules: options.rules,
|
|
310
|
+
participantInteractions: options.participantInteractions,
|
|
311
|
+
replayGhosts: options.replayGhosts,
|
|
312
|
+
trackQueryIndex: options.trackQueryIndex,
|
|
313
|
+
warmup: normalizeWarmupOptions(options.warmup, 'browser'),
|
|
325
314
|
});
|
|
326
315
|
}
|
|
327
316
|
|
|
328
317
|
async loadAssets() {
|
|
329
|
-
this.textures
|
|
330
|
-
try {
|
|
331
|
-
this.textures.car = await Assets.load(this.assets.car);
|
|
332
|
-
this.textures.car.source.scaleMode = 'linear';
|
|
333
|
-
this.textures.car.source.autoGenerateMipmaps = true;
|
|
334
|
-
} catch {
|
|
335
|
-
this.textures.car = Texture.WHITE;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
this.textures.safetyCar = await Assets.load(this.assets.safetyCar);
|
|
340
|
-
this.textures.safetyCar.source.scaleMode = 'linear';
|
|
341
|
-
this.textures.safetyCar.source.autoGenerateMipmaps = true;
|
|
342
|
-
} catch {
|
|
343
|
-
this.textures.safetyCar = Texture.WHITE;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
await Promise.all(Object.entries(this.assets.trackTextures).map(async ([key, url]) => {
|
|
347
|
-
try {
|
|
348
|
-
const texture = await Assets.load(url);
|
|
349
|
-
texture.source.scaleMode = 'linear';
|
|
350
|
-
this.textures[key] = texture;
|
|
351
|
-
} catch {
|
|
352
|
-
this.textures[key] = Texture.WHITE;
|
|
353
|
-
}
|
|
354
|
-
}));
|
|
318
|
+
this.textures = await loadAppTextures(this.assets);
|
|
355
319
|
}
|
|
356
320
|
|
|
357
321
|
createCars() {
|
|
358
|
-
const texture = this.textures.car ?? Texture.WHITE;
|
|
359
|
-
const baseScale = Math.min(
|
|
360
|
-
CAR_WORLD_LENGTH / Math.max(texture.width, 1),
|
|
361
|
-
CAR_WORLD_WIDTH / Math.max(texture.height, 1),
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
this.carSprites.forEach((sprite) => sprite.destroy());
|
|
365
|
-
this.carHitAreas.forEach((hit) => hit.destroy());
|
|
366
|
-
this.carSprites.clear();
|
|
367
|
-
this.carHitAreas.clear();
|
|
368
322
|
this.drsTrails.clear();
|
|
323
|
+
this.carRenderer.createCars({
|
|
324
|
+
drivers: this.drivers,
|
|
325
|
+
textures: this.textures,
|
|
326
|
+
carLayer: this.carLayer,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
369
329
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
330
|
+
createRawCarGeometryGraphic(driver) {
|
|
331
|
+
return this.carRenderer.createRawCarGeometryGraphic(driver);
|
|
332
|
+
}
|
|
374
333
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
sprite.eventMode = 'static';
|
|
382
|
-
sprite.cursor = 'pointer';
|
|
383
|
-
sprite.on('pointerdown', () => {
|
|
384
|
-
this.selectCar(driver.id, { focus: true });
|
|
385
|
-
});
|
|
386
|
-
this.carSprites.set(driver.id, sprite);
|
|
387
|
-
this.carLayer.addChild(sprite);
|
|
388
|
-
|
|
389
|
-
const hit = new Graphics();
|
|
390
|
-
hit.circle(0, 0, 24).fill({ color: 0xffffff, alpha: 0.001 });
|
|
391
|
-
hit.eventMode = 'static';
|
|
392
|
-
hit.cursor = 'pointer';
|
|
393
|
-
hit.on('pointerdown', () => {
|
|
394
|
-
this.selectCar(driver.id, { focus: true });
|
|
395
|
-
});
|
|
396
|
-
this.carHitAreas.set(driver.id, hit);
|
|
397
|
-
this.carLayer.addChild(hit);
|
|
398
|
-
});
|
|
334
|
+
createServiceCountdownLabel() {
|
|
335
|
+
return this.carRenderer.createServiceCountdownLabel();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
applyServiceCountdownTone(label, tone) {
|
|
339
|
+
this.carRenderer.applyServiceCountdownTone(label, tone);
|
|
399
340
|
}
|
|
400
341
|
|
|
401
342
|
bindControls() {
|
|
@@ -417,27 +358,38 @@ export class F1SimulatorApp {
|
|
|
417
358
|
this.trailLayer?.clear();
|
|
418
359
|
this.lastTimingRenderTime = 0;
|
|
419
360
|
this.lastTimingRaceMode = null;
|
|
361
|
+
this.lastTimingPenaltyKey = '';
|
|
362
|
+
this.lastTimingOrderKey = '';
|
|
420
363
|
this.renderTrack();
|
|
421
364
|
}, eventOptions);
|
|
422
365
|
|
|
423
366
|
this.cameraButtons.forEach((button) => {
|
|
424
367
|
button.addEventListener('click', () => {
|
|
425
|
-
this.
|
|
426
|
-
if (CAMERA_PRESETS[this.camera.mode]) this.camera.zoom = CAMERA_PRESETS[this.camera.mode];
|
|
427
|
-
this.updateCameraControls();
|
|
368
|
+
this.setCameraMode(button.dataset.cameraMode);
|
|
428
369
|
}, eventOptions);
|
|
429
370
|
});
|
|
430
371
|
|
|
431
372
|
this.zoomInButton?.addEventListener('click', () => {
|
|
432
|
-
this.
|
|
433
|
-
this.updateCameraControls();
|
|
373
|
+
this.adjustCameraZoom(1);
|
|
434
374
|
}, eventOptions);
|
|
435
375
|
|
|
436
376
|
this.zoomOutButton?.addEventListener('click', () => {
|
|
437
|
-
this.
|
|
438
|
-
this.updateCameraControls();
|
|
377
|
+
this.adjustCameraZoom(-1);
|
|
439
378
|
}, eventOptions);
|
|
440
379
|
|
|
380
|
+
this.simulationSpeedButtons?.forEach((button) => {
|
|
381
|
+
button.addEventListener('click', () => {
|
|
382
|
+
this.cycleSimulationSpeed();
|
|
383
|
+
}, eventOptions);
|
|
384
|
+
});
|
|
385
|
+
this.updateSimulationSpeedControls();
|
|
386
|
+
|
|
387
|
+
this.bannerMuteButtons?.forEach((button) => {
|
|
388
|
+
button.addEventListener('click', () => {
|
|
389
|
+
this.setRaceDataBannersMuted(!this.raceDataBannersMuted);
|
|
390
|
+
}, eventOptions);
|
|
391
|
+
});
|
|
392
|
+
|
|
441
393
|
this.overviewModeButtons.forEach((button) => {
|
|
442
394
|
button.addEventListener('click', () => {
|
|
443
395
|
this.overviewMode = button.dataset.overviewMode === 'driver' ? 'driver' : 'vehicle';
|
|
@@ -451,7 +403,7 @@ export class F1SimulatorApp {
|
|
|
451
403
|
this.timingGapMode = button.dataset.timingGapMode === 'leader' ? 'leader' : 'interval';
|
|
452
404
|
this.syncTimingGapModeControls();
|
|
453
405
|
const snapshot = this.sim?.snapshot?.();
|
|
454
|
-
if (snapshot) this.renderTiming(snapshot.cars, snapshot.raceControl.mode);
|
|
406
|
+
if (snapshot) this.renderTiming(snapshot.cars, snapshot.raceControl.mode, snapshot.penalties ?? []);
|
|
455
407
|
}, eventOptions);
|
|
456
408
|
});
|
|
457
409
|
|
|
@@ -467,9 +419,56 @@ export class F1SimulatorApp {
|
|
|
467
419
|
this.options.onDriverOpen?.(driver);
|
|
468
420
|
}, eventOptions);
|
|
469
421
|
|
|
422
|
+
this.readouts.raceDataDismiss?.addEventListener('click', (event) => {
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
event.stopPropagation();
|
|
425
|
+
this.dismissRaceDataPanel(performance.now());
|
|
426
|
+
}, eventOptions);
|
|
427
|
+
|
|
470
428
|
this.readouts.telemetryDrawerToggle?.addEventListener('click', () => {
|
|
471
429
|
this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen);
|
|
472
430
|
}, eventOptions);
|
|
431
|
+
|
|
432
|
+
this.bindCameraWheelControls(eventOptions);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
setCameraMode(mode) {
|
|
436
|
+
const snapshot = this.sim?.snapshot?.();
|
|
437
|
+
if (!this.cameraController.setMode(mode, snapshot)) return false;
|
|
438
|
+
this.updateCameraControls(snapshot);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
adjustCameraZoom(direction) {
|
|
443
|
+
this.cameraController.adjustZoom(direction);
|
|
444
|
+
this.updateCameraControls();
|
|
445
|
+
return this.camera.zoom;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
cycleSimulationSpeed() {
|
|
449
|
+
const currentIndex = SIMULATION_SPEED_STEPS.indexOf(this.simulationSpeed);
|
|
450
|
+
const nextIndex = currentIndex >= 0
|
|
451
|
+
? (currentIndex + 1) % SIMULATION_SPEED_STEPS.length
|
|
452
|
+
: 0;
|
|
453
|
+
this.simulationSpeed = SIMULATION_SPEED_STEPS[nextIndex];
|
|
454
|
+
this.updateSimulationSpeedControls();
|
|
455
|
+
return this.simulationSpeed;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
updateSimulationSpeedControls() {
|
|
459
|
+
this.simulationSpeedButtons?.forEach((button) => {
|
|
460
|
+
const label = `${this.simulationSpeed}x`;
|
|
461
|
+
button.textContent = label;
|
|
462
|
+
button.setAttribute?.('aria-label', `Simulation speed ${label}`);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
bindCameraWheelControls(eventOptions) {
|
|
467
|
+
if (!this.canvasHost?.addEventListener) return;
|
|
468
|
+
this.canvasHost.addEventListener('wheel', (event) => {
|
|
469
|
+
event.preventDefault?.();
|
|
470
|
+
this.adjustCameraZoom(event.deltaY > 0 ? -1 : 1);
|
|
471
|
+
}, { ...eventOptions, passive: false });
|
|
473
472
|
}
|
|
474
473
|
|
|
475
474
|
setTelemetryDrawerOpen(open) {
|
|
@@ -487,401 +486,204 @@ export class F1SimulatorApp {
|
|
|
487
486
|
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close telemetry' : 'Telemetry';
|
|
488
487
|
}
|
|
489
488
|
this.invalidateCameraSafeArea();
|
|
489
|
+
this.syncRendererToCurrentLayout({ render: true });
|
|
490
|
+
this.scheduleLayoutResizeSync(380);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
setRaceDataBannersMuted(muted, now = performance.now()) {
|
|
494
|
+
this.raceDataBannersMuted = Boolean(muted);
|
|
495
|
+
if (this.raceDataBannersMuted) {
|
|
496
|
+
this.activeRaceDataId = null;
|
|
497
|
+
this.radioState.visible = false;
|
|
498
|
+
this.radioState.nextChangeAt = Number.POSITIVE_INFINITY;
|
|
499
|
+
this.hideRaceDataPanel();
|
|
500
|
+
} else {
|
|
501
|
+
this.resetRaceDataBannerState(now);
|
|
502
|
+
}
|
|
503
|
+
this.updateBannerMuteControls();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
resizeRendererToCanvasHost() {
|
|
507
|
+
if (!this.app?.renderer?.resize || !this.canvasHost) return;
|
|
508
|
+
const width = Math.max(1, Math.round(this.canvasHost.clientWidth || 0));
|
|
509
|
+
const height = Math.max(1, Math.round(this.canvasHost.clientHeight || 0));
|
|
510
|
+
this.app.renderer.resize(width, height);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
renderInitialFrame(snapshot) {
|
|
514
|
+
const renderSnapshot = interpolateRenderSnapshotInto(this.renderSnapshotBuffer, snapshot, 0);
|
|
515
|
+
this.resizeRendererToCanvasHost();
|
|
516
|
+
this.applyCamera(renderSnapshot, { immediate: true });
|
|
517
|
+
this.renderDrsTrails(renderSnapshot);
|
|
518
|
+
this.renderPitLaneStatus(renderSnapshot);
|
|
519
|
+
this.renderCars(renderSnapshot);
|
|
520
|
+
this.app?.render?.();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
syncRendererToCurrentLayout({ render = false } = {}) {
|
|
524
|
+
this.resizeRendererToCanvasHost();
|
|
525
|
+
const snapshot = this.sim?.snapshotRender?.() ?? this.sim?.snapshot?.();
|
|
526
|
+
if (!snapshot) return;
|
|
527
|
+
const renderSnapshot = interpolateRenderSnapshotInto(
|
|
528
|
+
this.renderSnapshotBuffer,
|
|
529
|
+
snapshot,
|
|
530
|
+
clamp(this.accumulator / FIXED_STEP, 0, 1),
|
|
531
|
+
);
|
|
532
|
+
this.applyCamera(renderSnapshot);
|
|
533
|
+
if (render) {
|
|
534
|
+
this.renderDrsTrails(renderSnapshot);
|
|
535
|
+
this.renderPitLaneStatus(renderSnapshot);
|
|
536
|
+
this.renderCars(renderSnapshot);
|
|
537
|
+
this.app?.render?.();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
scheduleLayoutResizeSync(durationMs = 360) {
|
|
542
|
+
if (typeof requestAnimationFrame !== 'function') return;
|
|
543
|
+
const startedAt = performance.now();
|
|
544
|
+
const run = (timestamp = performance.now()) => {
|
|
545
|
+
this.syncRendererToCurrentLayout({ render: true });
|
|
546
|
+
if (timestamp - startedAt < durationMs) {
|
|
547
|
+
this.layoutResizeFrame = requestAnimationFrame(run);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
this.layoutResizeFrame = requestAnimationFrame(run);
|
|
490
551
|
}
|
|
491
552
|
|
|
492
553
|
observeLayoutResize() {
|
|
493
554
|
if (typeof ResizeObserver !== 'function') return;
|
|
494
555
|
this.layoutResizeObserver?.disconnect?.();
|
|
495
|
-
this.layoutResizeObserver = new ResizeObserver(() =>
|
|
556
|
+
this.layoutResizeObserver = new ResizeObserver(() => {
|
|
557
|
+
this.invalidateCameraSafeArea();
|
|
558
|
+
this.syncRendererToCurrentLayout({ render: true });
|
|
559
|
+
});
|
|
496
560
|
if (this.canvasHost) this.layoutResizeObserver.observe(this.canvasHost);
|
|
497
561
|
if (this.readouts.timingTower) this.layoutResizeObserver.observe(this.readouts.timingTower);
|
|
498
562
|
}
|
|
499
563
|
|
|
500
564
|
invalidateCameraSafeArea() {
|
|
501
|
-
this.
|
|
565
|
+
this.cameraController.invalidateSafeArea();
|
|
502
566
|
}
|
|
503
567
|
|
|
504
568
|
observeRuntimeVisibility() {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (typeof IntersectionObserver === 'function') {
|
|
508
|
-
this.visibilityObserver?.disconnect?.();
|
|
509
|
-
this.visibilityObserver = new IntersectionObserver((entries) => {
|
|
510
|
-
const entry = entries.find((item) => item.target === this.canvasHost) ?? entries[0];
|
|
511
|
-
this.runtimeViewportVisible = Boolean(entry?.isIntersecting || entry?.intersectionRatio > 0);
|
|
512
|
-
this.syncRuntimeTicker();
|
|
513
|
-
}, {
|
|
514
|
-
root: null,
|
|
515
|
-
rootMargin: '240px 0px',
|
|
516
|
-
threshold: 0,
|
|
517
|
-
});
|
|
518
|
-
this.visibilityObserver.observe(this.canvasHost);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (typeof document !== 'undefined') {
|
|
522
|
-
this.visibilityChangeHandler = () => {
|
|
523
|
-
this.runtimeDocumentVisible = document.visibilityState !== 'hidden';
|
|
524
|
-
this.syncRuntimeTicker();
|
|
525
|
-
};
|
|
526
|
-
document.addEventListener('visibilitychange', this.visibilityChangeHandler, {
|
|
527
|
-
signal: this.abortController.signal,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
this.syncRuntimeTicker();
|
|
569
|
+
observeRuntimeVisibility(this);
|
|
532
570
|
}
|
|
533
571
|
|
|
534
572
|
syncRuntimeTicker() {
|
|
535
|
-
|
|
536
|
-
if (!this.app?.ticker) return;
|
|
537
|
-
|
|
538
|
-
const shouldRun = this.runtimeViewportVisible && this.runtimeDocumentVisible;
|
|
539
|
-
if (shouldRun === this.runtimeTickerRunning) return;
|
|
540
|
-
|
|
541
|
-
this.runtimeTickerRunning = shouldRun;
|
|
542
|
-
if (shouldRun) {
|
|
543
|
-
this.resetFrameClock();
|
|
544
|
-
this.app.ticker.start?.();
|
|
545
|
-
} else {
|
|
546
|
-
this.app.ticker.stop?.();
|
|
547
|
-
}
|
|
573
|
+
syncRuntimeTicker(this);
|
|
548
574
|
}
|
|
549
575
|
|
|
550
576
|
resetFrameClock(now = performance.now()) {
|
|
551
577
|
this.accumulator = 0;
|
|
552
578
|
this.lastTime = now;
|
|
553
579
|
this.nextGameFrameTime = now + TARGET_FRAME_MS;
|
|
580
|
+
this.fps.current = 0;
|
|
554
581
|
this.fps.frames = 0;
|
|
555
582
|
this.fps.lastSample = now;
|
|
556
583
|
}
|
|
557
584
|
|
|
558
585
|
tick() {
|
|
559
|
-
|
|
560
|
-
const now = performance.now();
|
|
561
|
-
if (now < this.nextGameFrameTime - FRAME_PACING_EPSILON_MS) return;
|
|
562
|
-
|
|
563
|
-
const elapsedFrameCount = clamp(
|
|
564
|
-
Math.floor((now - this.nextGameFrameTime + FRAME_PACING_EPSILON_MS) / TARGET_FRAME_MS) + 1,
|
|
565
|
-
1,
|
|
566
|
-
MAX_FRAME_CATCHUP_COUNT,
|
|
567
|
-
);
|
|
568
|
-
const frameSeconds = (TARGET_FRAME_MS * elapsedFrameCount) / 1000;
|
|
569
|
-
this.nextGameFrameTime += TARGET_FRAME_MS * elapsedFrameCount;
|
|
570
|
-
if (now - this.nextGameFrameTime > TARGET_FRAME_MS * MAX_FRAME_CATCHUP_COUNT) {
|
|
571
|
-
this.nextGameFrameTime = now + TARGET_FRAME_MS;
|
|
572
|
-
}
|
|
573
|
-
this.lastTime = now;
|
|
574
|
-
this.sampleFps(now);
|
|
575
|
-
this.accumulator += frameSeconds * SIM_SPEED;
|
|
576
|
-
|
|
577
|
-
let simulationSteps = 0;
|
|
578
|
-
while (this.accumulator >= FIXED_STEP && simulationSteps < MAX_SIMULATION_STEPS_PER_RENDER) {
|
|
579
|
-
this.sim.step(FIXED_STEP);
|
|
580
|
-
const stepSnapshot = this.sim.snapshot();
|
|
581
|
-
this.emitSnapshotLifecycle(stepSnapshot);
|
|
582
|
-
this.accumulator -= FIXED_STEP;
|
|
583
|
-
simulationSteps += 1;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (this.accumulator >= FIXED_STEP) {
|
|
587
|
-
this.accumulator %= FIXED_STEP;
|
|
588
|
-
this.nextGameFrameTime = now + TARGET_FRAME_MS;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const snapshot = this.sim.snapshot();
|
|
592
|
-
const renderSnapshot = createRenderSnapshot(snapshot, clamp(this.accumulator / FIXED_STEP, 0, 1));
|
|
593
|
-
this.applyCamera(renderSnapshot);
|
|
594
|
-
this.renderDrsTrails(renderSnapshot);
|
|
595
|
-
this.renderCars(renderSnapshot);
|
|
596
|
-
if (now - this.lastDomUpdateTime >= DOM_UPDATE_INTERVAL_MS) {
|
|
597
|
-
this.updateDom(snapshot, { emitLifecycle: false });
|
|
598
|
-
this.lastDomUpdateTime = now;
|
|
599
|
-
}
|
|
586
|
+
runFrameLoopTick(this);
|
|
600
587
|
}
|
|
601
588
|
|
|
602
|
-
renderExpertFrame(snapshot = this.sim?.snapshot(), { observation } = {}) {
|
|
589
|
+
renderExpertFrame(snapshot = this.sim?.snapshot(), { forceDomUpdate = false, observation } = {}) {
|
|
603
590
|
if (!snapshot) return;
|
|
604
|
-
const
|
|
591
|
+
const now = performance.now();
|
|
592
|
+
const renderSnapshot = interpolateRenderSnapshotInto(this.renderSnapshotBuffer, snapshot, 0);
|
|
605
593
|
this.applyCamera(renderSnapshot);
|
|
606
594
|
this.renderDrsTrails(renderSnapshot);
|
|
607
595
|
this.renderExpertSensorRays(renderSnapshot, observation);
|
|
596
|
+
this.renderPitLaneStatus(renderSnapshot);
|
|
608
597
|
this.renderCars(renderSnapshot);
|
|
609
|
-
this.
|
|
610
|
-
this.lastDomUpdateTime
|
|
598
|
+
this.app?.render?.();
|
|
599
|
+
if (forceDomUpdate || now - this.lastDomUpdateTime >= domUpdateIntervalForSpeed(this.simulationSpeed)) {
|
|
600
|
+
this.updateDom(snapshot, { emitLifecycle: false });
|
|
601
|
+
this.lastDomUpdateTime = now;
|
|
602
|
+
}
|
|
611
603
|
}
|
|
612
604
|
|
|
613
605
|
renderTrack() {
|
|
614
|
-
|
|
615
|
-
this.
|
|
606
|
+
this.pitLaneStatusRenderer.reset();
|
|
607
|
+
this.cameraController.invalidateTrackCaches();
|
|
616
608
|
const snapshot = this.sim.snapshot();
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const steps = 44;
|
|
624
|
-
for (let index = 0; index <= steps; index += 1) {
|
|
625
|
-
const basePoint = pointAt(track, zone.start + ((zone.end - zone.start) * index) / steps);
|
|
626
|
-
const point = offsetTrackPoint(basePoint, track.width / 2 - 22);
|
|
627
|
-
if (index === 0) zoneLine.moveTo(point.x, point.y);
|
|
628
|
-
else zoneLine.lineTo(point.x, point.y);
|
|
629
|
-
}
|
|
630
|
-
zoneLine.stroke({ width: 8, color: 0x14c784, alpha: 0.5, join: 'round', cap: 'round' });
|
|
631
|
-
this.drsLayer.addChild(zoneLine);
|
|
609
|
+
renderTrackSurface({
|
|
610
|
+
drsLayer: this.drsLayer,
|
|
611
|
+
sensorLayer: this.sensorLayer,
|
|
612
|
+
pitLaneStatusLayer: this.pitLaneStatusLayer,
|
|
613
|
+
trackAsset: this.trackAsset,
|
|
614
|
+
snapshot,
|
|
632
615
|
});
|
|
633
616
|
}
|
|
634
617
|
|
|
618
|
+
renderPitLaneStatus(snapshot) {
|
|
619
|
+
this.pitLaneStatusRenderer.render(snapshot, this.pitLaneStatusLayer);
|
|
620
|
+
}
|
|
621
|
+
|
|
635
622
|
renderCars(snapshot) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (!sprite || !hit) return;
|
|
640
|
-
sprite.x = car.x;
|
|
641
|
-
sprite.y = car.y;
|
|
642
|
-
sprite.currentRotation = smoothAngle(sprite.currentRotation, car.heading, 0.24);
|
|
643
|
-
sprite.rotation = sprite.currentRotation;
|
|
644
|
-
sprite.alpha = snapshot.raceControl.mode === 'safety-car' ? 0.82 : 1;
|
|
645
|
-
sprite.scale.set(sprite.baseScale);
|
|
646
|
-
sprite.tint = colorToTint(car.color);
|
|
647
|
-
hit.x = car.x;
|
|
648
|
-
hit.y = car.y;
|
|
623
|
+
this.replayGhostRenderer.render(snapshot, {
|
|
624
|
+
textures: this.textures,
|
|
625
|
+
replayGhostLayer: this.replayGhostLayer,
|
|
649
626
|
});
|
|
627
|
+
this.carRenderer.renderCars(snapshot, {
|
|
628
|
+
textures: this.textures,
|
|
629
|
+
carLayer: this.carLayer,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
650
632
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
this.safetySprite = new Sprite(texture);
|
|
654
|
-
this.safetySprite.anchor.set(0.5);
|
|
655
|
-
this.safetySprite.baseScale = Math.min(
|
|
656
|
-
SAFETY_CAR_WORLD_LENGTH / Math.max(texture.width, 1),
|
|
657
|
-
SAFETY_CAR_WORLD_WIDTH / Math.max(texture.height, 1),
|
|
658
|
-
);
|
|
659
|
-
this.safetySprite.scale.set(this.safetySprite.baseScale);
|
|
660
|
-
this.carLayer.addChild(this.safetySprite);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (this.safetySprite) {
|
|
664
|
-
this.safetySprite.visible = snapshot.safetyCar.deployed;
|
|
665
|
-
this.safetySprite.x = snapshot.safetyCar.x;
|
|
666
|
-
this.safetySprite.y = snapshot.safetyCar.y;
|
|
667
|
-
this.safetySprite.rotation = snapshot.safetyCar.heading;
|
|
668
|
-
}
|
|
633
|
+
renderServiceCountdownLabel(car) {
|
|
634
|
+
this.carRenderer.renderServiceCountdownLabel(car);
|
|
669
635
|
}
|
|
670
636
|
|
|
671
637
|
renderExpertSensorRays(snapshot, observation) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const car = carsById.get(driverId);
|
|
682
|
-
const rays = observation?.[driverId]?.object?.rays;
|
|
683
|
-
if (!car || !Array.isArray(rays) || rays.length === 0) return;
|
|
684
|
-
|
|
685
|
-
const origin = getCarRayOrigin(car);
|
|
686
|
-
|
|
687
|
-
rays.forEach((ray) => {
|
|
688
|
-
const rayVector = getCarRayVector(car, Number(ray.angleDegrees) || 0);
|
|
689
|
-
const totalDistanceMeters = Math.max(0, Number(ray.lengthMeters) || 0);
|
|
690
|
-
const trackDistanceMeters = Math.max(0, Number(ray.track?.distanceMeters) || totalDistanceMeters);
|
|
691
|
-
const carDistanceMeters = Math.max(0, Number(ray.car?.distanceMeters) || totalDistanceMeters);
|
|
692
|
-
const trackHit = Boolean(ray.track?.hit) && trackDistanceMeters <= totalDistanceMeters;
|
|
693
|
-
const carHit = Boolean(ray.car?.hit) && carDistanceMeters <= totalDistanceMeters;
|
|
694
|
-
const fullEnd = pointFromRay(origin, rayVector, metersToSimUnits(totalDistanceMeters));
|
|
695
|
-
|
|
696
|
-
this.sensorLayer
|
|
697
|
-
.moveTo(origin.x, origin.y)
|
|
698
|
-
.lineTo(fullEnd.x, fullEnd.y)
|
|
699
|
-
.stroke({ width: 2, color: 0xffffff, alpha: 0.18, cap: 'round' });
|
|
700
|
-
|
|
701
|
-
if (!trackHit && !carHit) return;
|
|
702
|
-
|
|
703
|
-
const carIsClosest = carHit && (!trackHit || carDistanceMeters <= trackDistanceMeters);
|
|
704
|
-
const hitDistanceMeters = carIsClosest ? carDistanceMeters : trackDistanceMeters;
|
|
705
|
-
const hitEnd = pointFromRay(origin, rayVector, metersToSimUnits(hitDistanceMeters));
|
|
706
|
-
const hitColor = carIsClosest
|
|
707
|
-
? SENSOR_RAY_CAR_COLOR
|
|
708
|
-
: ray.track?.kind === 'entry'
|
|
709
|
-
? SENSOR_RAY_TRACK_ENTRY_COLOR
|
|
710
|
-
: SENSOR_RAY_TRACK_COLOR;
|
|
711
|
-
|
|
712
|
-
this.sensorLayer
|
|
713
|
-
.moveTo(origin.x, origin.y)
|
|
714
|
-
.lineTo(hitEnd.x, hitEnd.y)
|
|
715
|
-
.stroke({ width: carIsClosest ? 5 : 3.4, color: hitColor, alpha: carIsClosest ? 0.9 : 0.76, cap: 'round' });
|
|
716
|
-
this.sensorLayer
|
|
717
|
-
.circle(hitEnd.x, hitEnd.y, carIsClosest ? 7 : 5)
|
|
718
|
-
.fill({ color: hitColor, alpha: 0.92 })
|
|
719
|
-
.circle(hitEnd.x, hitEnd.y, carIsClosest ? 10 : 8)
|
|
720
|
-
.stroke({ width: 1.5, color: 0x10131a, alpha: 0.72 });
|
|
721
|
-
});
|
|
638
|
+
renderExpertSensorRays({
|
|
639
|
+
snapshot,
|
|
640
|
+
observation,
|
|
641
|
+
sensorLayer: this.sensorLayer,
|
|
642
|
+
expertMode: this.expertMode,
|
|
643
|
+
expertOptions: {
|
|
644
|
+
...this.options.expert,
|
|
645
|
+
visualizeSensors: withSelectedExpertSensorDriver(this.options.expert?.visualizeSensors, this.selectedId),
|
|
646
|
+
},
|
|
722
647
|
});
|
|
723
648
|
}
|
|
724
649
|
|
|
725
650
|
renderDrsTrails(snapshot) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
snapshot.cars.forEach((car) => {
|
|
729
|
-
const history = this.drsTrails.get(car.id) ?? [];
|
|
730
|
-
const rear = {
|
|
731
|
-
x: car.x - Math.cos(car.heading) * CAR_WORLD_LENGTH * 0.46,
|
|
732
|
-
y: car.y - Math.sin(car.heading) * CAR_WORLD_LENGTH * 0.46,
|
|
733
|
-
at: snapshot.time,
|
|
734
|
-
};
|
|
735
|
-
const last = history[history.length - 1];
|
|
736
|
-
|
|
737
|
-
if (
|
|
738
|
-
car.drsActive &&
|
|
739
|
-
(!last || Math.hypot(rear.x - last.x, rear.y - last.y) >= DRS_TRAIL_MIN_DISTANCE)
|
|
740
|
-
) {
|
|
741
|
-
history.push(rear);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const activeTrail = history.filter((point) => snapshot.time - point.at <= DRS_TRAIL_TTL);
|
|
745
|
-
if (activeTrail.length) this.drsTrails.set(car.id, activeTrail);
|
|
746
|
-
else this.drsTrails.delete(car.id);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
this.trailLayer.clear();
|
|
750
|
-
this.drsTrails.forEach((history) => {
|
|
751
|
-
for (let index = 1; index < history.length; index += 1) {
|
|
752
|
-
const previous = history[index - 1];
|
|
753
|
-
const point = history[index];
|
|
754
|
-
const age = snapshot.time - point.at;
|
|
755
|
-
const life = clamp(1 - age / DRS_TRAIL_TTL, 0, 1);
|
|
756
|
-
this.trailLayer.moveTo(previous.x, previous.y);
|
|
757
|
-
this.trailLayer.lineTo(point.x, point.y);
|
|
758
|
-
this.trailLayer.stroke({
|
|
759
|
-
width: 5 + life * 7,
|
|
760
|
-
color: 0x3be8ff,
|
|
761
|
-
alpha: 0.22 + life * 0.48,
|
|
762
|
-
cap: 'round',
|
|
763
|
-
join: 'round',
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
});
|
|
651
|
+
this.drsTrailRenderer.render(snapshot, this.trailLayer);
|
|
767
652
|
}
|
|
768
653
|
|
|
769
|
-
applyCamera(snapshot) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const safeArea = this.getCameraSafeArea(width);
|
|
775
|
-
const baseScale = Math.min(safeArea.width / (WORLD.width + 260), height / (WORLD.height + 220));
|
|
776
|
-
const frame = this.getCameraFrame(snapshot, width, height, baseScale, safeArea);
|
|
777
|
-
const scale = frame.scale;
|
|
778
|
-
const target = frame.target;
|
|
779
|
-
this.camera.x += (target.x - this.camera.x) * 0.08;
|
|
780
|
-
this.camera.y += (target.y - this.camera.y) * 0.08;
|
|
781
|
-
const activeScale = this.camera.scale === null
|
|
782
|
-
? scale
|
|
783
|
-
: this.camera.scale + (scale - this.camera.scale) * 0.12;
|
|
784
|
-
this.camera.scale = activeScale;
|
|
785
|
-
this.worldLayer.scale.set(activeScale);
|
|
786
|
-
this.worldLayer.position.set(
|
|
787
|
-
frame.screenX - this.camera.x * activeScale,
|
|
788
|
-
frame.screenY - this.camera.y * activeScale,
|
|
789
|
-
);
|
|
654
|
+
applyCamera(snapshot, { immediate = false } = {}) {
|
|
655
|
+
this.cameraController.applyToWorldLayer(this.worldLayer, snapshot, {
|
|
656
|
+
immediate,
|
|
657
|
+
selectedId: this.selectedId,
|
|
658
|
+
});
|
|
790
659
|
}
|
|
791
660
|
|
|
792
661
|
getCameraSafeArea(width) {
|
|
793
|
-
|
|
794
|
-
return this.cameraSafeAreaCache.safeArea;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const canvasRect = this.canvasHost?.getBoundingClientRect?.();
|
|
798
|
-
const towerRect = this.readouts.timingTower?.getBoundingClientRect?.();
|
|
799
|
-
if (!canvasRect || !towerRect) {
|
|
800
|
-
const safeArea = { left: 0, width };
|
|
801
|
-
this.cameraSafeAreaCache = { width, safeArea };
|
|
802
|
-
return safeArea;
|
|
803
|
-
}
|
|
804
|
-
const overlapsCanvasHorizontally = towerRect.right > canvasRect.left && towerRect.left < canvasRect.right;
|
|
805
|
-
const hasVerticalBounds = Number.isFinite(canvasRect.top) &&
|
|
806
|
-
Number.isFinite(canvasRect.bottom) &&
|
|
807
|
-
Number.isFinite(towerRect.top) &&
|
|
808
|
-
Number.isFinite(towerRect.bottom);
|
|
809
|
-
const overlapsCanvasVertically = !hasVerticalBounds ||
|
|
810
|
-
(towerRect.bottom > canvasRect.top && towerRect.top < canvasRect.bottom);
|
|
811
|
-
const canvasWidth = Math.max(1, canvasRect.right - canvasRect.left || width);
|
|
812
|
-
const towerWidth = Math.max(0, towerRect.right - towerRect.left);
|
|
813
|
-
const isSideGutter = towerWidth < canvasWidth * 0.6;
|
|
814
|
-
if (!overlapsCanvasHorizontally || !overlapsCanvasVertically || !isSideGutter) {
|
|
815
|
-
const safeArea = { left: 0, width };
|
|
816
|
-
this.cameraSafeAreaCache = { width, safeArea };
|
|
817
|
-
return safeArea;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const overlayGap = 16;
|
|
821
|
-
const reservedLeft = clamp(towerRect.right - canvasRect.left + overlayGap, 0, width * 0.48);
|
|
822
|
-
const safeArea = {
|
|
823
|
-
left: reservedLeft,
|
|
824
|
-
width: Math.max(1, width - reservedLeft),
|
|
825
|
-
};
|
|
826
|
-
this.cameraSafeAreaCache = { width, safeArea };
|
|
827
|
-
return safeArea;
|
|
662
|
+
return this.cameraController.getSafeArea(width);
|
|
828
663
|
}
|
|
829
664
|
|
|
830
665
|
getCameraFrame(snapshot, width, height, baseScale, safeArea = { left: 0, width }) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (this.camera.mode === 'show-all') {
|
|
834
|
-
const bounds = snapshot.cars.reduce((box, car) => ({
|
|
835
|
-
minX: Math.min(box.minX, car.x),
|
|
836
|
-
minY: Math.min(box.minY, car.y),
|
|
837
|
-
maxX: Math.max(box.maxX, car.x),
|
|
838
|
-
maxY: Math.max(box.maxY, car.y),
|
|
839
|
-
}), {
|
|
840
|
-
minX: Infinity,
|
|
841
|
-
minY: Infinity,
|
|
842
|
-
maxX: -Infinity,
|
|
843
|
-
maxY: -Infinity,
|
|
844
|
-
});
|
|
845
|
-
const target = {
|
|
846
|
-
x: (bounds.minX + bounds.maxX) / 2,
|
|
847
|
-
y: (bounds.minY + bounds.maxY) / 2,
|
|
848
|
-
};
|
|
849
|
-
const fitWidth = Math.max(CAR_WORLD_LENGTH * 3, bounds.maxX - bounds.minX + SHOW_ALL_PADDING);
|
|
850
|
-
const fitHeight = Math.max(CAR_WORLD_LENGTH * 3, bounds.maxY - bounds.minY + SHOW_ALL_PADDING);
|
|
851
|
-
const safeHeight = Math.max(height * 0.48, height - SHOW_ALL_TOP_RESERVED - SHOW_ALL_BOTTOM_RESERVED);
|
|
852
|
-
const scale = clamp(
|
|
853
|
-
Math.min(safeArea.width / fitWidth, safeHeight / fitHeight),
|
|
854
|
-
baseScale * SHOW_ALL_MIN_ZOOM,
|
|
855
|
-
baseScale * SHOW_ALL_MAX_ZOOM,
|
|
856
|
-
);
|
|
857
|
-
return {
|
|
858
|
-
target,
|
|
859
|
-
scale,
|
|
860
|
-
screenX: screenCenterX,
|
|
861
|
-
screenY: SHOW_ALL_TOP_RESERVED + safeHeight / 2,
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return {
|
|
866
|
-
target: this.getCameraTarget(snapshot),
|
|
867
|
-
scale: baseScale * this.camera.zoom,
|
|
868
|
-
screenX: screenCenterX,
|
|
869
|
-
screenY: height / 2,
|
|
870
|
-
};
|
|
666
|
+
return this.cameraController.getFrame(snapshot, width, height, baseScale, safeArea, this.selectedId);
|
|
871
667
|
}
|
|
872
668
|
|
|
873
669
|
getCameraTarget(snapshot) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
670
|
+
return this.cameraController.getTarget(snapshot, this.selectedId);
|
|
671
|
+
}
|
|
877
672
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
673
|
+
getTrackCameraBounds(track) {
|
|
674
|
+
return this.cameraController.getTrackBounds(track);
|
|
675
|
+
}
|
|
882
676
|
|
|
883
|
-
|
|
884
|
-
return
|
|
677
|
+
getCameraBoundsFitScale(bounds, height, safeArea) {
|
|
678
|
+
return this.cameraController.getBoundsFitScale(bounds, height, safeArea);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
getPitCameraBounds(pitLane) {
|
|
682
|
+
return this.cameraController.getPitBounds(pitLane);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
getPitCameraFrame(pitLane, height, baseScale, safeArea, screenCenterX, minimumScale = null) {
|
|
686
|
+
return this.cameraController.getPitFrame(pitLane, height, baseScale, safeArea, screenCenterX, minimumScale);
|
|
885
687
|
}
|
|
886
688
|
|
|
887
689
|
updateDom(snapshot, { emitLifecycle = true } = {}) {
|
|
@@ -893,69 +695,50 @@ export class F1SimulatorApp {
|
|
|
893
695
|
|
|
894
696
|
if (emitLifecycle) this.emitSnapshotLifecycle(snapshot);
|
|
895
697
|
|
|
896
|
-
if (
|
|
698
|
+
if (
|
|
699
|
+
this.shouldAutoHideActiveRaceData() &&
|
|
700
|
+
now - this.lastRaceDataInteraction > RACE_DATA_SELECTED_VISIBLE_MS
|
|
701
|
+
) {
|
|
897
702
|
this.activeRaceDataId = null;
|
|
898
703
|
if (this.isRaceDataBannerEnabled('radio')) this.scheduleRadioBreak(now);
|
|
899
704
|
}
|
|
705
|
+
this.updateStewardMessageState(snapshot, now);
|
|
900
706
|
|
|
901
|
-
if (this.readouts.mode) {
|
|
902
|
-
const modeLabel = snapshot.raceControl.mode === 'safety-car'
|
|
903
|
-
? 'SC'
|
|
904
|
-
: snapshot.raceControl.finished
|
|
905
|
-
? 'FINISH'
|
|
906
|
-
: 'GREEN';
|
|
907
|
-
this.readouts.mode.textContent = modeLabel;
|
|
908
|
-
this.readouts.mode.style.color = snapshot.raceControl.mode === 'safety-car'
|
|
909
|
-
? 'var(--yellow)'
|
|
910
|
-
: snapshot.raceControl.mode === 'finished'
|
|
911
|
-
? 'var(--red)'
|
|
912
|
-
: 'var(--green)';
|
|
913
|
-
}
|
|
914
707
|
this.syncSafetyCarControls(snapshot.raceControl.mode === 'safety-car');
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
this.
|
|
920
|
-
this.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
this.
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
this.readouts.drs.textContent = snapshot.raceControl.mode === 'safety-car'
|
|
927
|
-
? 'DISABLED'
|
|
928
|
-
: activeDrs
|
|
929
|
-
? `${activeDrs} OPEN`
|
|
930
|
-
: 'ARMED';
|
|
931
|
-
}
|
|
932
|
-
if (this.readouts.contacts) this.readouts.contacts.textContent = String(contactCount);
|
|
933
|
-
this.renderStartLights(snapshot.raceControl);
|
|
934
|
-
if (this.readouts.camera) {
|
|
935
|
-
const zoom = this.camera.mode === 'show-all'
|
|
936
|
-
? Math.round(((this.camera.scale ?? 0) / Math.max(0.0001, this.getBaseScale())) * 100)
|
|
937
|
-
: Math.round(this.camera.zoom * 100);
|
|
938
|
-
this.readouts.camera.textContent = `${this.camera.mode.toUpperCase().replace('-', ' ')} ${zoom}%`;
|
|
939
|
-
}
|
|
940
|
-
if (this.readouts.fps) {
|
|
941
|
-
this.readouts.fps.textContent = this.fps.current ? `${this.fps.current}` : '--';
|
|
942
|
-
}
|
|
943
|
-
this.renderRaceFinish(snapshot);
|
|
708
|
+
this.lastFinishClassificationMarkup = renderRaceStatusReadouts({
|
|
709
|
+
readouts: this.readouts,
|
|
710
|
+
startLightNodes: this.startLightNodes,
|
|
711
|
+
snapshot,
|
|
712
|
+
camera: this.camera,
|
|
713
|
+
fps: this.fps,
|
|
714
|
+
activeDrs,
|
|
715
|
+
contactCount,
|
|
716
|
+
driverById: this.driverById,
|
|
717
|
+
lastFinishClassificationMarkup: this.lastFinishClassificationMarkup,
|
|
718
|
+
});
|
|
944
719
|
|
|
945
|
-
this.updateCameraControls();
|
|
720
|
+
this.updateCameraControls(snapshot);
|
|
946
721
|
this.syncTimingGapModeControls();
|
|
722
|
+
const timingPenaltyKey = this.getTimingPenaltyKey(snapshot.penalties ?? []);
|
|
723
|
+
const timingOrderKey = this.getTimingOrderKey(snapshot.cars);
|
|
724
|
+
const timingUpdateInterval = timingUpdateIntervalForSpeed(this.simulationSpeed);
|
|
947
725
|
if (
|
|
948
|
-
now - this.lastTimingRenderTime >=
|
|
949
|
-
this.lastTimingRaceMode !== snapshot.raceControl.mode
|
|
726
|
+
now - this.lastTimingRenderTime >= timingUpdateInterval ||
|
|
727
|
+
this.lastTimingRaceMode !== snapshot.raceControl.mode ||
|
|
728
|
+
this.lastTimingPenaltyKey !== timingPenaltyKey ||
|
|
729
|
+
this.lastTimingOrderKey !== timingOrderKey
|
|
950
730
|
) {
|
|
951
|
-
this.renderTiming(snapshot.cars, snapshot.raceControl.mode);
|
|
731
|
+
this.renderTiming(snapshot.cars, snapshot.raceControl.mode, snapshot.penalties ?? []);
|
|
952
732
|
this.lastTimingRenderTime = now;
|
|
953
733
|
this.lastTimingRaceMode = snapshot.raceControl.mode;
|
|
734
|
+
this.lastTimingPenaltyKey = timingPenaltyKey;
|
|
735
|
+
this.lastTimingOrderKey = timingOrderKey;
|
|
954
736
|
}
|
|
955
737
|
this.renderTelemetry(selected);
|
|
956
738
|
const activeRaceDataCar = this.activeRaceDataId
|
|
957
739
|
? snapshot.cars.find((car) => car.id === this.activeRaceDataId)
|
|
958
740
|
: null;
|
|
741
|
+
this.renderActiveStewardMessage();
|
|
959
742
|
if (activeRaceDataCar) {
|
|
960
743
|
this.renderRaceData(activeRaceDataCar);
|
|
961
744
|
} else {
|
|
@@ -978,12 +761,7 @@ export class F1SimulatorApp {
|
|
|
978
761
|
this.lastLeaderLap = leaderLap;
|
|
979
762
|
}
|
|
980
763
|
|
|
981
|
-
snapshot.events
|
|
982
|
-
const key = `${event.type}:${event.at ?? snapshot.time}:${event.carId ?? ''}:${event.otherCarId ?? ''}:${event.winnerId ?? ''}`;
|
|
983
|
-
if (this.emittedRaceEventKeys.has(key)) return;
|
|
984
|
-
this.emittedRaceEventKeys.add(key);
|
|
985
|
-
this.emitHostCallback('onRaceEvent', event, snapshot);
|
|
986
|
-
});
|
|
764
|
+
this.emitRaceEvents(snapshot.events, snapshot);
|
|
987
765
|
|
|
988
766
|
if (snapshot.raceControl.finished && !this.raceFinishEmitted) {
|
|
989
767
|
this.raceFinishEmitted = true;
|
|
@@ -995,39 +773,22 @@ export class F1SimulatorApp {
|
|
|
995
773
|
}
|
|
996
774
|
}
|
|
997
775
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const winner = snapshot.raceControl.winner;
|
|
1006
|
-
const winnerDriver = winner ? this.driverById.get(winner.id) : null;
|
|
1007
|
-
const winnerName = winnerDriver?.name ?? winner?.name ?? 'Winner';
|
|
1008
|
-
panel.style.setProperty('--driver-color', winner?.color ?? winnerDriver?.color ?? 'var(--red)');
|
|
1009
|
-
setText(this.readouts.finishWinner, winnerName);
|
|
1010
|
-
|
|
1011
|
-
if (this.readouts.finishClassification) {
|
|
1012
|
-
const topThree = (snapshot.raceControl.classification ?? []).slice(0, 3);
|
|
1013
|
-
const classificationMarkup = topThree.map((entry) => `
|
|
1014
|
-
<li>
|
|
1015
|
-
<span>P${escapeHtml(entry.rank)}</span>
|
|
1016
|
-
<strong>${escapeHtml(entry.timingCode ?? entry.code ?? entry.id)}</strong>
|
|
1017
|
-
</li>
|
|
1018
|
-
`).join('');
|
|
1019
|
-
if (classificationMarkup !== this.lastFinishClassificationMarkup) {
|
|
1020
|
-
this.readouts.finishClassification.innerHTML = classificationMarkup;
|
|
1021
|
-
this.lastFinishClassificationMarkup = classificationMarkup;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
776
|
+
emitRaceEvents(events = [], snapshot) {
|
|
777
|
+
events.forEach((event) => {
|
|
778
|
+
const key = `${event.type}:${event.at ?? snapshot.time}:${event.carId ?? ''}:${event.otherCarId ?? ''}:${event.winnerId ?? ''}`;
|
|
779
|
+
if (this.emittedRaceEventKeys.has(key)) return;
|
|
780
|
+
this.emittedRaceEventKeys.add(key);
|
|
781
|
+
this.emitHostCallback('onRaceEvent', event, snapshot);
|
|
782
|
+
});
|
|
1024
783
|
}
|
|
1025
784
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
785
|
+
renderRaceFinish(snapshot) {
|
|
786
|
+
this.lastFinishClassificationMarkup = renderRaceFinish({
|
|
787
|
+
readouts: this.readouts,
|
|
788
|
+
snapshot,
|
|
789
|
+
driverById: this.driverById,
|
|
790
|
+
lastFinishClassificationMarkup: this.lastFinishClassificationMarkup,
|
|
791
|
+
});
|
|
1031
792
|
}
|
|
1032
793
|
|
|
1033
794
|
sampleFps(now) {
|
|
@@ -1040,24 +801,7 @@ export class F1SimulatorApp {
|
|
|
1040
801
|
}
|
|
1041
802
|
|
|
1042
803
|
renderStartLights(raceControl) {
|
|
1043
|
-
|
|
1044
|
-
if (!panel) return;
|
|
1045
|
-
|
|
1046
|
-
const start = raceControl.start;
|
|
1047
|
-
const visible = Boolean(start?.visible);
|
|
1048
|
-
panel.hidden = !visible;
|
|
1049
|
-
if (!visible) return;
|
|
1050
|
-
|
|
1051
|
-
this.startLightNodes.forEach((light, index) => {
|
|
1052
|
-
light.classList.toggle('is-lit', index < (start.lightsLit ?? 0));
|
|
1053
|
-
});
|
|
1054
|
-
panel.classList.toggle('is-lights-out', raceControl.mode === 'green' && start.released);
|
|
1055
|
-
|
|
1056
|
-
if (this.readouts.startLightsLabel) {
|
|
1057
|
-
this.readouts.startLightsLabel.textContent = raceControl.mode === 'green' && start.released
|
|
1058
|
-
? 'Lights out'
|
|
1059
|
-
: `${start.lightsLit}/${start.lightCount}`;
|
|
1060
|
-
}
|
|
804
|
+
renderStartLights(this.readouts, this.startLightNodes, raceControl);
|
|
1061
805
|
}
|
|
1062
806
|
|
|
1063
807
|
selectCar(id, { focus = false } = {}) {
|
|
@@ -1066,9 +810,7 @@ export class F1SimulatorApp {
|
|
|
1066
810
|
this.lastRaceDataInteraction = performance.now();
|
|
1067
811
|
if (this.isRaceDataBannerEnabled('radio')) this.scheduleRadioBreak(this.lastRaceDataInteraction);
|
|
1068
812
|
if (focus) {
|
|
1069
|
-
this.
|
|
1070
|
-
this.camera.zoom = CAMERA_PRESETS.selected;
|
|
1071
|
-
this.updateCameraControls();
|
|
813
|
+
this.setCameraMode('selected');
|
|
1072
814
|
}
|
|
1073
815
|
this.lastTimingRenderTime = 0;
|
|
1074
816
|
const snapshot = this.sim.snapshot();
|
|
@@ -1077,256 +819,108 @@ export class F1SimulatorApp {
|
|
|
1077
819
|
if (driver) this.emitHostCallback('onDriverSelect', driver, snapshot);
|
|
1078
820
|
}
|
|
1079
821
|
|
|
1080
|
-
renderTiming(cars, raceMode) {
|
|
1081
|
-
if (!this.timingList) return;
|
|
822
|
+
renderTiming(cars, raceMode, penalties = []) {
|
|
1082
823
|
this.syncTimingGapModeControls();
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
</li>
|
|
1113
|
-
`;
|
|
1114
|
-
}).join('');
|
|
1115
|
-
if (timingMarkup !== this.lastTimingMarkup) {
|
|
1116
|
-
this.timingList.innerHTML = timingMarkup;
|
|
1117
|
-
this.lastTimingMarkup = timingMarkup;
|
|
1118
|
-
}
|
|
824
|
+
this.lastTimingMarkup = renderTimingTower({
|
|
825
|
+
timingList: this.timingList,
|
|
826
|
+
cars,
|
|
827
|
+
raceMode,
|
|
828
|
+
penalties,
|
|
829
|
+
driverById: this.driverById,
|
|
830
|
+
selectedId: this.selectedId,
|
|
831
|
+
timingGapMode: this.timingGapMode,
|
|
832
|
+
timingPenaltyBadgesEnabled: this.timingPenaltyBadgesEnabled,
|
|
833
|
+
lastTimingMarkup: this.lastTimingMarkup,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
getTimingOrderKey(cars = []) {
|
|
838
|
+
return getTimingOrderKey(cars, { timingGapMode: this.timingGapMode });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
isWavedFlagCar(car) {
|
|
842
|
+
return isWavedFlagCar(car);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
getPenaltyByDriver(penalties = []) {
|
|
846
|
+
return getPenaltyByDriver(penalties);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
getTimingPenaltyKey(penalties = []) {
|
|
850
|
+
return getTimingPenaltyKey(penalties, {
|
|
851
|
+
timingPenaltyBadgesEnabled: this.timingPenaltyBadgesEnabled,
|
|
852
|
+
});
|
|
1119
853
|
}
|
|
1120
854
|
|
|
1121
855
|
formatTimingGap(car) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
856
|
+
return formatRaceGap(car, this.timingGapMode);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
formatLapGap(laps) {
|
|
860
|
+
return formatLapGap(laps);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
formatTelemetryGap(car, mode) {
|
|
864
|
+
return formatTelemetryGap(car, mode);
|
|
1126
865
|
}
|
|
1127
866
|
|
|
1128
867
|
syncTimingGapModeControls() {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
button.setAttribute('aria-pressed', String(active));
|
|
1134
|
-
button.classList?.toggle?.('is-active', active);
|
|
868
|
+
syncTimingGapModeControls({
|
|
869
|
+
readouts: this.readouts,
|
|
870
|
+
buttons: this.timingGapModeButtons,
|
|
871
|
+
timingGapMode: this.timingGapMode,
|
|
1135
872
|
});
|
|
1136
873
|
}
|
|
1137
874
|
|
|
1138
875
|
renderTelemetry(car) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const drsState = car.drsActive ? 'OPEN' : car.drsEligible ? 'READY' : 'OFF';
|
|
1144
|
-
const surface = (car.surface ?? 'track').toUpperCase();
|
|
1145
|
-
|
|
1146
|
-
setTextAll(this.readouts.selectedCode, car.code);
|
|
1147
|
-
this.readouts.selectedCode?.forEach?.((node) => {
|
|
1148
|
-
node.style.color = car.color;
|
|
876
|
+
renderTelemetryReadouts({
|
|
877
|
+
readouts: this.readouts,
|
|
878
|
+
car,
|
|
879
|
+
driverById: this.driverById,
|
|
1149
880
|
});
|
|
1150
|
-
this.readouts.telemetrySectorBanners?.forEach?.((node) => {
|
|
1151
|
-
node.style.setProperty('--driver-color', car.color);
|
|
1152
|
-
});
|
|
1153
|
-
setTextAll(this.readouts.selectedName, car.name);
|
|
1154
|
-
setTextAll(this.readouts.speed, `${Math.round(car.speedKph)} km/h`);
|
|
1155
|
-
setTextAll(this.readouts.throttle, `${Math.round(car.throttle * 100)}%`);
|
|
1156
|
-
setTextAll(this.readouts.brake, `${Math.round(car.brake * 100)}%`);
|
|
1157
|
-
setTextAll(this.readouts.tyres, `${Math.round(car.tireEnergy ?? 100)}%`);
|
|
1158
|
-
setTextAll(this.readouts.selectedDrs, drsState);
|
|
1159
|
-
setTextAll(this.readouts.surface, surface);
|
|
1160
|
-
setTextAll(
|
|
1161
|
-
this.readouts.gap,
|
|
1162
|
-
car.rank === 1 || !Number.isFinite(car.gapAheadSeconds)
|
|
1163
|
-
? '--'
|
|
1164
|
-
: `${car.gapAheadSeconds.toFixed(2)}s`,
|
|
1165
|
-
);
|
|
1166
|
-
setTextAll(
|
|
1167
|
-
this.readouts.leaderGap,
|
|
1168
|
-
car.rank === 1 || !Number.isFinite(car.leaderGapSeconds)
|
|
1169
|
-
? '--'
|
|
1170
|
-
: `${car.leaderGapSeconds.toFixed(2)}s`,
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
881
|
this.renderCarDriverOverview(car);
|
|
1174
882
|
this.renderLapTelemetry(car.lapTelemetry);
|
|
1175
883
|
}
|
|
1176
884
|
|
|
1177
885
|
renderLapTelemetry(telemetry) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
setTextAll(this.readouts.currentSector, `S${telemetry.currentSector ?? 1}`);
|
|
1181
|
-
setTextAll(this.readouts.completedLaps, `${telemetry.completedLaps ?? 0} laps`);
|
|
1182
|
-
setTextAll(this.readouts.currentLapTime, formatTelemetryTime(telemetry.currentLapTime));
|
|
1183
|
-
setTextAll(this.readouts.lastLapTime, formatTelemetryTime(telemetry.lastLapTime));
|
|
1184
|
-
setTextAll(this.readouts.bestLapTime, formatTelemetryTime(telemetry.bestLapTime));
|
|
1185
|
-
|
|
1186
|
-
this.readouts.telemetrySectorBars?.forEach((bar) => {
|
|
1187
|
-
const sector = Number(bar.dataset.telemetrySectorBar);
|
|
1188
|
-
const index = sector - 1;
|
|
1189
|
-
const isActive = sector === telemetry.currentSector;
|
|
1190
|
-
const sectorComplete = Number.isFinite(telemetry.currentSectors?.[index]);
|
|
1191
|
-
const fill = sectorComplete
|
|
1192
|
-
? 100
|
|
1193
|
-
: (isActive ? clamp((telemetry.currentSectorProgress ?? 0) * 100, 0, 100) : 0);
|
|
1194
|
-
const fillValue = `${fill.toFixed(1)}%`;
|
|
1195
|
-
if (bar.style.getPropertyValue('--sector-fill') !== fillValue) {
|
|
1196
|
-
bar.style.setProperty('--sector-fill', fillValue);
|
|
1197
|
-
}
|
|
1198
|
-
bar.classList.toggle('is-active', isActive);
|
|
1199
|
-
bar.classList.toggle('is-complete', sectorComplete);
|
|
1200
|
-
setPerformanceClass(bar, telemetry.sectorPerformance?.current?.[index]);
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
this.readouts.telemetrySectorTimes?.forEach((node) => {
|
|
1204
|
-
const sector = Number(node.dataset.telemetrySectorTime);
|
|
1205
|
-
const index = sector - 1;
|
|
1206
|
-
const value = sector === telemetry.currentSector && !Number.isFinite(telemetry.currentSectors?.[index])
|
|
1207
|
-
? telemetry.currentSectorElapsed
|
|
1208
|
-
: telemetry.currentSectors?.[index];
|
|
1209
|
-
setText(node, formatTelemetryTime(value));
|
|
1210
|
-
setPerformanceClass(node, telemetry.sectorPerformance?.current?.[index]);
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
this.readouts.telemetrySectorLast?.forEach((node) => {
|
|
1214
|
-
const index = Number(node.dataset.telemetrySectorLast) - 1;
|
|
1215
|
-
setText(node, formatTelemetryTime(telemetry.lastSectors?.[index]));
|
|
1216
|
-
setPerformanceClass(node, telemetry.sectorPerformance?.last?.[index]);
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
this.readouts.telemetrySectorBest?.forEach((node) => {
|
|
1220
|
-
const index = Number(node.dataset.telemetrySectorBest) - 1;
|
|
1221
|
-
setText(node, formatTelemetryTime(telemetry.bestSectors?.[index]));
|
|
1222
|
-
setPerformanceClass(node, telemetry.sectorPerformance?.best?.[index]);
|
|
1223
|
-
});
|
|
886
|
+
renderLapTelemetry(this.readouts, telemetry);
|
|
1224
887
|
}
|
|
1225
888
|
|
|
1226
889
|
getOverviewFields(driver, mode) {
|
|
1227
|
-
|
|
1228
|
-
const ratings = driver?.constructorArgs?.driver?.ratings ?? {};
|
|
1229
|
-
return [
|
|
1230
|
-
...DRIVER_OVERVIEW_FIELDS.map(([key, label]) => ({
|
|
1231
|
-
key,
|
|
1232
|
-
label,
|
|
1233
|
-
value: ratings[key] ?? DRIVER_STAT_DEFINITIONS[key]?.neutral,
|
|
1234
|
-
})),
|
|
1235
|
-
...normalizeCustomFields([
|
|
1236
|
-
...(driver?.constructorArgs?.driver?.customFields ?? []),
|
|
1237
|
-
...(driver?.customFields ?? []),
|
|
1238
|
-
]),
|
|
1239
|
-
];
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
const ratings = driver?.constructorArgs?.vehicle?.ratings ?? driver?.vehicle?.ratings ?? {};
|
|
1243
|
-
return [
|
|
1244
|
-
...VEHICLE_OVERVIEW_FIELDS.map(([key, label]) => ({
|
|
1245
|
-
key,
|
|
1246
|
-
label,
|
|
1247
|
-
value: ratings[key] ?? VEHICLE_STAT_DEFINITIONS[key]?.neutral,
|
|
1248
|
-
})),
|
|
1249
|
-
...normalizeCustomFields(driver?.vehicle?.customFields ?? driver?.constructorArgs?.vehicle?.customFields ?? []),
|
|
1250
|
-
];
|
|
890
|
+
return getOverviewFields(driver, mode);
|
|
1251
891
|
}
|
|
1252
892
|
|
|
1253
893
|
renderCarDriverOverview(car) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
: [{ label: mode === 'driver' ? 'Driver fields' : 'Car fields', value: 'No custom fields' }];
|
|
1263
|
-
const imageSrc = mode === 'driver'
|
|
1264
|
-
? (driver?.driverImage ?? driver?.portrait ?? driver?.avatar ?? this.assets.driverHelmet)
|
|
1265
|
-
: this.assets.carOverview;
|
|
1266
|
-
const overviewCode = mode === 'driver'
|
|
1267
|
-
? `${car.code} driver`
|
|
1268
|
-
: `${driver?.vehicle?.name ?? car.vehicleName ?? car.code}`;
|
|
1269
|
-
const overviewKey = JSON.stringify({
|
|
1270
|
-
id: car.id,
|
|
1271
|
-
mode,
|
|
1272
|
-
color: car.color,
|
|
1273
|
-
code: car.code,
|
|
1274
|
-
icon,
|
|
1275
|
-
driverNumber,
|
|
1276
|
-
imageSrc,
|
|
1277
|
-
overviewCode,
|
|
1278
|
-
fields: displayFields.map((field) => [field.label, field.value]),
|
|
894
|
+
const previousKey = this.lastOverviewRenderKey;
|
|
895
|
+
this.lastOverviewRenderKey = renderCarDriverOverview({
|
|
896
|
+
readouts: this.readouts,
|
|
897
|
+
car,
|
|
898
|
+
driverById: this.driverById,
|
|
899
|
+
assets: this.assets,
|
|
900
|
+
overviewMode: this.overviewMode,
|
|
901
|
+
lastOverviewRenderKey: this.lastOverviewRenderKey,
|
|
1279
902
|
});
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
903
|
+
if (this.lastOverviewRenderKey !== previousKey) this.updateOverviewModeButtons();
|
|
904
|
+
}
|
|
1282
905
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
this.
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
if (this.readouts.carOverviewImage) {
|
|
1293
|
-
this.readouts.carOverviewImage.src = imageSrc;
|
|
1294
|
-
}
|
|
1295
|
-
if (this.readouts.carOverviewNumber) this.readouts.carOverviewNumber.textContent = driverNumber;
|
|
1296
|
-
if (this.readouts.carOverviewCoreStat) this.readouts.carOverviewCoreStat.textContent = mode === 'driver' ? 'Driver' : 'Car';
|
|
1297
|
-
this.readouts.carOverviewFields.forEach((fieldNode, index) => {
|
|
1298
|
-
const field = displayFields[index];
|
|
1299
|
-
fieldNode.hidden = !field;
|
|
1300
|
-
if (!field) return;
|
|
1301
|
-
const labelNode = fieldNode.querySelector('[data-overview-field-label]');
|
|
1302
|
-
const valueNode = fieldNode.querySelector('[data-overview-field-value]');
|
|
1303
|
-
setText(labelNode, field.label);
|
|
1304
|
-
setText(valueNode, field.value);
|
|
906
|
+
renderRaceData(car) {
|
|
907
|
+
renderRaceData({
|
|
908
|
+
car,
|
|
909
|
+
drivers: this.drivers,
|
|
910
|
+
readouts: this.readouts,
|
|
911
|
+
options: {
|
|
912
|
+
...this.options,
|
|
913
|
+
isRaceDataBannerEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
914
|
+
},
|
|
1305
915
|
});
|
|
1306
|
-
this.updateOverviewModeButtons();
|
|
1307
916
|
}
|
|
1308
917
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
this.readouts.raceDataPanel.classList.remove('is-hidden');
|
|
1316
|
-
this.readouts.raceDataPanel.classList.add('is-project-mode');
|
|
1317
|
-
this.readouts.raceDataPanel.classList.remove('is-radio-mode');
|
|
1318
|
-
this.readouts.raceDataPanel.removeAttribute('data-idle-mode');
|
|
1319
|
-
setText(this.readouts.raceDataKicker, 'Project');
|
|
1320
|
-
setText(this.readouts.raceDataTitle, driver.name);
|
|
1321
|
-
if (this.readouts.raceDataNumber) {
|
|
1322
|
-
this.readouts.raceDataNumber.textContent = formatDriverNumber(car.driverNumber ?? driver.driverNumber);
|
|
1323
|
-
}
|
|
1324
|
-
if (this.readouts.raceDataSubtitle) {
|
|
1325
|
-
this.readouts.raceDataSubtitle.textContent = `${car.code} - P${car.rank} - ${driver.raceData?.[0] ?? 'Project entry'}`;
|
|
1326
|
-
}
|
|
1327
|
-
if (this.readouts.raceDataOpen) {
|
|
1328
|
-
this.readouts.raceDataOpen.hidden = typeof this.options.onDriverOpen !== 'function';
|
|
1329
|
-
}
|
|
918
|
+
shouldAutoHideActiveRaceData() {
|
|
919
|
+
return shouldAutoHideActiveRaceData({
|
|
920
|
+
activeRaceDataId: this.activeRaceDataId,
|
|
921
|
+
options: this.options,
|
|
922
|
+
readouts: this.readouts,
|
|
923
|
+
});
|
|
1330
924
|
}
|
|
1331
925
|
|
|
1332
926
|
renderProjectRadio(now = performance.now()) {
|
|
@@ -1341,26 +935,52 @@ export class F1SimulatorApp {
|
|
|
1341
935
|
return;
|
|
1342
936
|
}
|
|
1343
937
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
this.readouts.raceDataPanel.classList.add('is-radio-mode');
|
|
1349
|
-
this.readouts.raceDataPanel.classList.remove('is-project-mode');
|
|
1350
|
-
this.readouts.raceDataPanel.dataset.idleMode = 'quote';
|
|
1351
|
-
setText(this.readouts.raceDataKicker, 'Project radio');
|
|
1352
|
-
setText(this.readouts.raceDataTitle, radio.title);
|
|
1353
|
-
setText(this.readouts.raceDataNumber, '');
|
|
1354
|
-
setText(this.readouts.raceDataSubtitle, radio.subtitle);
|
|
1355
|
-
if (this.readouts.raceDataOpen) this.readouts.raceDataOpen.hidden = true;
|
|
938
|
+
renderProjectRadio({
|
|
939
|
+
readouts: this.readouts,
|
|
940
|
+
radio: this.getProjectRadioQuote(),
|
|
941
|
+
});
|
|
1356
942
|
}
|
|
1357
943
|
|
|
1358
944
|
hideRaceDataPanel() {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
945
|
+
hideRaceDataPanel(this.readouts);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
dismissRaceDataPanel(now = performance.now()) {
|
|
949
|
+
this.activeRaceDataId = null;
|
|
950
|
+
this.lastRaceDataInteraction = now;
|
|
951
|
+
this.radioState.visible = false;
|
|
952
|
+
this.radioState.nextChangeAt = this.getNextRadioBreakTime(now);
|
|
953
|
+
this.hideRaceDataPanel();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
updateStewardMessageState(snapshot, now = performance.now()) {
|
|
957
|
+
const state = updateStewardMessageState({
|
|
958
|
+
snapshot,
|
|
959
|
+
now,
|
|
960
|
+
state: {
|
|
961
|
+
activeStewardMessage: this.activeStewardMessage,
|
|
962
|
+
lastPenaltyBannerId: this.lastPenaltyBannerId,
|
|
963
|
+
lastStewardMessageKey: this.lastStewardMessageKey,
|
|
964
|
+
},
|
|
965
|
+
driverById: this.driverById,
|
|
966
|
+
penaltyBannerEnabled: this.penaltyBannerEnabled,
|
|
967
|
+
stewardMessageNode: this.readouts.stewardMessage,
|
|
968
|
+
});
|
|
969
|
+
this.activeStewardMessage = state.activeStewardMessage;
|
|
970
|
+
this.lastPenaltyBannerId = state.lastPenaltyBannerId;
|
|
971
|
+
this.lastStewardMessageKey = state.lastStewardMessageKey;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
createPenaltyStewardMessage(penalty) {
|
|
975
|
+
return createPenaltyStewardMessage(penalty, this.driverById);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
createWarningStewardMessage(event) {
|
|
979
|
+
return createWarningStewardMessage(event, this.driverById);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
renderActiveStewardMessage() {
|
|
983
|
+
renderActiveStewardMessage(this.readouts, this.activeStewardMessage);
|
|
1364
984
|
}
|
|
1365
985
|
|
|
1366
986
|
updateRadioSchedule(now) {
|
|
@@ -1387,26 +1007,22 @@ export class F1SimulatorApp {
|
|
|
1387
1007
|
}
|
|
1388
1008
|
|
|
1389
1009
|
scheduleRadioBreak(now) {
|
|
1390
|
-
|
|
1391
|
-
|
|
1010
|
+
scheduleRadioBreak({
|
|
1011
|
+
radioState: this.radioState,
|
|
1012
|
+
now,
|
|
1013
|
+
isEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
1014
|
+
nextRandom: () => this.nextRadioRandom(),
|
|
1015
|
+
});
|
|
1392
1016
|
}
|
|
1393
1017
|
|
|
1394
1018
|
scheduleRadioPopup(now) {
|
|
1395
|
-
|
|
1396
|
-
this.
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
this.radioState.visible = true;
|
|
1403
|
-
this.radioState.driverIndex = driverIndex;
|
|
1404
|
-
this.radioState.quoteIndex = Math.floor(this.nextRadioRandom() * quoteCount);
|
|
1405
|
-
this.radioState.nextChangeAt = now + this.randomRadioRange(RADIO_VISIBLE_MIN_MS, RADIO_VISIBLE_MAX_MS);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
randomRadioRange(min, max) {
|
|
1409
|
-
return min + this.nextRadioRandom() * (max - min);
|
|
1019
|
+
scheduleRadioPopup({
|
|
1020
|
+
radioState: this.radioState,
|
|
1021
|
+
now,
|
|
1022
|
+
drivers: this.drivers,
|
|
1023
|
+
isEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
1024
|
+
nextRandom: () => this.nextRadioRandom(),
|
|
1025
|
+
});
|
|
1410
1026
|
}
|
|
1411
1027
|
|
|
1412
1028
|
nextRadioRandom() {
|
|
@@ -1415,62 +1031,64 @@ export class F1SimulatorApp {
|
|
|
1415
1031
|
}
|
|
1416
1032
|
|
|
1417
1033
|
getNextRadioBreakTime(now) {
|
|
1418
|
-
return
|
|
1419
|
-
|
|
1420
|
-
:
|
|
1034
|
+
return getNextRadioBreakTime({
|
|
1035
|
+
now,
|
|
1036
|
+
isEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
1037
|
+
nextRandom: () => this.nextRadioRandom(),
|
|
1038
|
+
});
|
|
1421
1039
|
}
|
|
1422
1040
|
|
|
1423
1041
|
resetRaceDataBannerState(now = performance.now()) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
:
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
? now + this.randomRadioRange(RADIO_VISIBLE_MIN_MS, RADIO_VISIBLE_MAX_MS)
|
|
1433
|
-
: this.getNextRadioBreakTime(now);
|
|
1434
|
-
this.radioState.driverIndex = 0;
|
|
1435
|
-
this.radioState.quoteIndex = 0;
|
|
1042
|
+
resetRaceDataBannerState({
|
|
1043
|
+
state: this,
|
|
1044
|
+
now,
|
|
1045
|
+
initialMode: this.raceDataBannerConfig.initial ?? 'project',
|
|
1046
|
+
selectedId: this.selectedId,
|
|
1047
|
+
isEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
1048
|
+
nextRandom: () => this.nextRadioRandom(),
|
|
1049
|
+
});
|
|
1436
1050
|
}
|
|
1437
1051
|
|
|
1438
1052
|
isRaceDataBannerEnabled(kind) {
|
|
1439
|
-
return this.raceDataBannerConfig.enabled?.includes(kind) ?? true;
|
|
1053
|
+
return !this.raceDataBannersMuted && (this.raceDataBannerConfig.enabled?.includes(kind) ?? true);
|
|
1440
1054
|
}
|
|
1441
1055
|
|
|
1442
1056
|
getProjectRadioQuote() {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
color: driver.color,
|
|
1448
|
-
title: driver.name,
|
|
1449
|
-
subtitle: `${driver.code} - "${quote}"`,
|
|
1450
|
-
};
|
|
1057
|
+
return getProjectRadioQuote({
|
|
1058
|
+
drivers: this.drivers,
|
|
1059
|
+
radioState: this.radioState,
|
|
1060
|
+
});
|
|
1451
1061
|
}
|
|
1452
1062
|
|
|
1453
|
-
updateCameraControls() {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1063
|
+
updateCameraControls(snapshot = this.sim?.snapshot?.()) {
|
|
1064
|
+
updateCameraControlButtons({
|
|
1065
|
+
camera: this.camera,
|
|
1066
|
+
cameraButtons: this.cameraButtons,
|
|
1067
|
+
snapshot,
|
|
1068
|
+
hasPitCamera: (currentSnapshot) => this.hasPitCamera(currentSnapshot),
|
|
1069
|
+
isCameraModeAvailable: (mode, currentSnapshot) => this.isCameraModeAvailable(mode, currentSnapshot),
|
|
1458
1070
|
});
|
|
1071
|
+
this.updateBannerMuteControls();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
updateBannerMuteControls() {
|
|
1075
|
+
updateToggleButtons(this.bannerMuteButtons, this.raceDataBannersMuted);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
hasPitCamera(snapshot = this.sim?.snapshot?.()) {
|
|
1079
|
+
return this.cameraController.hasPitCamera(snapshot);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
isCameraModeAvailable(mode, snapshot = this.sim?.snapshot?.()) {
|
|
1083
|
+
return this.cameraController.isModeAvailable(mode, snapshot);
|
|
1459
1084
|
}
|
|
1460
1085
|
|
|
1461
1086
|
updateOverviewModeButtons() {
|
|
1462
|
-
this.overviewModeButtons.
|
|
1463
|
-
const isActive = button.dataset.overviewMode === this.overviewMode;
|
|
1464
|
-
button.classList.toggle('is-active', isActive);
|
|
1465
|
-
button.setAttribute('aria-pressed', String(isActive));
|
|
1466
|
-
});
|
|
1087
|
+
updateModeButtons(this.overviewModeButtons, this.overviewMode, 'overviewMode');
|
|
1467
1088
|
}
|
|
1468
1089
|
|
|
1469
1090
|
syncSafetyCarControls(active) {
|
|
1470
|
-
this.safetyButtons
|
|
1471
|
-
button.classList.toggle('is-active', active);
|
|
1472
|
-
button.setAttribute('aria-pressed', String(active));
|
|
1473
|
-
});
|
|
1091
|
+
updateToggleButtons(this.safetyButtons, active);
|
|
1474
1092
|
}
|
|
1475
1093
|
|
|
1476
1094
|
applyExpertOptions(nextOptions = {}) {
|
|
@@ -1513,8 +1131,12 @@ export class F1SimulatorApp {
|
|
|
1513
1131
|
if (hasOwnOption(nextOptions, 'expert') && expertOptionsChanged(nextOptions.expert, this.options.expert)) {
|
|
1514
1132
|
throw new Error('PaddockJS restart() does not support changing expert mode. Destroy and mount a new simulator with the new expert options.');
|
|
1515
1133
|
}
|
|
1134
|
+
this.expert?.destroy?.();
|
|
1135
|
+
this.expert = null;
|
|
1516
1136
|
this.applyExpertOptions(nextOptions);
|
|
1517
1137
|
this.raceDataBannerConfig = this.options.ui?.raceDataBanners ?? this.raceDataBannerConfig;
|
|
1138
|
+
this.penaltyBannerEnabled = Boolean(this.options.ui?.penaltyBanners);
|
|
1139
|
+
this.timingPenaltyBadgesEnabled = Boolean(this.options.ui?.timingPenaltyBadges);
|
|
1518
1140
|
this.root.style.setProperty('--broadcast-panel-surface', `url('${this.assets.broadcastPanel}')`);
|
|
1519
1141
|
applyPaddockThemeCssVariables(this.root, this.options.theme);
|
|
1520
1142
|
this.sim = this.createRaceSimulation();
|
|
@@ -1528,11 +1150,20 @@ export class F1SimulatorApp {
|
|
|
1528
1150
|
this.resetRaceDataBannerState(performance.now());
|
|
1529
1151
|
this.lastTimingRenderTime = 0;
|
|
1530
1152
|
this.lastTimingRaceMode = null;
|
|
1153
|
+
this.lastTimingPenaltyKey = '';
|
|
1154
|
+
this.lastTimingOrderKey = '';
|
|
1155
|
+
this.activePenaltyBanner = null;
|
|
1156
|
+
this.lastPenaltyBannerId = null;
|
|
1157
|
+
this.activeStewardMessage = null;
|
|
1158
|
+
this.lastStewardMessageKey = null;
|
|
1531
1159
|
this.lastLeaderLap = null;
|
|
1532
1160
|
this.emittedRaceEventKeys.clear();
|
|
1533
1161
|
this.raceFinishEmitted = false;
|
|
1534
1162
|
this.renderTrack();
|
|
1535
|
-
this.
|
|
1163
|
+
const snapshot = this.sim.snapshot();
|
|
1164
|
+
this.updateDom(snapshot);
|
|
1165
|
+
this.lastDomUpdateTime = performance.now();
|
|
1166
|
+
this.renderInitialFrame(snapshot);
|
|
1536
1167
|
}
|
|
1537
1168
|
|
|
1538
1169
|
setSafetyCarDeployed(deployed) {
|
|
@@ -1541,12 +1172,52 @@ export class F1SimulatorApp {
|
|
|
1541
1172
|
this.syncSafetyCarControls(active);
|
|
1542
1173
|
}
|
|
1543
1174
|
|
|
1175
|
+
setRedFlagDeployed(deployed) {
|
|
1176
|
+
this.sim?.setRedFlag?.(Boolean(deployed));
|
|
1177
|
+
if (this.sim) this.updateDom(this.sim.snapshot());
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
setPitLaneOpen(open) {
|
|
1181
|
+
this.sim?.setPitLaneOpen?.(Boolean(open));
|
|
1182
|
+
if (this.sim) this.updateDom(this.sim.snapshot());
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
setPitIntent(driverId, intent, targetCompound) {
|
|
1186
|
+
const applied = Boolean(this.sim?.setPitIntent?.(driverId, intent, targetCompound));
|
|
1187
|
+
if (applied) this.updateDom(this.sim.snapshot());
|
|
1188
|
+
return applied;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
getPitIntent(driverId) {
|
|
1192
|
+
return this.sim?.getPitIntent?.(driverId) ?? 0;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
getPitTargetCompound(driverId) {
|
|
1196
|
+
return this.sim?.getPitTargetCompound?.(driverId) ?? null;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
servePenalty(penaltyId) {
|
|
1200
|
+
const penalty = this.sim?.servePenalty(penaltyId) ?? null;
|
|
1201
|
+
if (penalty) this.updateDom(this.sim.snapshot());
|
|
1202
|
+
return penalty;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
cancelPenalty(penaltyId) {
|
|
1206
|
+
const penalty = this.sim?.cancelPenalty(penaltyId) ?? null;
|
|
1207
|
+
if (penalty) this.updateDom(this.sim.snapshot());
|
|
1208
|
+
return penalty;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1544
1211
|
getSnapshot() {
|
|
1545
1212
|
return this.sim?.snapshot() ?? null;
|
|
1546
1213
|
}
|
|
1547
1214
|
|
|
1548
1215
|
destroy() {
|
|
1549
1216
|
this.abortController.abort();
|
|
1217
|
+
if (this.layoutResizeFrame && typeof cancelAnimationFrame === 'function') {
|
|
1218
|
+
cancelAnimationFrame(this.layoutResizeFrame);
|
|
1219
|
+
}
|
|
1220
|
+
this.layoutResizeFrame = null;
|
|
1550
1221
|
this.layoutResizeObserver?.disconnect?.();
|
|
1551
1222
|
this.layoutResizeObserver = null;
|
|
1552
1223
|
this.visibilityObserver?.disconnect?.();
|
|
@@ -1554,10 +1225,14 @@ export class F1SimulatorApp {
|
|
|
1554
1225
|
if (this.app && this.tickerCallback) {
|
|
1555
1226
|
this.app.ticker.remove(this.tickerCallback);
|
|
1556
1227
|
}
|
|
1228
|
+
this.expert?.destroy?.();
|
|
1229
|
+
this.expert = null;
|
|
1230
|
+
this.replayGhostRenderer.destroy();
|
|
1557
1231
|
this.app?.destroy(true, { children: true, texture: false });
|
|
1558
1232
|
this.app = null;
|
|
1559
1233
|
this.carSprites.clear();
|
|
1560
1234
|
this.carHitAreas.clear();
|
|
1235
|
+
this.serviceCountdownLabels.clear();
|
|
1561
1236
|
this.drsTrails.clear();
|
|
1562
1237
|
}
|
|
1563
1238
|
}
|