@inventure71/paddockjs 0.1.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/AGENTS.md +86 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +176 -0
- package/README.md +165 -0
- package/assets/f1-broadcast-panel-surface.png +0 -0
- package/assets/f1-car-sprite-game.png +0 -0
- package/assets/f1-driver-helmet.png +0 -0
- package/assets/f1-safety-car-sprite.png +0 -0
- package/assets/f1-texture-asphalt.png +0 -0
- package/assets/f1Logo.png +0 -0
- package/docs/architecture.md +209 -0
- package/docs/concepts.md +178 -0
- package/docs/data_contract.md +360 -0
- package/docs/data_model.md +167 -0
- package/docs/index.md +15 -0
- package/docs/learnings.md +47 -0
- package/docs/rules.md +181 -0
- package/docs/system_specs.md +211 -0
- package/package.json +85 -0
- package/src/api/PaddockSimulatorController.js +233 -0
- package/src/app/F1SimulatorApp.js +1293 -0
- package/src/app/domBindings.js +64 -0
- package/src/config/defaultAssets.js +29 -0
- package/src/config/defaultOptions.js +184 -0
- package/src/data/championship.js +225 -0
- package/src/data/customFields.js +14 -0
- package/src/data/demoDrivers.js +134 -0
- package/src/data/driverData.js +71 -0
- package/src/data/normalizeDrivers.js +21 -0
- package/src/data/vehicleData.js +81 -0
- package/src/index.d.ts +399 -0
- package/src/index.js +116 -0
- package/src/rendering/proceduralTrackAsset.js +517 -0
- package/src/rendering/renderSnapshot.js +23 -0
- package/src/simulation/driverController.js +253 -0
- package/src/simulation/raceSimulation.js +933 -0
- package/src/simulation/simMath.js +34 -0
- package/src/simulation/trackModel.js +607 -0
- package/src/simulation/units.js +30 -0
- package/src/simulation/vehiclePhysics.js +104 -0
- package/src/standalone.js +15 -0
- package/src/styles.css +1818 -0
- package/src/ui/componentTemplates.js +229 -0
- package/src/ui/shellTemplate.js +43 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function querySimulatorDom(root) {
|
|
2
|
+
const readouts = {
|
|
3
|
+
timingTower: root.querySelector('[data-timing-tower]'),
|
|
4
|
+
mode: root.querySelector('[data-race-mode]'),
|
|
5
|
+
startLights: root.querySelector('[data-start-lights]'),
|
|
6
|
+
startLightsLabel: root.querySelector('[data-start-lights-label]'),
|
|
7
|
+
towerLap: root.querySelector('[data-tower-lap-readout]'),
|
|
8
|
+
towerTotalLaps: root.querySelector('[data-tower-total-laps]'),
|
|
9
|
+
towerSafetyBanner: root.querySelector('[data-tower-safety-banner]'),
|
|
10
|
+
timingGapLabel: root.querySelector('[data-timing-gap-label]'),
|
|
11
|
+
lap: root.querySelector('[data-lap-readout]'),
|
|
12
|
+
drs: root.querySelector('[data-drs-readout]'),
|
|
13
|
+
contacts: root.querySelector('[data-contact-readout]'),
|
|
14
|
+
camera: root.querySelector('[data-camera-readout]'),
|
|
15
|
+
fps: root.querySelector('[data-fps-readout]'),
|
|
16
|
+
selectedCode: root.querySelector('[data-selected-code]'),
|
|
17
|
+
selectedName: root.querySelector('[data-selected-name]'),
|
|
18
|
+
speed: root.querySelector('[data-telemetry-speed]'),
|
|
19
|
+
throttle: root.querySelector('[data-telemetry-throttle]'),
|
|
20
|
+
brake: root.querySelector('[data-telemetry-brake]'),
|
|
21
|
+
tyres: root.querySelector('[data-telemetry-tyres]'),
|
|
22
|
+
selectedDrs: root.querySelector('[data-telemetry-drs]'),
|
|
23
|
+
surface: root.querySelector('[data-telemetry-surface]'),
|
|
24
|
+
gap: root.querySelector('[data-telemetry-gap]'),
|
|
25
|
+
leaderGap: root.querySelector('[data-telemetry-leader-gap]'),
|
|
26
|
+
carOverview: root.querySelector('[data-paddock-component="car-driver-overview"]'),
|
|
27
|
+
carOverviewTitle: root.querySelector('[data-car-overview-title]'),
|
|
28
|
+
carOverviewDiagram: root.querySelector('.car-overview-diagram'),
|
|
29
|
+
carOverviewCode: root.querySelector('[data-car-overview-code]'),
|
|
30
|
+
carOverviewIcon: root.querySelector('[data-car-overview-icon]'),
|
|
31
|
+
carOverviewImage: root.querySelector('[data-car-overview-image]'),
|
|
32
|
+
carOverviewNumber: root.querySelector('[data-car-overview-number]'),
|
|
33
|
+
carOverviewCoreStat: root.querySelector('[data-car-overview-core-stat]'),
|
|
34
|
+
carOverviewFields: root.querySelectorAll('[data-overview-field]'),
|
|
35
|
+
raceDataPanel: root.querySelector('[data-race-data-panel]'),
|
|
36
|
+
raceDataKicker: root.querySelector('[data-race-data-kicker]'),
|
|
37
|
+
raceDataTitle: root.querySelector('[data-race-data-title]'),
|
|
38
|
+
raceDataNumber: root.querySelector('[data-race-data-number]'),
|
|
39
|
+
raceDataSubtitle: root.querySelector('[data-race-data-subtitle]'),
|
|
40
|
+
raceDataOpen: root.querySelector('[data-race-data-open]'),
|
|
41
|
+
finishPanel: root.querySelector('[data-race-finish-panel]'),
|
|
42
|
+
finishWinner: root.querySelector('[data-race-finish-winner]'),
|
|
43
|
+
finishClassification: root.querySelector('[data-race-finish-classification]'),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
canvasHost: root.querySelector('[data-track-canvas]'),
|
|
48
|
+
safetyButtons: root.querySelectorAll('[data-safety-car]'),
|
|
49
|
+
restartButton: root.querySelector('[data-restart-race]'),
|
|
50
|
+
openButton: root.querySelector('[data-race-data-open]'),
|
|
51
|
+
timingList: root.querySelector('[data-timing-list]'),
|
|
52
|
+
timingGapModeButtons: root.querySelectorAll('[data-timing-gap-mode]'),
|
|
53
|
+
cameraButtons: root.querySelectorAll('[data-camera-mode]'),
|
|
54
|
+
overviewModeButtons: root.querySelectorAll('[data-overview-mode]'),
|
|
55
|
+
zoomInButton: root.querySelector('[data-zoom-in]'),
|
|
56
|
+
zoomOutButton: root.querySelector('[data-zoom-out]'),
|
|
57
|
+
readouts,
|
|
58
|
+
startLightNodes: [...(readouts.startLights?.querySelectorAll('.start-lights__gantry span') ?? [])],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setText(node, value) {
|
|
63
|
+
if (node) node.textContent = value;
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import carSpriteUrl from '../../assets/f1-car-sprite-game.png';
|
|
2
|
+
import safetyCarSpriteUrl from '../../assets/f1-safety-car-sprite.png';
|
|
3
|
+
import broadcastPanelSurfaceUrl from '../../assets/f1-broadcast-panel-surface.png';
|
|
4
|
+
import asphaltTextureUrl from '../../assets/f1-texture-asphalt.png';
|
|
5
|
+
import f1LogoUrl from '../../assets/f1Logo.png';
|
|
6
|
+
import driverHelmetUrl from '../../assets/f1-driver-helmet.png';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_F1_SIMULATOR_ASSETS = {
|
|
9
|
+
car: carSpriteUrl,
|
|
10
|
+
carOverview: carSpriteUrl,
|
|
11
|
+
driverHelmet: driverHelmetUrl,
|
|
12
|
+
safetyCar: safetyCarSpriteUrl,
|
|
13
|
+
broadcastPanel: broadcastPanelSurfaceUrl,
|
|
14
|
+
f1Logo: f1LogoUrl,
|
|
15
|
+
trackTextures: {
|
|
16
|
+
asphalt: asphaltTextureUrl,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function resolveF1SimulatorAssets(overrides = {}) {
|
|
21
|
+
return {
|
|
22
|
+
...DEFAULT_F1_SIMULATOR_ASSETS,
|
|
23
|
+
...overrides,
|
|
24
|
+
trackTextures: {
|
|
25
|
+
...DEFAULT_F1_SIMULATOR_ASSETS.trackTextures,
|
|
26
|
+
...(overrides.trackTextures ?? {}),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { resolveF1SimulatorAssets } from './defaultAssets.js';
|
|
2
|
+
import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
|
|
3
|
+
import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
6
|
+
seed: 1971,
|
|
7
|
+
totalLaps: 10,
|
|
8
|
+
initialCameraMode: 'leader',
|
|
9
|
+
title: 'F1 Simulator Lab',
|
|
10
|
+
kicker: 'Race Control',
|
|
11
|
+
backLinkHref: 'projects.html',
|
|
12
|
+
backLinkLabel: 'Projects',
|
|
13
|
+
showBackLink: true,
|
|
14
|
+
ui: {
|
|
15
|
+
layoutPreset: 'standard',
|
|
16
|
+
cameraControls: 'embedded',
|
|
17
|
+
showFps: true,
|
|
18
|
+
showTimingTower: true,
|
|
19
|
+
showTelemetry: true,
|
|
20
|
+
telemetryIncludesOverview: true,
|
|
21
|
+
showRaceDataPanel: true,
|
|
22
|
+
raceDataBanners: {
|
|
23
|
+
initial: 'project',
|
|
24
|
+
enabled: ['project', 'radio'],
|
|
25
|
+
},
|
|
26
|
+
raceDataBannerSize: 'custom',
|
|
27
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
28
|
+
},
|
|
29
|
+
theme: {
|
|
30
|
+
accentColor: '#e10600',
|
|
31
|
+
greenColor: '#14c784',
|
|
32
|
+
yellowColor: '#ffd166',
|
|
33
|
+
timingTowerMaxWidth: '390px',
|
|
34
|
+
raceViewMinHeight: '620px',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const PADDOCK_SIMULATOR_PRESETS = {
|
|
39
|
+
dashboard: {
|
|
40
|
+
ui: {
|
|
41
|
+
layoutPreset: 'standard',
|
|
42
|
+
cameraControls: 'embedded',
|
|
43
|
+
showFps: true,
|
|
44
|
+
showTimingTower: true,
|
|
45
|
+
showTelemetry: true,
|
|
46
|
+
showRaceDataPanel: true,
|
|
47
|
+
raceDataBannerSize: 'custom',
|
|
48
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
'timing-overlay': {
|
|
52
|
+
ui: {
|
|
53
|
+
layoutPreset: 'left-tower-overlay',
|
|
54
|
+
cameraControls: 'external',
|
|
55
|
+
showFps: false,
|
|
56
|
+
showTimingTower: true,
|
|
57
|
+
showTelemetry: true,
|
|
58
|
+
showRaceDataPanel: true,
|
|
59
|
+
raceDataBannerSize: 'auto',
|
|
60
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
61
|
+
raceDataBanners: {
|
|
62
|
+
initial: 'project',
|
|
63
|
+
enabled: ['project', 'radio'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
'compact-race': {
|
|
68
|
+
ui: {
|
|
69
|
+
layoutPreset: 'standard',
|
|
70
|
+
cameraControls: 'embedded',
|
|
71
|
+
showFps: false,
|
|
72
|
+
showTimingTower: false,
|
|
73
|
+
showTelemetry: false,
|
|
74
|
+
showRaceDataPanel: true,
|
|
75
|
+
raceDataBannerSize: 'auto',
|
|
76
|
+
timingTowerVerticalFit: 'scroll',
|
|
77
|
+
},
|
|
78
|
+
theme: {
|
|
79
|
+
timingTowerMaxWidth: '340px',
|
|
80
|
+
raceViewMinHeight: '460px',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
'full-dashboard': {
|
|
84
|
+
ui: {
|
|
85
|
+
layoutPreset: 'standard',
|
|
86
|
+
cameraControls: 'external',
|
|
87
|
+
showFps: true,
|
|
88
|
+
showTimingTower: true,
|
|
89
|
+
showTelemetry: true,
|
|
90
|
+
telemetryIncludesOverview: true,
|
|
91
|
+
showRaceDataPanel: true,
|
|
92
|
+
raceDataBannerSize: 'custom',
|
|
93
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const PADDOCK_THEME_CSS_VARIABLES = {
|
|
99
|
+
accentColor: '--paddock-accent-color',
|
|
100
|
+
greenColor: '--paddock-green-color',
|
|
101
|
+
yellowColor: '--paddock-yellow-color',
|
|
102
|
+
timingTowerMaxWidth: '--paddock-timing-tower-max-width',
|
|
103
|
+
raceViewMinHeight: '--paddock-race-view-min-height',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function resolveF1SimulatorOptions(options = {}) {
|
|
107
|
+
const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
|
|
108
|
+
? options.preset
|
|
109
|
+
: null;
|
|
110
|
+
const preset = presetName ? PADDOCK_SIMULATOR_PRESETS[presetName] : {};
|
|
111
|
+
const requestedUi = options.ui ?? {};
|
|
112
|
+
const presetUi = preset.ui ?? {};
|
|
113
|
+
const ui = {
|
|
114
|
+
...DEFAULT_F1_SIMULATOR_OPTIONS.ui,
|
|
115
|
+
...presetUi,
|
|
116
|
+
...requestedUi,
|
|
117
|
+
raceDataBanners: {
|
|
118
|
+
...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners,
|
|
119
|
+
...(presetUi.raceDataBanners ?? {}),
|
|
120
|
+
...(requestedUi.raceDataBanners ?? {}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
ui.raceDataBanners.enabled = normalizeEnabledBanners(ui.raceDataBanners.enabled);
|
|
124
|
+
if (!['project', 'radio', 'hidden'].includes(ui.raceDataBanners.initial)) {
|
|
125
|
+
ui.raceDataBanners.initial = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.initial;
|
|
126
|
+
}
|
|
127
|
+
if (ui.raceDataBanners.initial !== 'hidden' && !ui.raceDataBanners.enabled.includes(ui.raceDataBanners.initial)) {
|
|
128
|
+
ui.raceDataBanners.initial = 'hidden';
|
|
129
|
+
}
|
|
130
|
+
if (!['expand-race-view', 'scroll'].includes(ui.timingTowerVerticalFit)) {
|
|
131
|
+
ui.timingTowerVerticalFit = DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingTowerVerticalFit;
|
|
132
|
+
}
|
|
133
|
+
if (!['auto', 'custom'].includes(ui.raceDataBannerSize)) {
|
|
134
|
+
ui.raceDataBannerSize = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBannerSize;
|
|
135
|
+
}
|
|
136
|
+
const mergedOptions = {
|
|
137
|
+
...DEFAULT_F1_SIMULATOR_OPTIONS,
|
|
138
|
+
...preset,
|
|
139
|
+
...options,
|
|
140
|
+
};
|
|
141
|
+
const theme = normalizeTheme({
|
|
142
|
+
...DEFAULT_F1_SIMULATOR_OPTIONS.theme,
|
|
143
|
+
...(preset.theme ?? {}),
|
|
144
|
+
...(options.theme ?? {}),
|
|
145
|
+
});
|
|
146
|
+
const drivers = normalizeSimulatorDrivers(mergedOptions.drivers, {
|
|
147
|
+
entries: mergedOptions.entries ?? CHAMPIONSHIP_ENTRY_BLUEPRINTS,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...DEFAULT_F1_SIMULATOR_OPTIONS,
|
|
152
|
+
...preset,
|
|
153
|
+
...options,
|
|
154
|
+
preset: presetName ?? options.preset,
|
|
155
|
+
ui,
|
|
156
|
+
theme,
|
|
157
|
+
drivers,
|
|
158
|
+
assets: resolveF1SimulatorAssets(options.assets),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function applyPaddockThemeCssVariables(root, theme = DEFAULT_F1_SIMULATOR_OPTIONS.theme) {
|
|
163
|
+
Object.entries(PADDOCK_THEME_CSS_VARIABLES).forEach(([key, variable]) => {
|
|
164
|
+
const value = theme?.[key];
|
|
165
|
+
if (value == null || value === '') return;
|
|
166
|
+
root?.style?.setProperty?.(variable, String(value));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeEnabledBanners(value) {
|
|
171
|
+
if (value === false) return [];
|
|
172
|
+
if (value === true || value == null) return [...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.enabled];
|
|
173
|
+
if (!Array.isArray(value)) return [...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.enabled];
|
|
174
|
+
return [...new Set(value.filter((item) => item === 'project' || item === 'radio'))];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeTheme(theme) {
|
|
178
|
+
return Object.fromEntries(
|
|
179
|
+
Object.entries(DEFAULT_F1_SIMULATOR_OPTIONS.theme).map(([key, fallback]) => [
|
|
180
|
+
key,
|
|
181
|
+
theme?.[key] == null || theme?.[key] === '' ? fallback : theme[key],
|
|
182
|
+
]),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { DriverData } from './driverData.js';
|
|
2
|
+
import { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
|
|
3
|
+
import { VehicleData } from './vehicleData.js';
|
|
4
|
+
|
|
5
|
+
export { DriverData, DRIVER_STAT_DEFINITIONS } from './driverData.js';
|
|
6
|
+
export { VehicleData, VEHICLE_STAT_DEFINITIONS } from './vehicleData.js';
|
|
7
|
+
|
|
8
|
+
export const CHAMPIONSHIP = {
|
|
9
|
+
id: 'project-race-2026',
|
|
10
|
+
name: 'Project Race',
|
|
11
|
+
season: 2026,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const CHAMPIONSHIP_ENTRY_BLUEPRINTS = [
|
|
15
|
+
{
|
|
16
|
+
driverId: 'budget',
|
|
17
|
+
driverNumber: 71,
|
|
18
|
+
timingName: 'Budget',
|
|
19
|
+
driver: new DriverData({ pace: 52, racecraft: 74, aggression: 38, riskTolerance: 47, patience: 81, consistency: 86 }),
|
|
20
|
+
vehicle: new VehicleData({ id: 'budget-bb01', name: 'BB-01 Ledger', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 }),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
driverId: 'noir',
|
|
24
|
+
driverNumber: 13,
|
|
25
|
+
timingName: 'Noir',
|
|
26
|
+
driver: new DriverData({ pace: 68, racecraft: 83, aggression: 61, riskTolerance: 72, patience: 57, consistency: 64 }),
|
|
27
|
+
vehicle: new VehicleData({ id: 'noir-nn13', name: 'NN-13 Shadow', power: 64, braking: 60, aero: 73, dragEfficiency: 57, mechanicalGrip: 69, weightControl: 54, tireCare: 51 }),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
driverId: 'vinyl',
|
|
31
|
+
driverNumber: 33,
|
|
32
|
+
timingName: 'HoloVinyl',
|
|
33
|
+
driver: new DriverData({ pace: 71, racecraft: 66, aggression: 52, riskTolerance: 59, patience: 62, consistency: 58 }),
|
|
34
|
+
vehicle: new VehicleData({ id: 'vinyl-hv33', name: 'HV-33 Groove', power: 70, braking: 49, aero: 58, dragEfficiency: 74, mechanicalGrip: 52, weightControl: 68, tireCare: 46 }),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
driverId: 'drsorriso',
|
|
38
|
+
driverNumber: 55,
|
|
39
|
+
timingName: 'DrSorriso',
|
|
40
|
+
driver: new DriverData({ pace: 46, racecraft: 69, aggression: 44, riskTolerance: 43, patience: 76, consistency: 73 }),
|
|
41
|
+
vehicle: new VehicleData({ id: 'sorriso-ds55', name: 'DS-55 Intake', power: 54, braking: 67, aero: 62, dragEfficiency: 49, mechanicalGrip: 71, weightControl: 45, tireCare: 69 }),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
driverId: 'victoria',
|
|
45
|
+
driverNumber: 11,
|
|
46
|
+
timingName: 'VictorIA',
|
|
47
|
+
driver: new DriverData({ pace: 78, racecraft: 79, aggression: 58, riskTolerance: 65, patience: 55, consistency: 68 }),
|
|
48
|
+
vehicle: new VehicleData({ id: 'victoria-vi11', name: 'VI-11 Vector', power: 76, braking: 58, aero: 65, dragEfficiency: 63, mechanicalGrip: 61, weightControl: 60, tireCare: 48 }),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
driverId: 'reminderz',
|
|
52
|
+
driverNumber: 7,
|
|
53
|
+
timingName: 'ReminderZ',
|
|
54
|
+
driver: new DriverData({ pace: 41, racecraft: 61, aggression: 34, riskTolerance: 39, patience: 85, consistency: 80 }),
|
|
55
|
+
vehicle: new VehicleData({ id: 'reminderz-rz07', name: 'RZ-07 Recall', power: 42, braking: 76, aero: 50, dragEfficiency: 72, mechanicalGrip: 58, weightControl: 63, tireCare: 88 }),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
driverId: 'clipclop',
|
|
59
|
+
driverNumber: 14,
|
|
60
|
+
timingName: 'ClipClop',
|
|
61
|
+
driver: new DriverData({ pace: 73, racecraft: 88, aggression: 76, riskTolerance: 78, patience: 48, consistency: 61 }),
|
|
62
|
+
vehicle: new VehicleData({ id: 'clipclop-cc14', name: 'CC-14 Sync', power: 68, braking: 63, aero: 59, dragEfficiency: 61, mechanicalGrip: 78, weightControl: 56, tireCare: 44 }),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
driverId: 'evolve',
|
|
66
|
+
driverNumber: 23,
|
|
67
|
+
timingName: 'Evolve',
|
|
68
|
+
driver: new DriverData({ pace: 56, racecraft: 70, aggression: 63, riskTolerance: 61, patience: 58, consistency: 52 }),
|
|
69
|
+
vehicle: new VehicleData({ id: 'evolve-ev23', name: 'EV-23 Morph', power: 60, braking: 53, aero: 79, dragEfficiency: 46, mechanicalGrip: 66, weightControl: 49, tireCare: 57 }),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
driverId: 'clash',
|
|
73
|
+
driverNumber: 47,
|
|
74
|
+
timingName: 'Clash',
|
|
75
|
+
driver: new DriverData({ pace: 44, racecraft: 67, aggression: 70, riskTolerance: 69, patience: 42, consistency: 59 }),
|
|
76
|
+
vehicle: new VehicleData({ id: 'clash-cr47', name: 'CR-47 Arena', power: 57, braking: 48, aero: 52, dragEfficiency: 55, mechanicalGrip: 73, weightControl: 52, tireCare: 64 }),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
driverId: 'core',
|
|
80
|
+
driverNumber: 91,
|
|
81
|
+
timingName: 'Core',
|
|
82
|
+
driver: new DriverData({ pace: 82, racecraft: 85, aggression: 67, riskTolerance: 74, patience: 53, consistency: 72 }),
|
|
83
|
+
vehicle: new VehicleData({ id: 'core-ai91', name: 'AI-91 Core', power: 80, braking: 61, aero: 72, dragEfficiency: 59, mechanicalGrip: 75, weightControl: 51, tireCare: 50 }),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
export const CHAMPIONSHIP_DRIVER_ENTRIES = CHAMPIONSHIP_ENTRY_BLUEPRINTS.map((entry) => ({
|
|
88
|
+
driverId: entry.driverId,
|
|
89
|
+
driverNumber: entry.driverNumber,
|
|
90
|
+
timingName: entry.timingName,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
94
|
+
|
|
95
|
+
function normalizeLetters(value) {
|
|
96
|
+
return String(value ?? '')
|
|
97
|
+
.normalize('NFKD')
|
|
98
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
99
|
+
.replace(/[^a-zA-Z]/g, '')
|
|
100
|
+
.toUpperCase();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function timingCodeCandidates(source) {
|
|
104
|
+
const letters = normalizeLetters(source);
|
|
105
|
+
if (!letters) return ['XXX'];
|
|
106
|
+
if (letters.length <= 3) return [letters.padEnd(3, 'X')];
|
|
107
|
+
|
|
108
|
+
const candidates = [letters.slice(0, 3)];
|
|
109
|
+
for (let index = 1; index <= letters.length - 3; index += 1) {
|
|
110
|
+
candidates.push(letters.slice(index, index + 3));
|
|
111
|
+
}
|
|
112
|
+
candidates.push(`${letters[0]}${letters[1]}${letters[letters.length - 1]}`);
|
|
113
|
+
return [...new Set(candidates)];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function pickUniqueTimingCode(source, usedCodes) {
|
|
117
|
+
for (const candidate of timingCodeCandidates(source)) {
|
|
118
|
+
if (!usedCodes.has(candidate)) {
|
|
119
|
+
usedCodes.add(candidate);
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const letters = normalizeLetters(source).padEnd(2, 'X');
|
|
125
|
+
for (const suffix of LETTERS) {
|
|
126
|
+
const candidate = `${letters[0]}${letters[1]}${suffix}`;
|
|
127
|
+
if (!usedCodes.has(candidate)) {
|
|
128
|
+
usedCodes.add(candidate);
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw new Error(`Unable to create a unique three-letter timing code for "${source}"`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertUniqueDriverNumbers(entries) {
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
entries.forEach((entry) => {
|
|
139
|
+
if (seen.has(entry.driverNumber)) {
|
|
140
|
+
throw new Error(`Duplicate championship driver number: ${entry.driverNumber}`);
|
|
141
|
+
}
|
|
142
|
+
seen.add(entry.driverNumber);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildDriverConstructorArgs(entry) {
|
|
147
|
+
const driver = entry.driver instanceof DriverData
|
|
148
|
+
? entry.driver
|
|
149
|
+
: new DriverData(entry.driver ?? {});
|
|
150
|
+
return driver.toConstructorArgs();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildVehicleConstructorArgs(entry) {
|
|
154
|
+
const vehicle = entry.vehicle instanceof VehicleData
|
|
155
|
+
? entry.vehicle
|
|
156
|
+
: new VehicleData(entry.vehicle ?? {});
|
|
157
|
+
return vehicle.toConstructorArgs();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function formatDriverNumber(driverNumber) {
|
|
161
|
+
return String(driverNumber ?? '').padStart(2, '0');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeTeamIcon(source) {
|
|
165
|
+
const letters = normalizeLetters(source);
|
|
166
|
+
return letters.slice(0, 2).padEnd(2, 'X');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeTeam(team, driver, timingCode) {
|
|
170
|
+
const source = team ?? driver.team;
|
|
171
|
+
if (!source) return null;
|
|
172
|
+
const name = source.name ?? source.id ?? `${driver.name ?? timingCode} Team`;
|
|
173
|
+
const id = (source.id ?? normalizeLetters(name).toLowerCase()) || `${driver.id}-team`;
|
|
174
|
+
return {
|
|
175
|
+
...source,
|
|
176
|
+
id,
|
|
177
|
+
name,
|
|
178
|
+
color: source.color ?? driver.color,
|
|
179
|
+
icon: source.icon ?? normalizeTeamIcon(source.name ?? source.id ?? timingCode ?? driver.name),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildChampionshipDriverGrid(drivers = DEMO_PROJECT_DRIVERS, entries = CHAMPIONSHIP_DRIVER_ENTRIES) {
|
|
184
|
+
assertUniqueDriverNumbers(entries);
|
|
185
|
+
|
|
186
|
+
const entryByDriverId = new Map(entries.map((entry) => [entry.driverId, entry]));
|
|
187
|
+
const blueprintByDriverId = new Map(CHAMPIONSHIP_ENTRY_BLUEPRINTS.map((entry) => [entry.driverId, entry]));
|
|
188
|
+
const usedTimingCodes = new Set();
|
|
189
|
+
|
|
190
|
+
return drivers.map((driver, index) => {
|
|
191
|
+
const entry = {
|
|
192
|
+
...(blueprintByDriverId.get(driver.id) ?? {}),
|
|
193
|
+
...(entryByDriverId.get(driver.id) ?? {}),
|
|
194
|
+
};
|
|
195
|
+
const timingCode = pickUniqueTimingCode(entry.timingName ?? driver.timingName ?? driver.name ?? driver.code, usedTimingCodes);
|
|
196
|
+
const driverNumber = entry.driverNumber ?? driver.driverNumber ?? index + 1;
|
|
197
|
+
const driverArgs = buildDriverConstructorArgs({ driverId: driver.id, ...entry });
|
|
198
|
+
const vehicleArgs = buildVehicleConstructorArgs({ driverId: driver.id, ...entry });
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
...driver,
|
|
202
|
+
code: timingCode,
|
|
203
|
+
timingCode,
|
|
204
|
+
raceName: timingCode,
|
|
205
|
+
driverNumber,
|
|
206
|
+
team: normalizeTeam(entry.team, driver, timingCode),
|
|
207
|
+
pace: driverArgs.pace,
|
|
208
|
+
racecraft: driverArgs.racecraft,
|
|
209
|
+
consistency: driverArgs.consistency,
|
|
210
|
+
personality: driverArgs.personality,
|
|
211
|
+
vehicle: vehicleArgs,
|
|
212
|
+
constructorArgs: {
|
|
213
|
+
driver: driverArgs,
|
|
214
|
+
vehicle: vehicleArgs,
|
|
215
|
+
},
|
|
216
|
+
championship: {
|
|
217
|
+
...CHAMPIONSHIP,
|
|
218
|
+
entryIndex: index,
|
|
219
|
+
vehicleId: vehicleArgs.id,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const CHAMPIONSHIP_PROJECT_DRIVERS = buildChampionshipDriverGrid();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function normalizeCustomFields(customFields) {
|
|
2
|
+
if (!customFields) return [];
|
|
3
|
+
|
|
4
|
+
const entries = Array.isArray(customFields)
|
|
5
|
+
? customFields
|
|
6
|
+
: Object.entries(customFields).map(([label, value]) => ({ label, value }));
|
|
7
|
+
|
|
8
|
+
return entries
|
|
9
|
+
.map((field) => ({
|
|
10
|
+
label: String(field?.label ?? '').trim(),
|
|
11
|
+
value: String(field?.value ?? '').trim(),
|
|
12
|
+
}))
|
|
13
|
+
.filter((field) => field.label && field.value);
|
|
14
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const DEMO_PROJECT_DRIVERS = [
|
|
2
|
+
{
|
|
3
|
+
id: 'budget',
|
|
4
|
+
code: 'BUD',
|
|
5
|
+
icon: 'BB',
|
|
6
|
+
raceName: 'BUDGET',
|
|
7
|
+
name: 'Budget Buddy',
|
|
8
|
+
color: '#ff2d55',
|
|
9
|
+
tire: 'M',
|
|
10
|
+
pace: 0.98,
|
|
11
|
+
racecraft: 0.78,
|
|
12
|
+
link: '/project_details/project-budget-buddy.html',
|
|
13
|
+
raceData: ['AI finance coach', 'Python + LLM', 'Budget guardrails'],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'noir',
|
|
17
|
+
code: 'NOI',
|
|
18
|
+
icon: 'NN',
|
|
19
|
+
raceName: 'NOIR',
|
|
20
|
+
name: 'Neural Noir',
|
|
21
|
+
color: '#ffb000',
|
|
22
|
+
tire: 'M',
|
|
23
|
+
pace: 1.01,
|
|
24
|
+
racecraft: 0.84,
|
|
25
|
+
link: '/project_details/project-neural-noir.html',
|
|
26
|
+
raceData: ['Procedural mystery', 'Gemini storytelling', 'Unreal loop'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'vinyl',
|
|
30
|
+
code: 'HOL',
|
|
31
|
+
icon: 'HV',
|
|
32
|
+
raceName: 'VINYL',
|
|
33
|
+
name: 'HoloVinyl',
|
|
34
|
+
color: '#00c2a8',
|
|
35
|
+
tire: 'H',
|
|
36
|
+
pace: 1.03,
|
|
37
|
+
racecraft: 0.75,
|
|
38
|
+
link: '/project_details/project-holovinyl.html',
|
|
39
|
+
raceData: ['Touchless DJ control', 'Computer vision', 'Hand tracking'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'drsorriso',
|
|
43
|
+
code: 'DRS',
|
|
44
|
+
icon: 'DR',
|
|
45
|
+
raceName: 'SORRISO',
|
|
46
|
+
name: 'DrSorrisoDonations',
|
|
47
|
+
color: '#e10600',
|
|
48
|
+
tire: 'S',
|
|
49
|
+
pace: 0.97,
|
|
50
|
+
racecraft: 0.73,
|
|
51
|
+
link: '/project_details/project-drsorrisodonations.html',
|
|
52
|
+
raceData: ['Donation intake', 'Database workflow', 'Automation tool'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'victoria',
|
|
56
|
+
code: 'VIC',
|
|
57
|
+
icon: 'VI',
|
|
58
|
+
raceName: 'VICTORIA',
|
|
59
|
+
name: 'VictorIA',
|
|
60
|
+
color: '#00a3ff',
|
|
61
|
+
tire: 'M',
|
|
62
|
+
pace: 1.04,
|
|
63
|
+
racecraft: 0.82,
|
|
64
|
+
link: '/project_details/project-victoria.html',
|
|
65
|
+
raceData: ['Game AI sandbox', 'Robotics ideas', 'Vision systems'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'reminderz',
|
|
69
|
+
code: 'REM',
|
|
70
|
+
icon: 'RZ',
|
|
71
|
+
raceName: 'REMIND',
|
|
72
|
+
name: 'ReminderZ',
|
|
73
|
+
color: '#a3e635',
|
|
74
|
+
tire: 'H',
|
|
75
|
+
pace: 0.95,
|
|
76
|
+
racecraft: 0.69,
|
|
77
|
+
link: '/project_details/project-remainder-v0.html',
|
|
78
|
+
raceData: ['Knowledge workflow', 'Personal graph', 'Capture tool'],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'clipclop',
|
|
82
|
+
code: 'CLP',
|
|
83
|
+
icon: 'CC',
|
|
84
|
+
raceName: 'CLIPCLOP',
|
|
85
|
+
name: 'ClipClop',
|
|
86
|
+
color: '#f97316',
|
|
87
|
+
tire: 'S',
|
|
88
|
+
pace: 1.02,
|
|
89
|
+
racecraft: 0.88,
|
|
90
|
+
link: '/project_details/project-clipclop.html',
|
|
91
|
+
raceData: ['Mac + Android sync', 'Local clipboard', 'Productivity app'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'evolve',
|
|
95
|
+
code: 'EVL',
|
|
96
|
+
icon: 'EV',
|
|
97
|
+
raceName: 'EVOLVE',
|
|
98
|
+
name: 'EvolveProject',
|
|
99
|
+
color: '#d946ef',
|
|
100
|
+
tire: 'M',
|
|
101
|
+
pace: 0.99,
|
|
102
|
+
racecraft: 0.77,
|
|
103
|
+
link: '/project_details/project-evolveproject.html',
|
|
104
|
+
raceData: ['Creature cards', 'Generative AI', 'Unreal prototype'],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'clash',
|
|
108
|
+
code: 'CRP',
|
|
109
|
+
icon: 'PY',
|
|
110
|
+
raceName: 'CLASH',
|
|
111
|
+
name: 'Clash Royale In Python',
|
|
112
|
+
color: '#22c55e',
|
|
113
|
+
tire: 'H',
|
|
114
|
+
pace: 0.96,
|
|
115
|
+
racecraft: 0.72,
|
|
116
|
+
link: '/project_details/project-algorithms-project.html',
|
|
117
|
+
raceData: ['Python arena', 'Pathfinding', 'Game algorithms'],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'core',
|
|
121
|
+
code: 'COR',
|
|
122
|
+
icon: 'AI',
|
|
123
|
+
raceName: 'CORE',
|
|
124
|
+
name: 'Core Conflict',
|
|
125
|
+
color: '#38bdf8',
|
|
126
|
+
tire: 'M',
|
|
127
|
+
pace: 1.05,
|
|
128
|
+
racecraft: 0.86,
|
|
129
|
+
link: '/project_details/project-gengame.html',
|
|
130
|
+
raceData: ['Runtime AI agents', 'Multiplayer state', 'Game systems'],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
export const PROJECT_DRIVERS = DEMO_PROJECT_DRIVERS;
|