Skip to content

Commit de966ca

Browse files
authored
update logic for relative deltas, adds base work for speeds and avg laps (#24)
1 parent 394292c commit de966ca

20 files changed

+1076
-65
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
"irating",
77
"irdashies"
88
],
9-
"svg.preview.background": "black"
9+
"svg.preview.background": "black",
10+
"eslint.useFlatConfig": true
1011
}

src/app/irsdk/types/_GENERATED_telemetry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface TelemetryVarList {
145145
EnterExitReset: TelemetryVariable<number[]>;
146146
TrackTemp: TelemetryVariable<number[]>;
147147
TrackTempCrew: TelemetryVariable<number[]>;
148+
TrackWetness: TelemetryVariable<number[]>;
148149
AirTemp: TelemetryVariable<number[]>;
149150
WeatherType: TelemetryVariable<number[]>;
150151
Skies: TelemetryVariable<number[]>;

src/frontend/components/Standings/Relative.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const Relative = () => {
4040
))}
4141
</tbody>
4242
</table>
43+
4344
<SessionFooter />
4445
</div>
4546
);
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { renderHook } from '@testing-library/react';
3+
import { useDriverRelatives } from './useDriverRelatives';
4+
import { useDriverCarIdx, useSessionStore, useTelemetryValues } from '@irdashies/context';
5+
import { useDriverStandings } from './useDriverPositions';
6+
import type { Standings } from '../createStandings';
7+
8+
// Mock the context hooks
9+
vi.mock('@irdashies/context', () => ({
10+
useDriverCarIdx: vi.fn(),
11+
useTelemetryValues: vi.fn(),
12+
useSessionStore: vi.fn(),
13+
}));
14+
15+
vi.mock('./useDriverPositions', () => ({
16+
useDriverStandings: vi.fn(),
17+
}));
18+
19+
describe('useDriverRelatives', () => {
20+
const mockDrivers: Standings[] = [
21+
{
22+
carIdx: 0,
23+
classPosition: 1,
24+
isPlayer: true,
25+
driver: {
26+
name: 'Driver 1',
27+
carNum: '1',
28+
license: 'A',
29+
rating: 2000,
30+
},
31+
fastestTime: 100,
32+
hasFastestTime: true,
33+
lastTime: 105,
34+
onPitRoad: false,
35+
onTrack: true,
36+
carClass: {
37+
id: 1,
38+
color: 0,
39+
name: 'Class 1',
40+
relativeSpeed: 1.0,
41+
estLapTime: 100,
42+
},
43+
},
44+
{
45+
carIdx: 1,
46+
classPosition: 2,
47+
isPlayer: false,
48+
driver: {
49+
name: 'Driver 2',
50+
carNum: '2',
51+
license: 'B',
52+
rating: 1800,
53+
},
54+
fastestTime: 102,
55+
hasFastestTime: false,
56+
lastTime: 107,
57+
onPitRoad: false,
58+
onTrack: true,
59+
carClass: {
60+
id: 1,
61+
color: 0,
62+
name: 'Class 1',
63+
relativeSpeed: 1.0,
64+
estLapTime: 100,
65+
},
66+
},
67+
{
68+
carIdx: 2,
69+
classPosition: 3,
70+
isPlayer: false,
71+
driver: {
72+
name: 'Driver 3',
73+
carNum: '3',
74+
license: 'C',
75+
rating: 1600,
76+
},
77+
fastestTime: 104,
78+
hasFastestTime: false,
79+
lastTime: 109,
80+
onPitRoad: false,
81+
onTrack: true,
82+
carClass: {
83+
id: 1,
84+
color: 0,
85+
name: 'Class 1',
86+
relativeSpeed: 1.0,
87+
estLapTime: 100,
88+
},
89+
},
90+
];
91+
92+
const mockCarIdxLapDistPct = [0.5, 0.6, 0.4]; // Player, Ahead, Behind
93+
const mockCarIdxEstTime = [99, 100, 90]; // Player, Same class, Faster class
94+
95+
beforeEach(() => {
96+
vi.clearAllMocks();
97+
vi.mocked(useDriverCarIdx).mockReturnValue(0);
98+
vi.mocked(useTelemetryValues).mockImplementation((key: string) => {
99+
if (key === 'CarIdxLapDistPct') return mockCarIdxLapDistPct;
100+
if (key === 'CarIdxEstTime') return mockCarIdxEstTime;
101+
return [];
102+
});
103+
vi.mocked(useDriverStandings).mockReturnValue(mockDrivers);
104+
vi.mocked(useSessionStore).mockReturnValue({
105+
session: {
106+
DriverInfo: {
107+
PaceCarIdx: 0,
108+
},
109+
},
110+
});
111+
});
112+
113+
it('should return empty array when no player is found', () => {
114+
vi.mocked(useDriverCarIdx).mockReturnValue(undefined);
115+
116+
const { result } = renderHook(() => useDriverRelatives({ buffer: 2 }));
117+
expect(result.current).toEqual([]);
118+
});
119+
120+
it('should calculate correct deltas for cars ahead and behind', () => {
121+
const { result } = renderHook(() => useDriverRelatives({ buffer: 2 }));
122+
123+
expect(result.current).toHaveLength(3); // Player + 1 ahead + 1 behind
124+
expect(result.current[0].carIdx).toBe(1); // Car ahead
125+
expect(result.current[1].carIdx).toBe(0); // Player
126+
expect(result.current[2].carIdx).toBe(2); // Car behind
127+
128+
// Car ahead should have positive delta
129+
expect(result.current[0].delta).toBeGreaterThan(0);
130+
// Player should have zero delta
131+
expect(result.current[1].delta).toBe(0);
132+
// Car behind should have negative delta
133+
expect(result.current[2].delta).toBeLessThan(0);
134+
});
135+
136+
it('should respect buffer limit', () => {
137+
const { result } = renderHook(() => useDriverRelatives({ buffer: 1 }));
138+
139+
// Should only include player and one car ahead/behind
140+
expect(result.current).toHaveLength(3);
141+
});
142+
143+
it.each([
144+
[0.1, 0.2, 0.8], // Player near start, Car ahead near start, Car behind near finish
145+
[0.2, 0.3, 0.9],
146+
[0, 0.1, 0.7],
147+
[0.9, 0, 0.6],
148+
])(
149+
'should handle cars crossing the start/finish line',
150+
(playerDistPct, aheadDistPct, behindDistPct) => {
151+
const mockCarIdxLapDistPctWithCrossing = [
152+
playerDistPct,
153+
aheadDistPct,
154+
behindDistPct,
155+
];
156+
157+
vi.mocked(useTelemetryValues).mockImplementation((key: string) => {
158+
if (key === 'CarIdxLapDistPct') return mockCarIdxLapDistPctWithCrossing;
159+
if (key === 'CarIdxEstTime') return mockCarIdxEstTime;
160+
return [];
161+
});
162+
163+
const { result } = renderHook(() => useDriverRelatives({ buffer: 1 }));
164+
165+
// Car ahead should still be ahead by 10%
166+
expect(result.current[0].carIdx).toBe(1);
167+
expect(result.current[0].relativePct).toBeCloseTo(0.1);
168+
expect(result.current[0].delta).toBeCloseTo(10);
169+
170+
// Player should be in the middle
171+
expect(result.current[1].carIdx).toBe(0);
172+
expect(result.current[1].relativePct).toBe(0);
173+
expect(result.current[1].delta).toBe(0);
174+
175+
// Car behind should be behind by 20%
176+
expect(result.current[2].carIdx).toBe(2);
177+
expect(result.current[2].relativePct).toBeCloseTo(-0.3);
178+
expect(result.current[2].delta).toBeCloseTo(-30);
179+
}
180+
);
181+
182+
it('should filter out off-track cars', () => {
183+
const mockDriversWithOffTrack = [
184+
{ ...mockDrivers[0] },
185+
{ ...mockDrivers[1], onTrack: false },
186+
{ ...mockDrivers[2] },
187+
];
188+
189+
vi.mocked(useDriverStandings).mockReturnValue(mockDriversWithOffTrack);
190+
191+
const { result } = renderHook(() => useDriverRelatives({ buffer: 2 }));
192+
193+
// Should not include the off-track car
194+
expect(result.current).toHaveLength(2);
195+
expect(result.current.some((driver) => driver.carIdx === 1)).toBe(false);
196+
});
197+
});

src/frontend/components/Standings/hooks/useDriverRelatives.tsx

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,47 @@ import { useDriverStandings } from './useDriverPositions';
99
export const useDriverRelatives = ({ buffer }: { buffer: number }) => {
1010
const drivers = useDriverStandings();
1111
const carIdxLapDistPct = useTelemetryValues('CarIdxLapDistPct');
12-
const carIdxLap = useTelemetryValues('CarIdxLap');
13-
1412
const playerIndex = useDriverCarIdx();
15-
const driverCarEstLapTime = useSessionStore(
16-
(s) => s.session?.DriverInfo?.DriverCarEstLapTime ?? 0
17-
);
13+
const paceCarIdx =
14+
useSessionStore((s) => s.session?.DriverInfo?.PaceCarIdx) ?? -1;
1815

1916
const standings = useMemo(() => {
17+
const calculateRelativePct = (carIdx: number) => {
18+
if (playerIndex === undefined) {
19+
return NaN;
20+
}
21+
22+
const playerDistPct = carIdxLapDistPct?.[playerIndex];
23+
const otherDistPct = carIdxLapDistPct?.[carIdx];
24+
25+
if (playerDistPct === undefined || otherDistPct === undefined) {
26+
return NaN;
27+
}
28+
29+
const relativePct = otherDistPct - playerDistPct;
30+
31+
if (relativePct > 0.5) {
32+
return relativePct - 1.0;
33+
} else if (relativePct < -0.5) {
34+
return relativePct + 1.0;
35+
}
36+
37+
return relativePct;
38+
};
39+
2040
const calculateDelta = (otherCarIdx: number) => {
2141
const playerCarIdx = playerIndex ?? 0;
2242

23-
const playerLapNum = carIdxLap?.[playerCarIdx];
2443
const playerDistPct = carIdxLapDistPct?.[playerCarIdx];
25-
26-
const otherLapNum = carIdxLap?.[otherCarIdx];
2744
const otherDistPct = carIdxLapDistPct?.[otherCarIdx];
45+
46+
const player = drivers.find((driver) => driver.carIdx === playerIndex);
47+
const other = drivers.find((driver) => driver.carIdx === otherCarIdx);
2848

29-
if (
30-
playerLapNum === undefined || playerLapNum < 0 ||
31-
playerDistPct === undefined || playerDistPct < 0 || playerDistPct > 1 ||
32-
otherLapNum === undefined || otherLapNum < 0 ||
33-
otherDistPct === undefined || otherDistPct < 0 || otherDistPct > 1 ||
34-
driverCarEstLapTime <= 0
35-
) {
36-
return NaN;
37-
}
49+
// Use the slower car's lap time for more conservative deltas in multiclass
50+
const playerEstLapTime = player?.carClass?.estLapTime ?? 0;
51+
const otherEstLapTime = other?.carClass?.estLapTime ?? 0;
52+
const baseLapTime = Math.max(playerEstLapTime, otherEstLapTime);
3853

3954
let distPctDifference = otherDistPct - playerDistPct;
4055

@@ -43,56 +58,45 @@ export const useDriverRelatives = ({ buffer }: { buffer: number }) => {
4358
} else if (distPctDifference < -0.5) {
4459
distPctDifference += 1.0;
4560
}
46-
47-
const timeDelta = distPctDifference * driverCarEstLapTime;
4861

49-
return timeDelta;
50-
};
62+
const timeDelta = distPctDifference * baseLapTime;
5163

52-
const isHalfLapDifference = (car1: number, car2: number) => {
53-
const diff = (car1 - car2 + 1) % 1;
54-
return diff <= 0.5;
64+
return timeDelta;
5565
};
5666

57-
const filterAndMapDrivers = (isAhead: boolean) => {
67+
const filterAndMapDrivers = () => {
5868
return drivers
59-
.filter((driver) => driver.onTrack || driver.carIdx === playerIndex)
60-
.filter((result) => {
61-
const playerDistPct = carIdxLapDistPct?.[playerIndex ?? 0];
62-
const carDistPct = carIdxLapDistPct?.[result.carIdx];
63-
return isAhead
64-
? isHalfLapDifference(carDistPct, playerDistPct)
65-
: isHalfLapDifference(playerDistPct, carDistPct);
66-
})
69+
.filter((driver) => driver.onTrack || driver.carIdx === playerIndex) // filter out drivers not on track
70+
.filter((driver) => driver.carIdx > -1 && driver.carIdx !== paceCarIdx) // filter out pace car
71+
.map((result) => ({
72+
...result,
73+
relativePct: calculateRelativePct(result.carIdx),
74+
}))
75+
.filter((result) => !isNaN(result.relativePct))
76+
.sort((a, b) => b.relativePct - a.relativePct) // sort by relative pct
6777
.map((result) => ({
6878
...result,
6979
delta: calculateDelta(result.carIdx),
7080
}))
71-
.filter((result) => (isAhead ? result.delta > 0 : result.delta < 0))
72-
.sort((a, b) => (isAhead ? a.delta - b.delta : b.delta - a.delta))
73-
.slice(0, buffer)
74-
.sort((a, b) => b.delta - a.delta);
81+
.filter((result) => !isNaN(result.delta));
7582
};
7683

77-
const carsAhead = filterAndMapDrivers(true);
78-
const player = drivers.find((result) => result.carIdx === playerIndex);
79-
const carsBehind = filterAndMapDrivers(false);
84+
const allRelatives = filterAndMapDrivers();
85+
const playerArrIndex = allRelatives.findIndex(
86+
(result) => result.carIdx === playerIndex
87+
);
8088

81-
if (!player) {
89+
// if the player is not in the list, return an empty array
90+
if (playerArrIndex === -1) {
8291
return [];
8392
}
8493

85-
const relatives = [...carsAhead, { ...player, delta: 0 }, ...carsBehind];
86-
87-
return relatives;
88-
}, [
89-
drivers,
90-
buffer,
91-
playerIndex,
92-
driverCarEstLapTime,
93-
carIdxLapDistPct,
94-
carIdxLap,
95-
]);
94+
// buffered slice to get only the drivers around the player
95+
const start = Math.max(0, playerArrIndex - buffer);
96+
const end = Math.min(allRelatives.length, playerArrIndex + buffer + 1);
97+
98+
return allRelatives.slice(start, end);
99+
}, [buffer, playerIndex, carIdxLapDistPct, drivers, paceCarIdx]);
96100

97101
return standings;
98102
};

0 commit comments

Comments
 (0)