-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathRejoinIndicator.tsx
More file actions
161 lines (139 loc) · 5.49 KB
/
Copy pathRejoinIndicator.tsx
File metadata and controls
161 lines (139 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* Main Rejoin Indicator Component
* Displays a widget showing the gap to the car behind and if its safe to rejoin, need to exercise caution or you should not rejoin until the car has passed
* Hidden until player is at a user defined speed (Default 30) and not in the pit lane. The gap thresholds are user configured as well
*/
import {
useTrackStateSnapshot,
useFocusCarIdx,
useDrivingState,
useSessionVisibility,
useDashboard,
} from '@irdashies/context';
import { useDriverRelatives } from '../Standings/hooks/useDriverRelatives';
import { useRejoinSettings } from './hooks/useRejoinSettings';
import { getDemoRejoinData } from './demoData';
import type { Standings } from '../Standings/createStandings';
import { speedFromMs } from '@irdashies/utils/units';
export const RejoinIndicator = () => {
const { isDemoMode } = useDashboard();
const settings = useRejoinSettings();
// Must call all hooks in same order every render
const isSessionVisible = useSessionVisibility(
settings?.config?.sessionVisibility
);
const playerIndex = useFocusCarIdx();
const trackState = useTrackStateSnapshot();
const playerInPitStall = trackState?.playerCarInPitStall ?? false;
const carIdxOnPitRoad = trackState?.carIdxOnPitRoad ?? [];
const carSpeedForPlayer = trackState?.speed;
const sessionTime = trackState?.sessionTime ?? 0;
const sessionState = trackState?.sessionState ?? 0;
const { isDriving } = useDrivingState();
const drivers = useDriverRelatives({ buffer: 3 });
// Generate demo data when in demo mode
if (isDemoMode) {
const demoData = getDemoRejoinData(settings);
return (
<RejoinIndicatorDisplay
gap={demoData.gap}
status={demoData.status as 'Clear' | 'Caution' | 'Do Not Rejoin'}
/>
);
}
// If we don't have dashboard settings or no focused player, hide
if (!settings) return null;
if (!settings.enabled) return null;
if (playerIndex === undefined) return null;
if (!isSessionVisible) return null;
if (!isDriving) return null;
// Choose the first car behind the player that is not in the pit lane or off-track
let carBehind: Standings | undefined = undefined;
let behindList: Standings[];
if (drivers && drivers.length) {
const playerArrIndex = drivers.findIndex((d) => d.carIdx === playerIndex);
if (playerArrIndex >= 0) {
behindList = drivers.slice(playerArrIndex + 1);
// Prefer the nearest car that is explicitly on-track (onTrack !== false) and not on pit road.
// This skips pit-road/off-track cars and uses the next on-track car further back if present.
const candidate = behindList.find(
(d) => d.onTrack !== false && !d.onPitRoad
);
// If we found a non-pit, on-track car behind the player, use it.
// Otherwise, set carBehind to undefined so we don't fall back to a pit car.
if (candidate) {
carBehind = candidate;
} else {
carBehind = undefined;
}
}
}
// Read telemetry and computed car speed for the focused car index
const speedKmH = speedFromMs(carSpeedForPlayer ?? 0, 'km/h');
const gap = Math.abs(carBehind?.delta ?? Number.POSITIVE_INFINITY);
const gapLabel = Number.isFinite(gap) ? gap.toFixed(1) : '--';
const cfg = settings
? settings.config
: { careGap: Number.POSITIVE_INFINITY, stopGap: Number.POSITIVE_INFINITY };
const status = !Number.isFinite(gap)
? { label: 'Clear', color: 'green' }
: gap >= cfg.careGap
? { label: 'Clear', color: 'green' }
: gap >= cfg.stopGap
? { label: 'Caution', color: 'amber' }
: { label: 'Do Not Rejoin', color: 'red' };
// Player on pit road uses previously read telemetry
const playerOnPitRoad =
playerIndex !== undefined ? !!carIdxOnPitRoad?.[playerIndex] : false;
// Decide visibility based on configured speed (km/h)
const isHiddenBySpeed = speedKmH > settings.config.showAtSpeed;
// Decide visibility based on player location (garage / pit stall / on pit road)
const isHiddenByLocation = playerInPitStall || playerOnPitRoad;
// Decide visibility when there is no valid on-track car behind
const isHiddenByNoCarBehind = !Number.isFinite(gap);
// Hide during standing start: pre-race session states (< 4) or session time < 3 seconds during racing
// SessionState 2 = WarmUp/ParadeLaps (pre-race), 3 = GetInCar (unlikely), 4 = Racing
const isHiddenBySessionStart =
sessionState < 4 || (sessionState === 4 && sessionTime < 3);
if (isHiddenBySpeed) return null;
if (isHiddenByLocation) return null;
if (isHiddenByNoCarBehind) return null;
if (isHiddenBySessionStart) return null;
const statusBg =
status.color === 'green'
? 'bg-green-700'
: status.color === 'amber'
? 'bg-amber-600'
: 'bg-red-700';
return (
<div
className={`w-full flex justify-between rounded-sm p-2 font-bold text-white ${statusBg}`}
>
<div className="text-lg">{gapLabel}s</div>
<div className="text-lg">{status.label}</div>
</div>
);
};
export const RejoinIndicatorDisplay = ({
gap,
status,
}: {
gap?: number | string;
status?: 'Clear' | 'Caution' | 'Do Not Rejoin';
}) => {
const statusBg =
status === 'Clear'
? 'bg-green-700'
: status === 'Caution'
? 'bg-amber-600'
: 'bg-red-700';
const gapLabel = gap ?? '--s';
return (
<div
className={`w-full flex justify-between rounded-sm p-2 font-bold text-white ${statusBg}`}
>
<div className="text-lg">{gapLabel}</div>
<div className="text-lg">{status}</div>
</div>
);
};