@inventure71/paddockjs 4.1.0 → 4.3.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 +76 -0
- package/INSTALL_AND_UPDATE.md +31 -5
- package/README.md +34 -3
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +111 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +32 -16
- package/docs/composable-layouts.md +2 -0
- package/docs/concepts.md +12 -1
- package/docs/custom_model_controller.md +1 -1
- package/docs/data_contract.md +33 -10
- package/docs/data_model.md +4 -2
- package/docs/efficiency_cleanup_2026-10-13.md +80 -0
- package/docs/getting-started.md +3 -0
- package/docs/grid_walk.md +80 -0
- package/docs/index.md +5 -2
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -0
- package/docs/loading.md +141 -0
- package/docs/maintainability_audit_2026-09-08.md +146 -0
- package/docs/racing_ai_improvements.md +145 -0
- package/docs/rules.md +43 -13
- package/docs/sense_contract.md +4 -1
- package/docs/system_specs.md +40 -12
- package/docs/testing-reliability-guide.md +67 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +23 -15
- package/src/api/MountedSimulatorComponents.js +101 -0
- package/src/api/PaddockSimulatorController.js +56 -81
- package/src/app/BrowserExpertAdapter.js +0 -6
- package/src/app/BrowserExpertExternalRenderer.js +72 -50
- package/src/app/ExternalTrackSnapshot.js +50 -0
- package/src/app/F1SimulatorApp.js +242 -297
- package/src/app/HostLifecycleEmitter.js +93 -0
- package/src/app/banners/raceDataBannerController.js +9 -4
- package/src/app/camera/cameraConstants.js +1 -1
- package/src/app/camera/cameraController.js +10 -13
- package/src/app/camera/cameraGeometry.js +1 -1
- package/src/app/controls/SimulatorViewControls.js +45 -0
- package/src/app/controls/workbenchBindings.js +64 -0
- package/src/app/controls/workbenchDisclosure.js +41 -0
- package/src/app/domBindings.js +21 -13
- package/src/app/gridWalk/GridWalkController.js +180 -0
- package/src/app/gridWalk/gridWalkCamera.js +88 -0
- package/src/app/gridWalk/gridWalkControls.js +24 -0
- package/src/app/gridWalk/gridWalkCrew.js +246 -0
- package/src/app/gridWalk/gridWalkLabels.js +104 -0
- package/src/app/gridWalk/gridWalkLayout.js +46 -0
- package/src/app/gridWalk/gridWalkNavigation.js +99 -0
- package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
- package/src/app/gridWalk/gridWalkSession.js +50 -0
- package/src/app/layoutSupport.js +2 -0
- package/src/app/raceOverlayClearanceSupport.js +21 -7
- package/src/app/readouts/carOverviewRenderer.js +9 -4
- package/src/app/readouts/readoutFormatters.js +1 -8
- package/src/app/readouts/telemetryRenderer.js +1 -3
- package/src/app/readouts/timingTowerRenderer.js +48 -50
- package/src/app/rendering/appAssets.js +8 -10
- package/src/app/rendering/carRenderer.js +27 -79
- package/src/app/rendering/displayUtils.js +0 -6
- package/src/app/rendering/expertSensorRenderer.js +1 -1
- package/src/app/rendering/replayGhostRenderer.js +4 -1
- package/src/app/rendering/trackRenderer.js +2 -2
- package/src/app/runtime/frameLoop.js +18 -17
- package/src/app/runtime/pixiStartupLifecycle.js +94 -0
- package/src/app/runtime/runtimeTiming.js +4 -1
- package/src/app/runtime/runtimeVisibility.js +7 -2
- package/src/config/cssValues.js +1 -1
- package/src/config/defaultOptions.js +12 -173
- package/src/config/restartOptions.js +1 -0
- package/src/config/runtimeSeed.js +10 -0
- package/src/config/simulatorDefaults.js +106 -0
- package/src/config/telemetryModules.js +20 -0
- package/src/config/theme/cssTokens.js +85 -0
- package/src/config/theme/themeCss.js +72 -0
- package/src/config/theme/themeInputs.js +155 -0
- package/src/config/theme/themeResolver.js +172 -0
- package/src/config/theme/themeSelectors.js +69 -0
- package/src/config/theme/themeTokens.js +257 -0
- package/src/config/theme/themeUtils.js +15 -0
- package/src/config/themeOptions.js +14 -830
- package/src/config/timingGapMode.js +0 -2
- package/src/config/uiOptions.js +53 -0
- package/src/data/championship.js +16 -158
- package/src/data/championshipDefaults.js +87 -0
- package/src/data/driverData.js +10 -20
- package/src/data/entryIdentity.js +67 -0
- package/src/data/entryPresentation.js +23 -0
- package/src/data/entryTypes.d.ts +160 -0
- package/src/data/index.d.ts +12 -195
- package/src/data/ratingScale.js +14 -0
- package/src/data/teamTypes.d.ts +23 -0
- package/src/data/trackTypes.d.ts +101 -0
- package/src/data/vehicleData.js +12 -23
- package/src/environment/actions.js +3 -2
- package/src/environment/controllerLoop.js +57 -44
- package/src/environment/controllerScheduler.js +30 -0
- package/src/environment/driverObservations.js +260 -0
- package/src/environment/driverReset.js +51 -0
- package/src/environment/episode.js +0 -1
- package/src/environment/evaluation.js +6 -90
- package/src/environment/evaluationTracker.js +100 -0
- package/src/environment/events.js +23 -0
- package/src/environment/index.d.ts +35 -91
- package/src/environment/metrics.js +3 -17
- package/src/environment/observationSchema.js +104 -0
- package/src/environment/observationVector.js +16 -115
- package/src/environment/observations.js +16 -316
- package/src/environment/options.js +5 -24
- package/src/environment/renderFrames.js +21 -0
- package/src/environment/resetOptions.js +41 -0
- package/src/environment/results.js +125 -0
- package/src/environment/rewards.js +10 -16
- package/src/environment/runtime.js +17 -300
- package/src/environment/scenarioOptions.js +90 -0
- package/src/environment/scenarioPlacements.js +70 -0
- package/src/environment/scenarioPresets.js +78 -0
- package/src/environment/scenarios.js +5 -236
- package/src/environment/scratch.js +18 -0
- package/src/environment/sensorOptions.js +20 -0
- package/src/environment/sensors/index.js +2 -556
- package/src/environment/sensors/indexedRayBands.js +0 -18
- package/src/environment/sensors/nearbyCars.js +18 -15
- package/src/environment/sensors/opponentRadar.js +3 -2
- package/src/environment/sensors/pitConnectorProximity.js +20 -0
- package/src/environment/sensors/rayBandTrace.js +1 -37
- package/src/environment/sensors/rayBatch.js +81 -0
- package/src/environment/sensors/rayConfig.js +26 -0
- package/src/environment/sensors/rayGeometry.js +0 -8
- package/src/environment/sensors/rayGuards.js +0 -6
- package/src/environment/sensors/rayOutputs.js +75 -0
- package/src/environment/sensors/rayQuery.js +110 -0
- package/src/environment/sensors/rayScratch.js +146 -0
- package/src/environment/sensors/sensorTargets.js +12 -4
- package/src/environment/sensors/surfaceRays.js +6 -61
- package/src/environment/sensors/trackRays.js +7 -80
- package/src/environment/simulationSetup.js +30 -0
- package/src/environment/specs.js +15 -109
- package/src/index.d.ts +43 -202
- package/src/index.js +28 -6
- package/src/placeholder/index.js +1 -8
- package/src/rendering/displayLifecycle.js +6 -0
- package/src/rendering/proceduralTrackAsset.js +1 -1
- package/src/rendering/renderSnapshot.js +3 -0
- package/src/rendering/track/finishLineRenderer.js +1 -1
- package/src/rendering/track/gridRenderer.js +1 -1
- package/src/rendering/track/offsetStrokeSafety.js +2 -2
- package/src/rendering/track/pitLaneRenderer.js +1 -1
- package/src/rendering/track/trackMaterialRenderer.js +4 -4
- package/src/rendering/track/trackRenderConstants.js +0 -1
- package/src/rendering/track/trackRenderGeometry.js +1 -7
- package/src/simulation/collisionGeometry.js +12 -20
- package/src/simulation/driver/advancedPathControls.js +53 -0
- package/src/simulation/driver/advancedRacePace.js +112 -0
- package/src/simulation/driver/advancedRacingControls.js +61 -0
- package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
- package/src/simulation/driver/advancedRacingSteering.js +76 -0
- package/src/simulation/driver/advancedRacingTiming.js +16 -0
- package/src/simulation/driver/arcadeRacePace.js +83 -0
- package/src/simulation/driver/arcadeRacingControls.js +111 -0
- package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
- package/src/simulation/driver/driverControlConstants.js +0 -2
- package/src/simulation/driver/driverController.js +16 -5
- package/src/simulation/driver/edgeRecovery.js +2 -3
- package/src/simulation/driver/racingControls.js +10 -396
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +54 -20
- package/src/simulation/driver/rejoinControls.js +59 -112
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +136 -27
- package/src/simulation/participants/participantInteractions.js +24 -15
- package/src/simulation/pit/pitCarPlacement.js +29 -0
- package/src/simulation/pit/pitFlow.js +8 -30
- package/src/simulation/pit/pitIntent.js +4 -3
- package/src/simulation/pit/pitOccupancy.js +2 -2
- package/src/simulation/pit/pitQueue.js +2 -18
- package/src/simulation/pit/pitRouteMovement.js +1 -36
- package/src/simulation/pit/pitRouting.js +5 -81
- package/src/simulation/pit/pitService.js +3 -32
- package/src/simulation/pit/pitServiceConstants.js +1 -0
- package/src/simulation/pit/pitServiceProgress.js +34 -0
- package/src/simulation/pit/pitSnapshots.js +8 -4
- package/src/simulation/pit/pitState.js +1 -5
- package/src/simulation/race/classification.js +1 -8
- package/src/simulation/race/raceFinish.js +3 -8
- package/src/simulation/race/raceOrder.js +2 -13
- package/src/simulation/race/raceProgress.js +6 -20
- package/src/simulation/race/raceSetup.js +1 -1
- package/src/simulation/race/raceStep.js +20 -11
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +5 -9
- package/src/simulation/race/stalledDnf.js +2 -4
- package/src/simulation/raceSimulation.js +6 -38
- package/src/simulation/replay/replayGhosts.js +44 -32
- package/src/simulation/rules/collisionSteward.js +0 -14
- package/src/simulation/rules/penaltyActions.js +37 -0
- package/src/simulation/rules/penaltyLedger.js +8 -1
- package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +20 -16
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- package/src/simulation/simMath.js +4 -0
- package/src/simulation/snapshots/raceSnapshots.js +30 -11
- package/src/simulation/timing/drsState.js +8 -0
- package/src/simulation/timing/drsTiming.js +2 -4
- package/src/simulation/timing/lapTelemetry.js +7 -4
- package/src/simulation/timing/sectorPerformance.js +14 -6
- package/src/simulation/timing/timingLines.js +12 -6
- package/src/simulation/track/pitLaneAccess.js +2 -2
- package/src/simulation/track/pitLaneLayout.js +4 -135
- package/src/simulation/track/pitLaneModel.js +70 -0
- package/src/simulation/track/pitLaneState.js +0 -1
- package/src/simulation/track/pitServiceLayout.js +121 -0
- package/src/simulation/track/proceduralTrack.js +77 -0
- package/src/simulation/track/spatialQueries.js +76 -31
- package/src/simulation/track/trackBuilder.js +54 -0
- package/src/simulation/track/trackConstants.js +0 -22
- package/src/simulation/track/trackMath.js +4 -29
- package/src/simulation/track/trackModel.js +12 -240
- package/src/simulation/track/trackModelCache.js +105 -0
- package/src/simulation/track/trackNearestQueries.js +1218 -0
- package/src/simulation/track/trackQueryConstants.js +3 -0
- package/src/simulation/track/trackQueryConstruction.js +368 -0
- package/src/simulation/track/trackQueryIndex.js +12 -1933
- package/src/simulation/track/trackQuerySegments.js +174 -0
- package/src/simulation/track/trackQueryStats.js +12 -0
- package/src/simulation/track/trackRayQueries.js +165 -0
- package/src/simulation/vehicle/advancedContactResponse.js +89 -0
- package/src/simulation/vehicle/advancedTireForces.js +92 -0
- package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
- package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
- package/src/simulation/vehicle/contactResolution.js +23 -9
- package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
- package/src/simulation/vehicle/pitWheelSurface.js +0 -12
- package/src/simulation/vehicle/runoffResponse.js +2 -4
- package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
- package/src/simulation/vehicle/vehicleGeometry.js +85 -103
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +42 -196
- package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
- package/src/simulation/vehicle/vehicleState.js +5 -3
- package/src/simulation/vehicle/wheelSurface.js +13 -432
- package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
- package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
- package/src/styles.css +173 -19
- package/src/ui/bannerTemplates.js +4 -2
- package/src/ui/cameraControlsTemplate.js +4 -2
- package/src/ui/gridWalkTemplate.js +29 -0
- package/src/ui/htmlEscaping.js +8 -0
- package/src/ui/raceCanvasTemplate.js +3 -2
- package/src/ui/shellTemplate.js +3 -0
- package/src/ui/telemetryDrawerTemplate.js +4 -2
- package/src/ui/telemetryTemplates.js +3 -22
- package/src/ui/templateUtils.js +2 -8
- package/src/ui/viewControlsTemplate.js +21 -0
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Application, Container, Graphics } from 'pixi.js';
|
|
2
2
|
import { applyPaddockThemeCssVariables } from '../config/defaultOptions.js';
|
|
3
3
|
import { formatCssUrl } from '../config/cssValues.js';
|
|
4
|
+
import { createRuntimeSeed } from '../config/runtimeSeed.js';
|
|
4
5
|
import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
|
|
5
6
|
import { ProceduralTrackAsset } from '../rendering/proceduralTrackAsset.js';
|
|
6
7
|
import { interpolateRenderSnapshotInto } from '../rendering/renderSnapshot.js';
|
|
@@ -8,7 +9,15 @@ import { createRaceSimulation, FIXED_STEP } from '../simulation/raceSimulation.j
|
|
|
8
9
|
import { clamp } from '../simulation/simMath.js';
|
|
9
10
|
import { WORLD } from '../simulation/track/trackModel.js';
|
|
10
11
|
import { CameraController } from './camera/cameraController.js';
|
|
12
|
+
import { GridWalkController } from './gridWalk/GridWalkController.js';
|
|
13
|
+
import { bindWorkbenchControls, bindCameraWheelControls } from './controls/workbenchBindings.js';
|
|
14
|
+
import {
|
|
15
|
+
renderTelemetryDrawerDisclosure,
|
|
16
|
+
isTimingPanelDisclosureMode,
|
|
17
|
+
syncTimingPanelDisclosureState,
|
|
18
|
+
} from './controls/workbenchDisclosure.js';
|
|
11
19
|
import { createBrowserExpertAdapter } from './BrowserExpertAdapter.js';
|
|
20
|
+
import { HostLifecycleEmitter } from './HostLifecycleEmitter.js';
|
|
12
21
|
import { installLayoutSupport } from './layoutSupport.js';
|
|
13
22
|
import { installRaceOverlayClearanceSupport } from './raceOverlayClearanceSupport.js';
|
|
14
23
|
import { querySimulatorDom, resolveNodes } from './domBindings.js';
|
|
@@ -27,11 +36,7 @@ import {
|
|
|
27
36
|
} from './readouts/timingTowerRenderer.js';
|
|
28
37
|
import { renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
29
38
|
import { renderLapTelemetry, renderTelemetryReadouts } from './readouts/telemetryRenderer.js';
|
|
30
|
-
import {
|
|
31
|
-
renderRaceFinish,
|
|
32
|
-
renderRaceStatusReadouts,
|
|
33
|
-
renderStartLights,
|
|
34
|
-
} from './readouts/raceStatusRenderer.js';
|
|
39
|
+
import { renderRaceStatusReadouts } from './readouts/raceStatusRenderer.js';
|
|
35
40
|
import {
|
|
36
41
|
updateCameraControlButtons,
|
|
37
42
|
updateModeButtons,
|
|
@@ -39,7 +44,6 @@ import {
|
|
|
39
44
|
} from './readouts/controlStateRenderer.js';
|
|
40
45
|
import {
|
|
41
46
|
createPenaltyStewardMessage,
|
|
42
|
-
createWarningStewardMessage,
|
|
43
47
|
renderActiveStewardMessage,
|
|
44
48
|
updateStewardMessageState,
|
|
45
49
|
} from './banners/stewardMessageController.js';
|
|
@@ -47,26 +51,21 @@ import {
|
|
|
47
51
|
hideRaceDataPanel,
|
|
48
52
|
getProjectRadioQuote,
|
|
49
53
|
RACE_DATA_SELECTED_VISIBLE_MS,
|
|
50
|
-
RADIO_SCHEDULE_CATCHUP_LIMIT_MS,
|
|
51
54
|
renderProjectRadio,
|
|
52
55
|
renderRaceData,
|
|
53
56
|
resetRaceDataBannerState,
|
|
54
57
|
scheduleRadioBreak,
|
|
55
|
-
scheduleRadioPopup,
|
|
56
58
|
shouldAutoHideActiveRaceData,
|
|
57
59
|
getNextRadioBreakTime,
|
|
60
|
+
updateRadioSchedule,
|
|
58
61
|
} from './banners/raceDataBannerController.js';
|
|
59
62
|
import { runFrameLoopTick } from './runtime/frameLoop.js';
|
|
60
63
|
import { observeRuntimeVisibility, syncRuntimeTicker } from './runtime/runtimeVisibility.js';
|
|
61
|
-
import {
|
|
64
|
+
import { configureRuntimeTicker, domUpdateIntervalForSpeed, timingUpdateIntervalForSpeed } from './runtime/runtimeTiming.js';
|
|
65
|
+
import { assertRuntimeStartupActive, createPixiStartupLifecycle, RuntimeStartupCancelledError } from './runtime/pixiStartupLifecycle.js';
|
|
62
66
|
import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
63
67
|
|
|
64
68
|
const SIMULATION_SPEED_STEPS = [1, 2, 3, 4, 5, 10];
|
|
65
|
-
const DRS_DRAG_REDUCTION_PERCENT = 58;
|
|
66
|
-
|
|
67
|
-
function uniqueNodes(nodes = []) {
|
|
68
|
-
return [...new Set([...nodes].filter(Boolean))];
|
|
69
|
-
}
|
|
70
69
|
|
|
71
70
|
function normalizeRuntimeSnapshot(snapshot, totalLaps) {
|
|
72
71
|
const source = snapshot && typeof snapshot === 'object' ? snapshot : {};
|
|
@@ -102,16 +101,6 @@ function normalizeRuntimeSnapshot(snapshot, totalLaps) {
|
|
|
102
101
|
};
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
function createMountTrackSeed() {
|
|
106
|
-
const values = new Uint32Array(1);
|
|
107
|
-
try {
|
|
108
|
-
globalThis.crypto?.getRandomValues?.(values);
|
|
109
|
-
} catch {
|
|
110
|
-
values[0] = 0;
|
|
111
|
-
}
|
|
112
|
-
return (values[0] || Math.floor(Date.now() + performance.now() * 1000)) >>> 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
104
|
function hasOwnOption(options, key) {
|
|
116
105
|
return Object.hasOwn(options ?? {}, key);
|
|
117
106
|
}
|
|
@@ -120,6 +109,10 @@ function expertOptionsChanged(nextExpertOptions, currentExpertOptions) {
|
|
|
120
109
|
return nextExpertOptions !== currentExpertOptions;
|
|
121
110
|
}
|
|
122
111
|
|
|
112
|
+
function timingRaceMode(snapshot) {
|
|
113
|
+
return snapshot?.raceControl?.finished ? 'finished' : snapshot?.raceControl?.mode;
|
|
114
|
+
}
|
|
115
|
+
|
|
123
116
|
function withSelectedExpertSensorDriver(visualizeSensors, selectedDriverId) {
|
|
124
117
|
if (!visualizeSensors) return visualizeSensors;
|
|
125
118
|
if (visualizeSensors === true) return { rays: true, selectedDriverId };
|
|
@@ -144,6 +137,7 @@ export class F1SimulatorApp {
|
|
|
144
137
|
constructor(root, options) {
|
|
145
138
|
this.root = root;
|
|
146
139
|
this.options = options;
|
|
140
|
+
this.hostLifecycle = new HostLifecycleEmitter(() => this.options, () => this.destroyed ? false : this.sim);
|
|
147
141
|
this.drivers = options.drivers;
|
|
148
142
|
this.driverById = new Map(this.drivers.map((driver) => [driver.id, driver]));
|
|
149
143
|
this.assets = options.assets;
|
|
@@ -155,6 +149,7 @@ export class F1SimulatorApp {
|
|
|
155
149
|
this.syncThemeContext();
|
|
156
150
|
Object.assign(this, querySimulatorDom(root));
|
|
157
151
|
this.abortController = new AbortController();
|
|
152
|
+
this.destroyed = false;
|
|
158
153
|
this.resizeHandler = null;
|
|
159
154
|
this.layoutResizeObserver = null;
|
|
160
155
|
this.layoutSupportCleanup = installLayoutSupport(root);
|
|
@@ -168,6 +163,8 @@ export class F1SimulatorApp {
|
|
|
168
163
|
this.expertMode = Boolean(options.expert?.enabled);
|
|
169
164
|
this.sim = null;
|
|
170
165
|
this.app = null;
|
|
166
|
+
this.pixiStartupLifecycle = null;
|
|
167
|
+
this.destroyCleanupErrors = null;
|
|
171
168
|
this.worldLayer = null;
|
|
172
169
|
this.trackAsset = null;
|
|
173
170
|
this.drsLayer = null;
|
|
@@ -184,19 +181,24 @@ export class F1SimulatorApp {
|
|
|
184
181
|
carSprites: this.carSprites,
|
|
185
182
|
carHitAreas: this.carHitAreas,
|
|
186
183
|
serviceCountdownLabels: this.serviceCountdownLabels,
|
|
187
|
-
onSelectCar: (driverId) =>
|
|
184
|
+
onSelectCar: (driverId, event) => {
|
|
185
|
+
if (this.gridWalk?.active) {
|
|
186
|
+
this.gridWalk.navigation.selectOnRelease(driverId, event);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.selectCar(driverId, { focus: true });
|
|
190
|
+
},
|
|
188
191
|
});
|
|
189
192
|
this.drsTrails = new Map();
|
|
190
193
|
this.drsTrailRenderer = new DrsTrailRenderer({ trails: this.drsTrails });
|
|
191
194
|
this.replayGhostRenderer = new ReplayGhostRenderer();
|
|
192
195
|
this.pitLaneStatusRenderer = new PitLaneStatusRenderer();
|
|
193
|
-
this.trackSeed = options.trackSeed ??
|
|
196
|
+
this.trackSeed = options.trackSeed ?? createRuntimeSeed();
|
|
194
197
|
this.raceDataBannerConfig = options.ui?.raceDataBanners ?? { initial: 'project', enabled: ['project', 'radio'] };
|
|
195
198
|
this.raceDataBannersMuted = false;
|
|
196
199
|
this.penaltyBannerEnabled = Boolean(options.ui?.penaltyBanners);
|
|
197
200
|
this.timingPenaltyBadgesEnabled = Boolean(options.ui?.timingPenaltyBadges);
|
|
198
201
|
this.activeRaceDataId = null;
|
|
199
|
-
this.activePenaltyBanner = null;
|
|
200
202
|
this.lastPenaltyBannerId = null;
|
|
201
203
|
this.activeStewardMessage = null;
|
|
202
204
|
this.lastStewardMessageKey = null;
|
|
@@ -220,7 +222,6 @@ export class F1SimulatorApp {
|
|
|
220
222
|
this.simulationSpeed = 1;
|
|
221
223
|
this.accumulator = 0;
|
|
222
224
|
this.lastTime = performance.now();
|
|
223
|
-
this.nextGameFrameTime = this.lastTime + TARGET_FRAME_MS;
|
|
224
225
|
this.lastDomUpdateTime = 0;
|
|
225
226
|
this.lastTimingRenderTime = 0;
|
|
226
227
|
this.lastTimingRaceMode = null;
|
|
@@ -238,9 +239,6 @@ export class F1SimulatorApp {
|
|
|
238
239
|
lastSample: this.lastTime,
|
|
239
240
|
};
|
|
240
241
|
this.renderSnapshotBuffer = {};
|
|
241
|
-
this.lastLeaderLap = null;
|
|
242
|
-
this.emittedRaceEventKeys = new Set();
|
|
243
|
-
this.raceFinishEmitted = false;
|
|
244
242
|
this.timingGapMode = normalizeTimingGapMode(options.ui?.timingGapMode);
|
|
245
243
|
this.telemetryDrawerOpen = Boolean(
|
|
246
244
|
this.readouts.telemetryDrawerWorkbench?.classList?.contains?.('is-telemetry-open'),
|
|
@@ -250,19 +248,30 @@ export class F1SimulatorApp {
|
|
|
250
248
|
async init() {
|
|
251
249
|
this.emitHostCallback('onLoadingChange', { loading: true, phase: 'initializing' });
|
|
252
250
|
try {
|
|
251
|
+
assertRuntimeStartupActive(this);
|
|
253
252
|
if (!this.canvasHost) {
|
|
254
253
|
throw new Error('F1 simulator canvas host is missing.');
|
|
255
254
|
}
|
|
256
255
|
|
|
257
256
|
this.sim = this.createRaceSimulation();
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
const application = new Application();
|
|
258
|
+
const lifecycleEmitter = this.hostLifecycle;
|
|
259
|
+
const startup = createPixiStartupLifecycle(application, (error) => {
|
|
260
|
+
if (this.destroyCleanupErrors) this.destroyCleanupErrors.push(error);
|
|
261
|
+
else lifecycleEmitter?.emitCallback('onError', error, { phase: 'destroy' });
|
|
262
|
+
});
|
|
263
|
+
this.app = application;
|
|
264
|
+
this.pixiStartupLifecycle = startup;
|
|
265
|
+
await startup.initialize({
|
|
260
266
|
resizeTo: this.canvasHost,
|
|
267
|
+
autoStart: false,
|
|
261
268
|
antialias: true,
|
|
262
269
|
autoDensity: true,
|
|
263
270
|
resolution: Math.min(window.devicePixelRatio || 1, 2),
|
|
264
271
|
backgroundAlpha: 0,
|
|
265
272
|
});
|
|
273
|
+
assertRuntimeStartupActive(this);
|
|
274
|
+
configureRuntimeTicker(application.ticker);
|
|
266
275
|
this.canvasHost.appendChild(this.app.canvas);
|
|
267
276
|
|
|
268
277
|
this.worldLayer = new Container();
|
|
@@ -274,7 +283,9 @@ export class F1SimulatorApp {
|
|
|
274
283
|
this.carLayer = new Container();
|
|
275
284
|
this.app.stage.addChild(this.worldLayer);
|
|
276
285
|
|
|
277
|
-
await this.loadAssets();
|
|
286
|
+
const textures = await this.loadAssets();
|
|
287
|
+
assertRuntimeStartupActive(this);
|
|
288
|
+
this.textures = textures;
|
|
278
289
|
this.trackAsset = new ProceduralTrackAsset({ textures: this.textures, world: WORLD });
|
|
279
290
|
this.worldLayer.addChild(
|
|
280
291
|
this.trackAsset.container,
|
|
@@ -286,6 +297,10 @@ export class F1SimulatorApp {
|
|
|
286
297
|
this.carLayer,
|
|
287
298
|
);
|
|
288
299
|
this.createCars();
|
|
300
|
+
if (this.options.gridWalk?.enabled) {
|
|
301
|
+
this.gridWalk = new GridWalkController(this);
|
|
302
|
+
this.gridWalk.sync({ updateTicker: false });
|
|
303
|
+
}
|
|
289
304
|
this.bindControls();
|
|
290
305
|
this.syncTimingPanelDisclosureState();
|
|
291
306
|
this.renderTrack();
|
|
@@ -298,7 +313,9 @@ export class F1SimulatorApp {
|
|
|
298
313
|
this.renderInitialFrame(snapshot);
|
|
299
314
|
this.completeComponentLoading();
|
|
300
315
|
this.emitHostCallback('onLoadingChange', { loading: false, phase: 'ready' });
|
|
316
|
+
assertRuntimeStartupActive(this);
|
|
301
317
|
this.emitHostCallback('onReady', { snapshot });
|
|
318
|
+
assertRuntimeStartupActive(this);
|
|
302
319
|
this.resizeHandler = () => {
|
|
303
320
|
this.syncTimingPanelDisclosureState();
|
|
304
321
|
this.renderSourceSnapshotBuffer ??= {};
|
|
@@ -315,27 +332,18 @@ export class F1SimulatorApp {
|
|
|
315
332
|
this.observeRuntimeVisibility();
|
|
316
333
|
}
|
|
317
334
|
} catch (error) {
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
const cancelled = this.destroyed;
|
|
336
|
+
if (!(error instanceof RuntimeStartupCancelledError) && !this.destroyed) {
|
|
337
|
+
this.emitHostCallback('onLoadingChange', { loading: false, phase: 'error' });
|
|
338
|
+
this.emitHostCallback('onError', error, { phase: 'init' });
|
|
339
|
+
}
|
|
320
340
|
this.destroy();
|
|
321
|
-
throw error;
|
|
341
|
+
throw cancelled ? new RuntimeStartupCancelledError() : error;
|
|
322
342
|
}
|
|
323
343
|
}
|
|
324
344
|
|
|
325
345
|
emitHostCallback(name, ...args) {
|
|
326
|
-
|
|
327
|
-
if (typeof callback !== 'function') return;
|
|
328
|
-
try {
|
|
329
|
-
callback(...args);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
if (name !== 'onError' && typeof this.options?.onError === 'function') {
|
|
332
|
-
try {
|
|
333
|
-
this.options.onError(error, { callback: name });
|
|
334
|
-
} catch {
|
|
335
|
-
// Host callbacks should not break the simulator runtime.
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
346
|
+
this.hostLifecycle?.emitCallback(name, ...args);
|
|
339
347
|
}
|
|
340
348
|
|
|
341
349
|
completeComponentLoading() {
|
|
@@ -362,7 +370,7 @@ export class F1SimulatorApp {
|
|
|
362
370
|
}
|
|
363
371
|
|
|
364
372
|
async loadAssets() {
|
|
365
|
-
|
|
373
|
+
return loadAppTextures(this.assets);
|
|
366
374
|
}
|
|
367
375
|
|
|
368
376
|
createCars() {
|
|
@@ -376,106 +384,54 @@ export class F1SimulatorApp {
|
|
|
376
384
|
|
|
377
385
|
bindControls() {
|
|
378
386
|
const eventOptions = { signal: this.abortController.signal };
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
387
|
+
bindWorkbenchControls({
|
|
388
|
+
safetyButtons: this.safetyButtons,
|
|
389
|
+
restartButton: this.restartButton,
|
|
390
|
+
restartButtons: this.restartButtons,
|
|
391
|
+
cameraButtons: this.cameraButtons,
|
|
392
|
+
zoomInButton: this.zoomInButton,
|
|
393
|
+
zoomInButtons: this.zoomInButtons,
|
|
394
|
+
zoomOutButton: this.zoomOutButton,
|
|
395
|
+
zoomOutButtons: this.zoomOutButtons,
|
|
396
|
+
simulationSpeedButtons: this.simulationSpeedButtons,
|
|
397
|
+
bannerMuteButtons: this.bannerMuteButtons,
|
|
398
|
+
overviewModeButtons: this.overviewModeButtons,
|
|
399
|
+
timingGapModeButtons: this.timingGapModeButtons,
|
|
400
|
+
timingLists: this.timingLists,
|
|
401
|
+
timingPanelToggles: this.readouts.timingPanelToggles,
|
|
402
|
+
raceDataOpens: this.readouts.raceDataOpens,
|
|
403
|
+
openButton: this.openButton,
|
|
404
|
+
raceDataDismisses: this.readouts.raceDataDismisses,
|
|
405
|
+
raceDataDismiss: this.readouts.raceDataDismiss,
|
|
406
|
+
telemetryDrawerToggle: this.readouts.telemetryDrawerToggle,
|
|
407
|
+
}, {
|
|
408
|
+
toggleSafetyCar: () => {
|
|
382
409
|
const next = this.sim.snapshot().raceControl.mode !== 'safety-car';
|
|
383
410
|
this.setSafetyCarDeployed(next);
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.syncSafetyCarControls(false);
|
|
392
|
-
this.drsTrails.clear();
|
|
393
|
-
this.trailLayer?.clear();
|
|
394
|
-
this.lastTimingRenderTime = 0;
|
|
395
|
-
this.lastTimingRaceMode = null;
|
|
396
|
-
this.lastTimingPenaltyKey = '';
|
|
397
|
-
this.lastTimingOrderKey = '';
|
|
398
|
-
this.renderTrack();
|
|
399
|
-
}, eventOptions);
|
|
400
|
-
|
|
401
|
-
this.cameraButtons.forEach((button) => {
|
|
402
|
-
button.addEventListener('click', () => {
|
|
403
|
-
this.setCameraMode(button.dataset.cameraMode);
|
|
404
|
-
}, eventOptions);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
this.zoomInButton?.addEventListener('click', () => {
|
|
408
|
-
this.adjustCameraZoom(1);
|
|
409
|
-
}, eventOptions);
|
|
410
|
-
|
|
411
|
-
this.zoomOutButton?.addEventListener('click', () => {
|
|
412
|
-
this.adjustCameraZoom(-1);
|
|
413
|
-
}, eventOptions);
|
|
414
|
-
|
|
415
|
-
this.simulationSpeedButtons?.forEach((button) => {
|
|
416
|
-
button.addEventListener('click', () => {
|
|
417
|
-
this.cycleSimulationSpeed();
|
|
418
|
-
}, eventOptions);
|
|
419
|
-
});
|
|
420
|
-
this.updateSimulationSpeedControls();
|
|
421
|
-
|
|
422
|
-
this.bannerMuteButtons?.forEach((button) => {
|
|
423
|
-
button.addEventListener('click', () => {
|
|
411
|
+
},
|
|
412
|
+
restart: () => this.restart(),
|
|
413
|
+
setCameraMode: (mode) => this.setCameraMode(mode),
|
|
414
|
+
adjustCameraZoom: (direction) => this.adjustCameraZoom(direction),
|
|
415
|
+
cycleSimulationSpeed: () => this.cycleSimulationSpeed(),
|
|
416
|
+
updateSimulationSpeedControls: () => this.updateSimulationSpeedControls(),
|
|
417
|
+
toggleRaceDataBanners: () => {
|
|
424
418
|
this.setRaceDataBannersMuted(!this.raceDataBannersMuted);
|
|
425
|
-
},
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
this.overviewModeButtons.forEach((button) => {
|
|
429
|
-
button.addEventListener('click', () => {
|
|
430
|
-
this.overviewMode = button.dataset.overviewMode === 'driver' ? 'driver' : 'vehicle';
|
|
419
|
+
},
|
|
420
|
+
setOverviewMode: (mode) => {
|
|
421
|
+
this.overviewMode = mode;
|
|
431
422
|
const selected = this.sim?.snapshot().cars.find((car) => car.id === this.selectedId);
|
|
432
423
|
this.renderCarDriverOverview(selected);
|
|
433
|
-
},
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const row = event.target instanceof Element ? event.target.closest('[data-driver-id]') : null;
|
|
445
|
-
if (!row) return;
|
|
446
|
-
this.selectCar(row.dataset.driverId, { focus: true });
|
|
447
|
-
}, eventOptions);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
this.readouts.timingPanelToggles?.forEach?.((toggle) => {
|
|
451
|
-
toggle.addEventListener?.('click', () => {
|
|
452
|
-
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
453
|
-
this.setTimingPanelOpen(!racePanel?.classList?.contains?.('is-timing-panel-open'), racePanel);
|
|
454
|
-
}, eventOptions);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
const raceDataOpenButtons = uniqueNodes(this.readouts.raceDataOpens?.length
|
|
458
|
-
? this.readouts.raceDataOpens
|
|
459
|
-
: [this.openButton]);
|
|
460
|
-
raceDataOpenButtons.forEach((button) => button.addEventListener('click', () => {
|
|
461
|
-
const driver = this.driverById.get(this.activeRaceDataId ?? this.selectedId);
|
|
462
|
-
if (!driver) return;
|
|
463
|
-
this.options.onDriverOpen?.(driver);
|
|
464
|
-
}, eventOptions));
|
|
465
|
-
|
|
466
|
-
const raceDataDismissButtons = uniqueNodes(this.readouts.raceDataDismisses?.length
|
|
467
|
-
? this.readouts.raceDataDismisses
|
|
468
|
-
: [this.readouts.raceDataDismiss]);
|
|
469
|
-
raceDataDismissButtons.forEach((button) => button.addEventListener('click', (event) => {
|
|
470
|
-
event.preventDefault();
|
|
471
|
-
event.stopPropagation();
|
|
472
|
-
this.dismissRaceDataPanel(performance.now());
|
|
473
|
-
}, eventOptions));
|
|
474
|
-
|
|
475
|
-
this.readouts.telemetryDrawerToggle?.addEventListener('click', () => {
|
|
476
|
-
this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen);
|
|
477
|
-
}, eventOptions);
|
|
478
|
-
|
|
424
|
+
},
|
|
425
|
+
toggleTimingGapMode: () => this.toggleTimingGapMode(),
|
|
426
|
+
selectCar: (id) => this.selectCar(id, { focus: true }),
|
|
427
|
+
setTimingPanelOpen: (open, panel) => this.setTimingPanelOpen(open, panel),
|
|
428
|
+
openDriver: () => {
|
|
429
|
+
const driver = this.driverById.get(this.activeRaceDataId ?? this.selectedId);
|
|
430
|
+
if (driver) this.emitHostCallback('onDriverOpen', driver);
|
|
431
|
+
},
|
|
432
|
+
dismissRaceDataPanel: (now) => this.dismissRaceDataPanel(now),
|
|
433
|
+
toggleTelemetryDrawer: () => this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen),
|
|
434
|
+
}, eventOptions.signal);
|
|
479
435
|
this.bindCameraWheelControls(eventOptions);
|
|
480
436
|
}
|
|
481
437
|
|
|
@@ -511,31 +467,20 @@ export class F1SimulatorApp {
|
|
|
511
467
|
}
|
|
512
468
|
|
|
513
469
|
bindCameraWheelControls(eventOptions) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
470
|
+
bindCameraWheelControls(this.canvasHost, (direction) => {
|
|
471
|
+
const before = this.camera.zoom;
|
|
472
|
+
return this.adjustCameraZoom(direction) !== before;
|
|
473
|
+
}, eventOptions,
|
|
474
|
+
() => !this.gridWalk?.active);
|
|
519
475
|
}
|
|
520
476
|
|
|
521
477
|
setTelemetryDrawerOpen(open) {
|
|
522
478
|
this.telemetryDrawerOpen = Boolean(open);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.readouts.telemetryDrawer
|
|
526
|
-
this.readouts.
|
|
527
|
-
}
|
|
528
|
-
this.readouts.telemetryDrawer?.setAttribute?.('inert', '');
|
|
529
|
-
this.readouts.telemetryDrawer?.setAttribute?.('aria-hidden', 'true');
|
|
530
|
-
}
|
|
531
|
-
this.readouts.telemetryDrawerToggle?.setAttribute?.('aria-expanded', String(this.telemetryDrawerOpen));
|
|
532
|
-
if (this.readouts.telemetryDrawerToggle) {
|
|
533
|
-
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close' : 'Telemetry';
|
|
534
|
-
this.readouts.telemetryDrawerToggle.setAttribute(
|
|
535
|
-
'aria-label',
|
|
536
|
-
this.telemetryDrawerOpen ? 'Close telemetry' : 'Open telemetry',
|
|
537
|
-
);
|
|
538
|
-
}
|
|
479
|
+
renderTelemetryDrawerDisclosure({
|
|
480
|
+
workbench: this.readouts.telemetryDrawerWorkbench,
|
|
481
|
+
drawer: this.readouts.telemetryDrawer,
|
|
482
|
+
toggle: this.readouts.telemetryDrawerToggle,
|
|
483
|
+
}, this.telemetryDrawerOpen);
|
|
539
484
|
this.invalidateCameraSafeArea();
|
|
540
485
|
this.syncRendererToCurrentLayout({ render: true });
|
|
541
486
|
this.scheduleLayoutResizeSync(380);
|
|
@@ -550,29 +495,15 @@ export class F1SimulatorApp {
|
|
|
550
495
|
}
|
|
551
496
|
|
|
552
497
|
isTimingPanelDisclosureMode(toggle = this.readouts.timingPanelToggle) {
|
|
553
|
-
|
|
554
|
-
const style = getComputedStyle(toggle);
|
|
555
|
-
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
498
|
+
return isTimingPanelDisclosureMode(toggle);
|
|
556
499
|
}
|
|
557
500
|
|
|
558
501
|
syncTimingPanelDisclosureState(targetPanel = null) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const disclosureMode = this.isTimingPanelDisclosureMode(toggle);
|
|
565
|
-
const open = disclosureMode && Boolean(racePanel?.classList?.contains?.('is-timing-panel-open'));
|
|
566
|
-
racePanel?.classList?.toggle?.('is-timing-panel-open', open);
|
|
567
|
-
if (!disclosureMode || open) {
|
|
568
|
-
tower?.removeAttribute?.('inert');
|
|
569
|
-
tower?.setAttribute?.('aria-hidden', 'false');
|
|
570
|
-
} else {
|
|
571
|
-
tower?.setAttribute?.('inert', '');
|
|
572
|
-
tower?.setAttribute?.('aria-hidden', 'true');
|
|
573
|
-
}
|
|
574
|
-
toggle.setAttribute?.('aria-expanded', String(open));
|
|
575
|
-
}
|
|
502
|
+
syncTimingPanelDisclosureState({
|
|
503
|
+
toggles: this.readouts.timingPanelToggles,
|
|
504
|
+
targetPanel,
|
|
505
|
+
isDisclosureMode: (toggle) => this.isTimingPanelDisclosureMode(toggle),
|
|
506
|
+
});
|
|
576
507
|
}
|
|
577
508
|
|
|
578
509
|
setRaceDataBannersMuted(muted, now = performance.now()) {
|
|
@@ -586,6 +517,7 @@ export class F1SimulatorApp {
|
|
|
586
517
|
this.resetRaceDataBannerState(now);
|
|
587
518
|
}
|
|
588
519
|
this.updateBannerMuteControls();
|
|
520
|
+
if (this.gridWalk?.active) this.gridWalk.sync();
|
|
589
521
|
}
|
|
590
522
|
|
|
591
523
|
resizeRendererToCanvasHost() {
|
|
@@ -606,6 +538,11 @@ export class F1SimulatorApp {
|
|
|
606
538
|
}
|
|
607
539
|
|
|
608
540
|
syncRendererToCurrentLayout({ render = false } = {}) {
|
|
541
|
+
if (this.gridWalk?.active) {
|
|
542
|
+
this.gridWalk.render();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.gridWalk?.syncLayout();
|
|
609
546
|
this.resizeRendererToCanvasHost();
|
|
610
547
|
this.renderSourceSnapshotBuffer ??= {};
|
|
611
548
|
const snapshot = this.sim?.snapshotRenderInto?.(this.renderSourceSnapshotBuffer) ?? this.sim?.snapshotRender?.() ?? this.sim?.snapshot?.();
|
|
@@ -626,10 +563,15 @@ export class F1SimulatorApp {
|
|
|
626
563
|
|
|
627
564
|
scheduleLayoutResizeSync(durationMs = 360) {
|
|
628
565
|
if (typeof requestAnimationFrame !== 'function') return;
|
|
629
|
-
|
|
566
|
+
this.layoutResizeSyncUntil = Math.max(
|
|
567
|
+
this.layoutResizeSyncUntil ?? 0,
|
|
568
|
+
performance.now() + durationMs,
|
|
569
|
+
);
|
|
570
|
+
if (this.layoutResizeFrame != null) return;
|
|
630
571
|
const run = (timestamp = performance.now()) => {
|
|
572
|
+
this.layoutResizeFrame = null;
|
|
631
573
|
this.syncRendererToCurrentLayout({ render: true });
|
|
632
|
-
if (timestamp
|
|
574
|
+
if (timestamp < this.layoutResizeSyncUntil) {
|
|
633
575
|
this.layoutResizeFrame = requestAnimationFrame(run);
|
|
634
576
|
}
|
|
635
577
|
};
|
|
@@ -664,7 +606,6 @@ export class F1SimulatorApp {
|
|
|
664
606
|
resetFrameClock(now = performance.now()) {
|
|
665
607
|
this.accumulator = 0;
|
|
666
608
|
this.lastTime = now;
|
|
667
|
-
this.nextGameFrameTime = now + TARGET_FRAME_MS;
|
|
668
609
|
this.fps.current = 0;
|
|
669
610
|
this.fps.frames = 0;
|
|
670
611
|
this.fps.lastSample = now;
|
|
@@ -675,7 +616,10 @@ export class F1SimulatorApp {
|
|
|
675
616
|
}
|
|
676
617
|
|
|
677
618
|
renderExpertFrame(snapshot = this.sim?.snapshot(), { forceDomUpdate = false, observation } = {}) {
|
|
678
|
-
if (!snapshot) return;
|
|
619
|
+
if (this.destroyed || !snapshot) return;
|
|
620
|
+
const simulation = this.sim;
|
|
621
|
+
this.emitSnapshotLifecycle(snapshot);
|
|
622
|
+
if (this.destroyed || this.sim !== simulation) return;
|
|
679
623
|
const now = performance.now();
|
|
680
624
|
this.sampleFps(now);
|
|
681
625
|
const renderSnapshot = interpolateRenderSnapshotInto(this.renderSnapshotBuffer, snapshot, 0);
|
|
@@ -709,6 +653,7 @@ export class F1SimulatorApp {
|
|
|
709
653
|
}
|
|
710
654
|
|
|
711
655
|
renderCars(snapshot) {
|
|
656
|
+
if (this.gridWalk?.active) snapshot = this.gridWalk.session.snapshot;
|
|
712
657
|
this.replayGhostRenderer.render(snapshot, {
|
|
713
658
|
textures: this.textures,
|
|
714
659
|
replayGhostLayer: this.replayGhostLayer,
|
|
@@ -716,13 +661,10 @@ export class F1SimulatorApp {
|
|
|
716
661
|
this.carRenderer.renderCars(snapshot, {
|
|
717
662
|
textures: this.textures,
|
|
718
663
|
carLayer: this.carLayer,
|
|
664
|
+
snapToPose: Boolean(this.gridWalk?.active),
|
|
719
665
|
});
|
|
720
666
|
}
|
|
721
667
|
|
|
722
|
-
renderServiceCountdownLabel(car) {
|
|
723
|
-
this.carRenderer.renderServiceCountdownLabel(car);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
668
|
renderExpertSensorRays(snapshot, observation) {
|
|
727
669
|
renderExpertSensorRays({
|
|
728
670
|
snapshot,
|
|
@@ -740,9 +682,14 @@ export class F1SimulatorApp {
|
|
|
740
682
|
this.drsTrailRenderer.render(snapshot, this.trailLayer);
|
|
741
683
|
}
|
|
742
684
|
|
|
743
|
-
applyCamera(snapshot, { immediate = false } = {}) {
|
|
685
|
+
applyCamera(snapshot, { immediate = false, elapsedMs } = {}) {
|
|
686
|
+
if (this.gridWalk?.active) {
|
|
687
|
+
this.gridWalk.applyCamera();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
744
690
|
this.cameraController.applyToWorldLayer(this.worldLayer, snapshot, {
|
|
745
691
|
immediate,
|
|
692
|
+
elapsedMs,
|
|
746
693
|
selectedId: this.selectedId,
|
|
747
694
|
});
|
|
748
695
|
}
|
|
@@ -771,11 +718,9 @@ export class F1SimulatorApp {
|
|
|
771
718
|
return this.cameraController.getPitBounds(pitLane);
|
|
772
719
|
}
|
|
773
720
|
|
|
774
|
-
getPitCameraFrame(pitLane, height, baseScale, safeArea, screenCenterX, minimumScale = null) {
|
|
775
|
-
return this.cameraController.getPitFrame(pitLane, height, baseScale, safeArea, screenCenterX, minimumScale);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
721
|
updateDom(snapshot, { emitLifecycle = true } = {}) {
|
|
722
|
+
if (this.destroyed) return;
|
|
723
|
+
const simulation = this.sim;
|
|
779
724
|
snapshot = normalizeRuntimeSnapshot(snapshot, this.totalLaps);
|
|
780
725
|
const leader = snapshot.cars[0];
|
|
781
726
|
const selected = snapshot.cars.find((car) => car.id === this.selectedId) ?? leader;
|
|
@@ -784,6 +729,7 @@ export class F1SimulatorApp {
|
|
|
784
729
|
const now = performance.now();
|
|
785
730
|
|
|
786
731
|
if (emitLifecycle) this.emitSnapshotLifecycle(snapshot);
|
|
732
|
+
if (this.destroyed || this.sim !== simulation) return;
|
|
787
733
|
this.syncThemeContext(snapshot);
|
|
788
734
|
|
|
789
735
|
if (
|
|
@@ -810,18 +756,19 @@ export class F1SimulatorApp {
|
|
|
810
756
|
|
|
811
757
|
this.updateCameraControls(snapshot);
|
|
812
758
|
this.syncTimingGapModeControls();
|
|
759
|
+
const currentTimingRaceMode = timingRaceMode(snapshot);
|
|
813
760
|
const timingPenaltyKey = this.getTimingPenaltyKey(snapshot.penalties ?? []);
|
|
814
761
|
const timingOrderKey = this.getTimingOrderKey(snapshot.cars);
|
|
815
762
|
const timingUpdateInterval = timingUpdateIntervalForSpeed(this.simulationSpeed);
|
|
816
763
|
if (
|
|
817
764
|
now - this.lastTimingRenderTime >= timingUpdateInterval ||
|
|
818
|
-
this.lastTimingRaceMode !==
|
|
765
|
+
this.lastTimingRaceMode !== currentTimingRaceMode ||
|
|
819
766
|
this.lastTimingPenaltyKey !== timingPenaltyKey ||
|
|
820
767
|
this.lastTimingOrderKey !== timingOrderKey
|
|
821
768
|
) {
|
|
822
|
-
this.renderTiming(snapshot.cars,
|
|
769
|
+
this.renderTiming(snapshot.cars, currentTimingRaceMode, snapshot.penalties ?? []);
|
|
823
770
|
this.lastTimingRenderTime = now;
|
|
824
|
-
this.lastTimingRaceMode =
|
|
771
|
+
this.lastTimingRaceMode = currentTimingRaceMode;
|
|
825
772
|
this.lastTimingPenaltyKey = timingPenaltyKey;
|
|
826
773
|
this.lastTimingOrderKey = timingOrderKey;
|
|
827
774
|
}
|
|
@@ -838,48 +785,11 @@ export class F1SimulatorApp {
|
|
|
838
785
|
}
|
|
839
786
|
|
|
840
787
|
emitSnapshotLifecycle(snapshot) {
|
|
841
|
-
|
|
842
|
-
const leaderLap = leader?.lap;
|
|
843
|
-
if (Number.isFinite(leaderLap)) {
|
|
844
|
-
if (this.lastLeaderLap != null && leaderLap !== this.lastLeaderLap) {
|
|
845
|
-
this.emitHostCallback('onLapChange', {
|
|
846
|
-
previousLeaderLap: this.lastLeaderLap,
|
|
847
|
-
leaderLap,
|
|
848
|
-
leader,
|
|
849
|
-
snapshot,
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
this.lastLeaderLap = leaderLap;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
this.emitRaceEvents(snapshot.events, snapshot);
|
|
856
|
-
|
|
857
|
-
if (snapshot.raceControl.finished && !this.raceFinishEmitted) {
|
|
858
|
-
this.raceFinishEmitted = true;
|
|
859
|
-
this.emitHostCallback('onRaceFinish', {
|
|
860
|
-
winner: snapshot.raceControl.winner,
|
|
861
|
-
classification: snapshot.raceControl.classification ?? [],
|
|
862
|
-
snapshot,
|
|
863
|
-
});
|
|
864
|
-
}
|
|
788
|
+
this.hostLifecycle?.emitSnapshot(snapshot);
|
|
865
789
|
}
|
|
866
790
|
|
|
867
791
|
emitRaceEvents(events = [], snapshot) {
|
|
868
|
-
|
|
869
|
-
const key = `${event.type}:${event.at ?? snapshot.time}:${event.carId ?? ''}:${event.otherCarId ?? ''}:${event.winnerId ?? ''}`;
|
|
870
|
-
if (this.emittedRaceEventKeys.has(key)) return;
|
|
871
|
-
this.emittedRaceEventKeys.add(key);
|
|
872
|
-
this.emitHostCallback('onRaceEvent', event, snapshot);
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
renderRaceFinish(snapshot) {
|
|
877
|
-
this.lastFinishClassificationMarkup = renderRaceFinish({
|
|
878
|
-
readouts: this.readouts,
|
|
879
|
-
snapshot,
|
|
880
|
-
driverById: this.driverById,
|
|
881
|
-
lastFinishClassificationMarkup: this.lastFinishClassificationMarkup,
|
|
882
|
-
});
|
|
792
|
+
this.hostLifecycle?.emitRaceEvents(events, snapshot);
|
|
883
793
|
}
|
|
884
794
|
|
|
885
795
|
sampleFps(now) {
|
|
@@ -891,25 +801,29 @@ export class F1SimulatorApp {
|
|
|
891
801
|
this.fps.lastSample = now;
|
|
892
802
|
}
|
|
893
803
|
|
|
894
|
-
renderStartLights(raceControl) {
|
|
895
|
-
renderStartLights(this.readouts, this.startLightNodes, raceControl);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
804
|
selectCar(id, { focus = false } = {}) {
|
|
805
|
+
if (!this.driverById.has(id)) return;
|
|
899
806
|
this.selectedId = id;
|
|
900
807
|
this.activeRaceDataId = this.isRaceDataBannerEnabled('project') ? id : null;
|
|
901
808
|
this.lastRaceDataInteraction = performance.now();
|
|
902
809
|
if (this.isRaceDataBannerEnabled('radio')) this.scheduleRadioBreak(this.lastRaceDataInteraction);
|
|
903
|
-
if (focus) {
|
|
810
|
+
if (focus && !this.gridWalk?.active) {
|
|
904
811
|
this.setCameraMode('selected');
|
|
905
812
|
}
|
|
906
813
|
this.lastTimingRenderTime = 0;
|
|
907
|
-
const
|
|
814
|
+
const simulation = this.sim;
|
|
815
|
+
const snapshot = simulation.snapshot();
|
|
908
816
|
this.updateDom(snapshot);
|
|
817
|
+
if (this.destroyed || this.sim !== simulation) return;
|
|
909
818
|
const driver = this.driverById.get(id);
|
|
910
819
|
if (driver) this.emitHostCallback('onDriverSelect', driver, snapshot);
|
|
820
|
+
if (!this.destroyed && this.sim === simulation) this.gridWalk?.select(id, { focus });
|
|
911
821
|
}
|
|
912
822
|
|
|
823
|
+
setView(view) { return this.gridWalk?.setView(view) ?? false; }
|
|
824
|
+
|
|
825
|
+
getView() { return this.gridWalk?.session.view ?? 'race'; }
|
|
826
|
+
|
|
913
827
|
renderTiming(cars, raceMode, penalties = []) {
|
|
914
828
|
this.syncTimingGapModeControls();
|
|
915
829
|
this.lastTimingMarkup = renderTimingTowers({
|
|
@@ -943,7 +857,7 @@ export class F1SimulatorApp {
|
|
|
943
857
|
this.lastTimingRenderTime = 0;
|
|
944
858
|
this.syncTimingGapModeControls();
|
|
945
859
|
const snapshot = this.sim?.snapshot?.();
|
|
946
|
-
if (snapshot) this.renderTiming(snapshot.cars, snapshot
|
|
860
|
+
if (snapshot) this.renderTiming(snapshot.cars, timingRaceMode(snapshot), snapshot.penalties ?? []);
|
|
947
861
|
return this.timingGapMode;
|
|
948
862
|
}
|
|
949
863
|
|
|
@@ -977,7 +891,6 @@ export class F1SimulatorApp {
|
|
|
977
891
|
renderTelemetryReadouts({
|
|
978
892
|
readouts: this.readouts,
|
|
979
893
|
car,
|
|
980
|
-
driverById: this.driverById,
|
|
981
894
|
});
|
|
982
895
|
this.renderCarDriverOverview(car);
|
|
983
896
|
this.renderLapTelemetry(car.lapTelemetry);
|
|
@@ -1072,51 +985,24 @@ export class F1SimulatorApp {
|
|
|
1072
985
|
return createPenaltyStewardMessage(penalty, this.driverById);
|
|
1073
986
|
}
|
|
1074
987
|
|
|
1075
|
-
createWarningStewardMessage(event) {
|
|
1076
|
-
return createWarningStewardMessage(event, this.driverById);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
988
|
renderActiveStewardMessage() {
|
|
1080
989
|
renderActiveStewardMessage(this.readouts, this.activeStewardMessage);
|
|
1081
990
|
}
|
|
1082
991
|
|
|
1083
992
|
updateRadioSchedule(now) {
|
|
1084
|
-
|
|
1085
|
-
this.radioState.visible = false;
|
|
1086
|
-
this.radioState.nextChangeAt = Number.POSITIVE_INFINITY;
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
if (
|
|
1090
|
-
Number.isFinite(this.radioState.nextChangeAt) &&
|
|
1091
|
-
now - this.radioState.nextChangeAt > RADIO_SCHEDULE_CATCHUP_LIMIT_MS
|
|
1092
|
-
) {
|
|
1093
|
-
if (this.radioState.visible) this.scheduleRadioBreak(now);
|
|
1094
|
-
else this.scheduleRadioPopup(now);
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
while (now >= this.radioState.nextChangeAt) {
|
|
1098
|
-
if (this.radioState.visible) {
|
|
1099
|
-
this.scheduleRadioBreak(this.radioState.nextChangeAt);
|
|
1100
|
-
} else {
|
|
1101
|
-
this.scheduleRadioPopup(this.radioState.nextChangeAt);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
scheduleRadioBreak(now) {
|
|
1107
|
-
scheduleRadioBreak({
|
|
993
|
+
updateRadioSchedule({
|
|
1108
994
|
radioState: this.radioState,
|
|
1109
995
|
now,
|
|
996
|
+
drivers: this.drivers,
|
|
1110
997
|
isEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
1111
998
|
nextRandom: () => this.nextRadioRandom(),
|
|
1112
999
|
});
|
|
1113
1000
|
}
|
|
1114
1001
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1002
|
+
scheduleRadioBreak(now) {
|
|
1003
|
+
scheduleRadioBreak({
|
|
1117
1004
|
radioState: this.radioState,
|
|
1118
1005
|
now,
|
|
1119
|
-
drivers: this.drivers,
|
|
1120
1006
|
isEnabled: (kind) => this.isRaceDataBannerEnabled(kind),
|
|
1121
1007
|
nextRandom: () => this.nextRadioRandom(),
|
|
1122
1008
|
});
|
|
@@ -1245,7 +1131,7 @@ export class F1SimulatorApp {
|
|
|
1245
1131
|
this.totalLaps = this.options.totalLaps;
|
|
1246
1132
|
this.assets = this.options.assets;
|
|
1247
1133
|
if (hasOwnOption(nextOptions, 'trackSeed')) {
|
|
1248
|
-
this.trackSeed = this.options.trackSeed ??
|
|
1134
|
+
this.trackSeed = this.options.trackSeed ?? createRuntimeSeed();
|
|
1249
1135
|
}
|
|
1250
1136
|
if (nextOptions.drivers) {
|
|
1251
1137
|
this.drivers = nextOptions.drivers;
|
|
@@ -1276,6 +1162,10 @@ export class F1SimulatorApp {
|
|
|
1276
1162
|
this.lastThemeContextKey = null;
|
|
1277
1163
|
this.syncThemeContext();
|
|
1278
1164
|
this.sim = this.createRaceSimulation();
|
|
1165
|
+
this.gridWalk?.destroy();
|
|
1166
|
+
this.gridWalk = null;
|
|
1167
|
+
this.gridWalk = this.options.gridWalk?.enabled ? new GridWalkController(this) : null;
|
|
1168
|
+
this.gridWalk?.sync();
|
|
1279
1169
|
if (this.expertMode) {
|
|
1280
1170
|
this.expert = createBrowserExpertAdapter(this, this.options.expert);
|
|
1281
1171
|
}
|
|
@@ -1287,18 +1177,19 @@ export class F1SimulatorApp {
|
|
|
1287
1177
|
this.lastTimingRaceMode = null;
|
|
1288
1178
|
this.lastTimingPenaltyKey = '';
|
|
1289
1179
|
this.lastTimingOrderKey = '';
|
|
1290
|
-
this.activePenaltyBanner = null;
|
|
1291
1180
|
this.lastPenaltyBannerId = null;
|
|
1292
1181
|
this.activeStewardMessage = null;
|
|
1293
1182
|
this.lastStewardMessageKey = null;
|
|
1294
|
-
this.
|
|
1295
|
-
this.emittedRaceEventKeys.clear();
|
|
1296
|
-
this.raceFinishEmitted = false;
|
|
1183
|
+
this.hostLifecycle.reset();
|
|
1297
1184
|
this.renderTrack();
|
|
1298
1185
|
const snapshot = this.sim.snapshot();
|
|
1299
1186
|
this.updateDom(snapshot);
|
|
1300
1187
|
this.lastDomUpdateTime = performance.now();
|
|
1188
|
+
// Restart may create fresh surfaces around the already-ready renderer.
|
|
1189
|
+
// Complete their loading state before measuring the final display frame.
|
|
1190
|
+
this.completeComponentLoading();
|
|
1301
1191
|
this.renderInitialFrame(snapshot);
|
|
1192
|
+
this.syncRuntimeTicker();
|
|
1302
1193
|
}
|
|
1303
1194
|
|
|
1304
1195
|
setSafetyCarDeployed(deployed) {
|
|
@@ -1348,34 +1239,88 @@ export class F1SimulatorApp {
|
|
|
1348
1239
|
}
|
|
1349
1240
|
|
|
1350
1241
|
destroy() {
|
|
1351
|
-
this.
|
|
1242
|
+
if (this.destroyed) return;
|
|
1243
|
+
this.destroyed = true;
|
|
1244
|
+
const lifecycleEmitter = this.hostLifecycle;
|
|
1245
|
+
const cleanupErrors = [];
|
|
1246
|
+
this.destroyCleanupErrors = cleanupErrors;
|
|
1247
|
+
const cleanup = (run) => {
|
|
1248
|
+
try { run(); } catch (error) { cleanupErrors.push(error); }
|
|
1249
|
+
};
|
|
1250
|
+
cleanup(() => this.gridWalk?.destroy());
|
|
1251
|
+
this.gridWalk = null;
|
|
1252
|
+
cleanup(() => this.abortController?.abort?.());
|
|
1352
1253
|
if (this.layoutResizeFrame && typeof cancelAnimationFrame === 'function') {
|
|
1353
|
-
cancelAnimationFrame(this.layoutResizeFrame);
|
|
1254
|
+
cleanup(() => cancelAnimationFrame(this.layoutResizeFrame));
|
|
1354
1255
|
}
|
|
1355
1256
|
this.layoutResizeFrame = null;
|
|
1257
|
+
this.layoutResizeSyncUntil = 0;
|
|
1356
1258
|
if (this.runtimeVisibilitySyncFrame && typeof cancelAnimationFrame === 'function') {
|
|
1357
|
-
cancelAnimationFrame(this.runtimeVisibilitySyncFrame);
|
|
1259
|
+
cleanup(() => cancelAnimationFrame(this.runtimeVisibilitySyncFrame));
|
|
1358
1260
|
}
|
|
1359
1261
|
this.runtimeVisibilitySyncFrame = null;
|
|
1360
|
-
this.layoutResizeObserver?.disconnect?.();
|
|
1262
|
+
cleanup(() => this.layoutResizeObserver?.disconnect?.());
|
|
1361
1263
|
this.layoutResizeObserver = null;
|
|
1362
|
-
this.layoutSupportCleanup?.();
|
|
1264
|
+
cleanup(() => this.layoutSupportCleanup?.());
|
|
1363
1265
|
this.layoutSupportCleanup = null;
|
|
1364
|
-
this.raceOverlayClearanceCleanup?.();
|
|
1266
|
+
cleanup(() => this.raceOverlayClearanceCleanup?.());
|
|
1365
1267
|
this.raceOverlayClearanceCleanup = null;
|
|
1366
|
-
this.visibilityObserver?.disconnect?.();
|
|
1268
|
+
cleanup(() => this.visibilityObserver?.disconnect?.());
|
|
1367
1269
|
this.visibilityObserver = null;
|
|
1368
1270
|
if (this.app && this.tickerCallback) {
|
|
1369
|
-
this.app.ticker.remove(this.tickerCallback);
|
|
1271
|
+
cleanup(() => this.app.ticker.remove(this.tickerCallback));
|
|
1370
1272
|
}
|
|
1371
|
-
this.expert?.destroy?.();
|
|
1273
|
+
cleanup(() => this.expert?.destroy?.());
|
|
1372
1274
|
this.expert = null;
|
|
1373
|
-
this.replayGhostRenderer
|
|
1374
|
-
this.
|
|
1275
|
+
cleanup(() => this.replayGhostRenderer?.destroy?.());
|
|
1276
|
+
if (this.pixiStartupLifecycle) cleanup(() => this.pixiStartupLifecycle.destroy());
|
|
1277
|
+
else cleanup(() => this.app?.destroy(true, { children: true, texture: false }));
|
|
1278
|
+
this.pixiStartupLifecycle = null;
|
|
1375
1279
|
this.app = null;
|
|
1376
1280
|
this.carSprites.clear();
|
|
1377
1281
|
this.carHitAreas.clear();
|
|
1378
1282
|
this.serviceCountdownLabels.clear();
|
|
1379
1283
|
this.drsTrails.clear();
|
|
1284
|
+
this.sim = null;
|
|
1285
|
+
this.root = null;
|
|
1286
|
+
this.canvasHost = null;
|
|
1287
|
+
this.readouts = {};
|
|
1288
|
+
this.safetyButtons = [];
|
|
1289
|
+
this.restartButton = null;
|
|
1290
|
+
this.restartButtons = [];
|
|
1291
|
+
this.openButton = null;
|
|
1292
|
+
this.timingList = null;
|
|
1293
|
+
this.timingLists = [];
|
|
1294
|
+
this.timingGapModeButtons = [];
|
|
1295
|
+
this.cameraButtons = [];
|
|
1296
|
+
this.overviewModeButtons = [];
|
|
1297
|
+
this.bannerMuteButtons = [];
|
|
1298
|
+
this.simulationSpeedButtons = [];
|
|
1299
|
+
this.zoomInButton = null;
|
|
1300
|
+
this.zoomInButtons = [];
|
|
1301
|
+
this.zoomOutButton = null;
|
|
1302
|
+
this.zoomOutButtons = [];
|
|
1303
|
+
this.startLightNodes = [];
|
|
1304
|
+
this.worldLayer = null;
|
|
1305
|
+
this.trackAsset = null;
|
|
1306
|
+
this.drsLayer = null;
|
|
1307
|
+
this.trailLayer = null;
|
|
1308
|
+
this.sensorLayer = null;
|
|
1309
|
+
this.pitLaneStatusLayer = null;
|
|
1310
|
+
this.replayGhostLayer = null;
|
|
1311
|
+
this.carLayer = null;
|
|
1312
|
+
this.textures = {};
|
|
1313
|
+
this.renderSnapshotBuffer = null;
|
|
1314
|
+
this.renderSourceSnapshotBuffer = null;
|
|
1315
|
+
this.cameraController = null;
|
|
1316
|
+
this.camera = null;
|
|
1317
|
+
this.carRenderer = null;
|
|
1318
|
+
this.drsTrailRenderer = null;
|
|
1319
|
+
this.replayGhostRenderer = null;
|
|
1320
|
+
this.pitLaneStatusRenderer = null;
|
|
1321
|
+
this.hostLifecycle = null;
|
|
1322
|
+
this.abortController = null;
|
|
1323
|
+
this.destroyCleanupErrors = null;
|
|
1324
|
+
cleanupErrors.forEach((error) => lifecycleEmitter?.emitCallback('onError', error, { phase: 'destroy' }));
|
|
1380
1325
|
}
|
|
1381
1326
|
}
|