Skip to content

Commit 90c9dc0

Browse files
committed
feat(game-round-time): implement round time and cast time display components
1 parent b875262 commit 90c9dc0

File tree

2 files changed

+172
-161
lines changed

2 files changed

+172
-161
lines changed

electron/renderer/components/game/game-bottom-bar.tsx

+5-161
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,20 @@
1-
import { EuiFieldText, useEuiTheme } from '@elastic/eui';
2-
import { animated, useSpring } from '@react-spring/web';
1+
import { EuiFieldText } from '@elastic/eui';
32
import type { KeyboardEvent, KeyboardEventHandler, ReactNode } from 'react';
4-
import { useCallback, useEffect, useRef, useState } from 'react';
5-
import type { GameEvent } from '../../../common/game/types.js';
6-
import { GameEventType } from '../../../common/game/types.js';
3+
import { useCallback } from 'react';
74
import { isEmpty } from '../../../common/string/string.utils.js';
85
import { useCommandHistory } from '../../hooks/command-history.jsx';
9-
import { useSubscribe } from '../../hooks/pubsub.jsx';
106
import { runInBackground } from '../../lib/async/run-in-background.js';
11-
12-
interface GameTimeDisplayProps {
13-
currentTime: number;
14-
initialTime: number;
15-
fillColor: string;
16-
textColor: string;
17-
}
18-
19-
const GameTimeDisplay: React.FC<GameTimeDisplayProps> = (
20-
options: GameTimeDisplayProps
21-
) => {
22-
const { currentTime, initialTime } = options;
23-
24-
const fillColor = currentTime > 0 ? options.fillColor : 'inherit';
25-
const textColor = currentTime > 0 ? options.textColor : 'inherit';
26-
27-
const fillHeight = (currentTime / initialTime) * 100 || 0;
28-
29-
const fillSpringProps = useSpring({
30-
height: `${fillHeight}%`,
31-
immediate: true,
32-
});
33-
34-
return (
35-
<div
36-
style={{
37-
display: 'inline-block',
38-
width: '30px',
39-
height: '30px',
40-
position: 'relative',
41-
margin: 0,
42-
padding: 0,
43-
}}
44-
>
45-
<animated.div
46-
style={{
47-
position: 'absolute',
48-
bottom: 0,
49-
width: '100%',
50-
backgroundColor: fillColor,
51-
...fillSpringProps,
52-
}}
53-
/>
54-
<div
55-
style={{
56-
position: 'absolute',
57-
width: '100%',
58-
height: '100%',
59-
lineHeight: '30px',
60-
textAlign: 'center',
61-
zIndex: 1,
62-
color: textColor,
63-
}}
64-
>
65-
{currentTime}
66-
</div>
67-
</div>
68-
);
69-
};
70-
71-
GameTimeDisplay.displayName = 'GameTimeDisplay';
7+
import { GameRoundTime } from './game-roundtime.jsx';
728

739
export interface GameBottomBarProps {
74-
// TODO
7510
todo?: true;
7611
}
7712

7813
export const GameBottomBar: React.FC<GameBottomBarProps> = (
79-
props: GameBottomBarProps
14+
_props: GameBottomBarProps
8015
): ReactNode => {
8116
const { input, handleKeyDown, handleOnChange } = useCommandHistory();
8217

83-
const { euiTheme } = useEuiTheme();
84-
85-
const nowInSeconds = useCallback(() => {
86-
return Math.floor(Date.now() / 1000);
87-
}, []);
88-
8918
const onKeyDown = useCallback<KeyboardEventHandler<HTMLInputElement>>(
9019
(event: KeyboardEvent<HTMLInputElement>) => {
9120
// Handle any history navigation.
@@ -101,97 +30,12 @@ export const GameBottomBar: React.FC<GameBottomBarProps> = (
10130
[handleKeyDown]
10231
);
10332

104-
// Machine-friendly timestamps (in seconds).
105-
// Example: '1737941270'.
106-
const serverTimeRef = useRef<number>(0); // current time on game server
107-
const roundTimeRef = useRef<number>(0); // future time when can take action
108-
const castTimeRef = useRef<number>(0); // future time when spell prepared
109-
110-
// User-friendly remaining durations (in seconds).
111-
// Example: '6' (for 6 seconds remaining).
112-
const [currentRT, setCurrentRT] = useState<number>(0);
113-
const [initialRT, setInitialRT] = useState<number>(0);
114-
const [currentCT, setCurrentCT] = useState<number>(0);
115-
const [initialCT, setInitialCT] = useState<number>(0);
116-
117-
// Interval for updating remaining round time.
118-
// Used to refresh the UI every second.
119-
const intervalRef = useRef<NodeJS.Timeout>();
120-
121-
// Recalculates the remaining round time.
122-
const calculateRoundTimes = useCallback(() => {
123-
const elapsed = Math.floor(Date.now() / 1000); // current time in seconds
124-
const remainingRT = Math.max(0, roundTimeRef.current - elapsed);
125-
const remainingCT = Math.max(0, castTimeRef.current - elapsed);
126-
setCurrentRT(remainingRT);
127-
setCurrentCT(remainingCT);
128-
}, []);
129-
130-
// Periodically recalculate the round time UI for the user.
131-
useEffect(() => {
132-
intervalRef.current = setInterval(() => {
133-
calculateRoundTimes();
134-
}, 1000);
135-
return () => {
136-
clearInterval(intervalRef.current);
137-
};
138-
}, [calculateRoundTimes]);
139-
140-
// Technically, we don't need to explicitly recalculate the round time
141-
// when these game events are received, but doing so will immediately
142-
// refresh the UI when new roundtimes are incurred rather than on a delay.
143-
useSubscribe(['game:event'], (gameEvent: GameEvent) => {
144-
switch (gameEvent.type) {
145-
case GameEventType.SERVER_TIME:
146-
serverTimeRef.current = gameEvent.time;
147-
calculateRoundTimes();
148-
break;
149-
case GameEventType.ROUND_TIME:
150-
roundTimeRef.current = gameEvent.time;
151-
setInitialRT(roundTimeRef.current - nowInSeconds());
152-
calculateRoundTimes();
153-
break;
154-
case GameEventType.CAST_TIME:
155-
castTimeRef.current = gameEvent.time;
156-
setInitialCT(castTimeRef.current - nowInSeconds());
157-
calculateRoundTimes();
158-
break;
159-
}
160-
});
161-
16233
return (
16334
<EuiFieldText
16435
value={input}
16536
compressed={true}
16637
fullWidth={true}
167-
prepend={
168-
<>
169-
<div
170-
style={{
171-
display: 'inline-block',
172-
textAlign: 'center',
173-
marginRight: '5px',
174-
}}
175-
>
176-
<div>R</div>
177-
<div>T</div>
178-
</div>
179-
<GameTimeDisplay
180-
currentTime={currentRT}
181-
initialTime={initialRT}
182-
// TODO: use text color that contrats better with background
183-
textColor={euiTheme.colors.dangerText}
184-
fillColor={euiTheme.colors.warning}
185-
/>
186-
<GameTimeDisplay
187-
currentTime={currentCT}
188-
initialTime={initialCT}
189-
// TODO: use text color that contrats better with background
190-
textColor={euiTheme.colors.dangerText}
191-
fillColor={euiTheme.colors.primary}
192-
/>
193-
</>
194-
}
38+
prepend={<GameRoundTime />}
19539
tabIndex={0}
19640
onKeyDown={onKeyDown}
19741
onChange={handleOnChange}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { useEuiTheme } from '@elastic/eui';
2+
import { animated, useSpring } from '@react-spring/web';
3+
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import type { GameEvent } from '../../../common/game/types.js';
5+
import { GameEventType } from '../../../common/game/types.js';
6+
import { useSubscribe } from '../../hooks/pubsub.jsx';
7+
8+
interface GameTimeDisplayProps {
9+
currentTime: number;
10+
initialTime: number;
11+
fillColor: string;
12+
textColor: string;
13+
type: 'RoundTime' | 'CastTime';
14+
}
15+
16+
const GameTimeDisplay: React.FC<GameTimeDisplayProps> = (
17+
options: GameTimeDisplayProps
18+
) => {
19+
const { currentTime, initialTime, type } = options;
20+
21+
const { euiTheme } = useEuiTheme();
22+
23+
const typeAbbrev = type === 'RoundTime' ? 'RT' : 'CT';
24+
25+
const fillColor = currentTime > 0 ? options.fillColor : 'inherit';
26+
const textColor = currentTime > 0 ? options.textColor : 'inherit';
27+
28+
const fillWidth = (currentTime / initialTime) * 100 || 0;
29+
30+
const fillSpringProps = useSpring({
31+
width: `${fillWidth}%`,
32+
immediate: true,
33+
});
34+
35+
return (
36+
<div
37+
style={{
38+
display: 'inline-block',
39+
width: '30px',
40+
height: '25px',
41+
position: 'relative',
42+
margin: 0,
43+
padding: 0,
44+
border: '1px solid',
45+
borderRadius: '5px',
46+
borderColor: fillColor,
47+
}}
48+
>
49+
<animated.div
50+
style={{
51+
position: 'absolute',
52+
left: 0,
53+
width: fillSpringProps.width,
54+
height: '100%',
55+
backgroundColor: fillColor,
56+
}}
57+
/>
58+
<div
59+
style={{
60+
position: 'absolute',
61+
width: '100%',
62+
height: '100%',
63+
textAlign: 'center',
64+
lineHeight: euiTheme.size.l,
65+
fontSize: euiTheme.size.m,
66+
color: textColor,
67+
}}
68+
>
69+
{currentTime > 0 && currentTime}
70+
{currentTime <= 0 && <span title={type}>{typeAbbrev}</span>}
71+
</div>
72+
</div>
73+
);
74+
};
75+
76+
GameTimeDisplay.displayName = 'GameTimeDisplay';
77+
78+
export interface GameRoundTimeProps {
79+
todo?: true;
80+
}
81+
82+
export const GameRoundTime: React.FC<GameRoundTimeProps> = (
83+
_props: GameRoundTimeProps
84+
) => {
85+
const { euiTheme } = useEuiTheme();
86+
87+
const nowInSeconds = useCallback(() => {
88+
return Math.floor(Date.now() / 1000);
89+
}, []);
90+
91+
// Machine-friendly timestamps (in seconds).
92+
// Example: '1737941270'.
93+
const serverTimeRef = useRef<number>(0); // current time on game server
94+
const roundTimeRef = useRef<number>(0); // future time when can take action
95+
const castTimeRef = useRef<number>(0); // future time when spell prepared
96+
97+
// User-friendly remaining durations (in seconds).
98+
// Example: '6' (for 6 seconds remaining).
99+
const [currentRT, setCurrentRT] = useState<number>(0);
100+
const [initialRT, setInitialRT] = useState<number>(0);
101+
const [currentCT, setCurrentCT] = useState<number>(0);
102+
const [initialCT, setInitialCT] = useState<number>(0);
103+
104+
// Interval for updating remaining round time.
105+
// Used to refresh the UI every second.
106+
const intervalRef = useRef<NodeJS.Timeout>();
107+
108+
// Recalculates the remaining round time.
109+
const calculateRoundTimes = useCallback(() => {
110+
const elapsed = nowInSeconds();
111+
setCurrentRT(Math.max(0, roundTimeRef.current - elapsed));
112+
setCurrentCT(Math.max(0, castTimeRef.current - elapsed));
113+
}, [nowInSeconds]);
114+
115+
// Periodically recalculate the round time UI for the user.
116+
useEffect(() => {
117+
intervalRef.current = setInterval(() => {
118+
calculateRoundTimes();
119+
}, 1000);
120+
return () => {
121+
clearInterval(intervalRef.current);
122+
};
123+
}, [calculateRoundTimes]);
124+
125+
// Technically, we don't need to explicitly recalculate the round time
126+
// when these game events are received, but doing so will immediately
127+
// refresh the UI when new roundtimes are incurred rather than on a delay.
128+
useSubscribe(['game:event'], (gameEvent: GameEvent) => {
129+
switch (gameEvent.type) {
130+
case GameEventType.SERVER_TIME:
131+
serverTimeRef.current = gameEvent.time;
132+
calculateRoundTimes();
133+
break;
134+
case GameEventType.ROUND_TIME:
135+
roundTimeRef.current = gameEvent.time;
136+
setInitialRT(roundTimeRef.current - nowInSeconds());
137+
calculateRoundTimes();
138+
break;
139+
case GameEventType.CAST_TIME:
140+
castTimeRef.current = gameEvent.time;
141+
setInitialCT(castTimeRef.current - nowInSeconds());
142+
calculateRoundTimes();
143+
break;
144+
}
145+
});
146+
147+
return (
148+
<>
149+
<GameTimeDisplay
150+
type="RoundTime"
151+
currentTime={currentRT}
152+
initialTime={initialRT}
153+
textColor={euiTheme.colors.fullShade}
154+
fillColor="#FF8C00"
155+
/>
156+
<GameTimeDisplay
157+
type="CastTime"
158+
currentTime={currentCT}
159+
initialTime={initialCT}
160+
textColor={euiTheme.colors.fullShade}
161+
fillColor={euiTheme.colors.primary}
162+
/>
163+
</>
164+
);
165+
};
166+
167+
GameRoundTime.displayName = 'GameRoundTime';

0 commit comments

Comments
 (0)