Skip to content

Commit

Permalink
get keyboard shortcuts and sounds working
Browse files Browse the repository at this point in the history
  • Loading branch information
nicknisi committed Apr 17, 2022
1 parent 0b3ca16 commit ab1ce55
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 22 deletions.
22 changes: 17 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ import classes from './App.module.css';
import { Contestant } from './components/Contestant';
import { Contestants } from './components/Contestants';
import { Round } from './components/Round';
import { useGameData, useGameStatus, useGameView } from './hooks/game';
import { useAudioControls } from './hooks/audio';
import { useGameControls, useGameData, useGameStatus, useGameView } from './hooks/game';

function App() {
const loaded = useGameStatus();
const { currentRound, style: gameStyle, name: gameName, contestants, round, numRounds, nextRound } = useGameData();
let winner = undefined;
const {
currentRound,
style: gameStyle,
name: gameName,
contestants,
round,
numRounds,
setRound,
style,
winner,
} = useGameData();
useGameControls();
useAudioControls(style);
const view = useGameView();

return (
Expand All @@ -21,7 +33,7 @@ function App() {
className={classes.round}
disabled={currentRound >= numRounds - 1}
onClick={() => {
nextRound();
setRound();
}}
>
<div className={classes.roundNumber}>Round {String(currentRound + 1)}</div>
Expand All @@ -32,7 +44,7 @@ function App() {
{winner ? (
<div key="winner-view" className={classes.winner}>
<h1>Winner</h1>
<Contestant {...winner} />
<Contestant hideControls {...winner} />
</div>
) : view === 'contestants' ? (
<Contestants key="contestants-view" horizontal contestants={contestants} />
Expand Down
21 changes: 12 additions & 9 deletions src/components/Contestant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import classes from './contestant.module.css';

export interface ContestantProps extends ContestantData {
large?: boolean;
hideControls?: boolean;
}

export const usePlayer = (player: ContestantData) => {
Expand All @@ -28,18 +29,20 @@ export const usePlayer = (player: ContestantData) => {
};
};

export function Contestant({ large, name, handle, avatar, score }: ContestantProps) {
export function Contestant({ large, name, handle, avatar, score, hideControls }: ContestantProps) {
const { increment, decrement } = usePlayer({ handle, avatar, name, score });
return (
<div className={`${classes.root} ${large ? classes.large : ''}`}>
<div className="actions">
<button className={classes.actionButton} onClick={() => increment()}>
+
</button>
<button className={classes.actionButton} onClick={() => decrement()}>
-
</button>
</div>
{!hideControls && (
<div className="actions">
<button className={classes.actionButton} onClick={() => increment()}>
+
</button>
<button className={classes.actionButton} onClick={() => decrement()}>
-
</button>
</div>
)}
<img className={classes.avatar} src={avatar ?? `http://localhost:8888/?handle=${handle}`} />
<div className={classes.info}>
<div className={classes.name}>{name}</div>
Expand Down
74 changes: 74 additions & 0 deletions src/hooks/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffectOnce } from './utils';
import { GameStyle } from '../types';

const sounds = {
correctAnswer: 'Correct Answer.wav',
correctSteal: 'Correct Steal.wav',
jsDangerStressTheme: 'JS Danger Stress Theme.wav',
jsDangerTheme: 'JS Danger Theme.wav',
goPanicTheme: 'Go Panic Theme.wav',
goPanicWinner: 'Go Panic Winner.wav',
jsDangerWinner: 'JS Danger Winner.wav',
timesUp: 'Times Up.wav',
wrongAnswer: 'Wrong Answer.wav',
wrongSteal: 'Wrong Steal.wav',
} as const;

const players = new Map<string, HTMLAudioElement>();

function stopAudio() {
players.forEach((player) => {
player.pause();
player.currentTime = 0;
});
}

function playAudio(sound: keyof typeof sounds) {
const path = `assets/${sounds[sound]}`;
if (!players.has(path)) {
players.set(path, new Audio(path));
}
const player = players.get(path)!;
player.play();
}

export const useAudioControls = (style: GameStyle) => {
useEffectOnce(() => {
const listener = (event: KeyboardEvent) => {
switch (event.key) {
case 'Escape':
case 'q':
case 'Q':
stopAudio();
break;
case 'b':
case 'B':
playAudio('timesUp');
break;
case 'y':
playAudio('correctAnswer');
break;
case 'Y':
playAudio('correctSteal');
break;
case 'n':
playAudio('wrongAnswer');
break;
case 'N':
playAudio('wrongSteal');
break;
case 't':
playAudio(`${style}Theme`);
break;
case 'T':
playAudio('jsDangerStressTheme');
break;
case 'w':
playAudio(`${style}Winner`);
break;
}
};
globalThis.document.addEventListener('keyup', listener);
return () => globalThis.document.removeEventListener('keyup', listener);
});
};
47 changes: 43 additions & 4 deletions src/hooks/game.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useActor, useSelector } from '@xstate/react';
import { useCallback, useContext, useMemo } from 'react';
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { StateFrom, StateValueMap } from 'xstate';
import { GameContext } from '../GameProvider';
import { gameMachine, GameMachineContext } from '../machines/gameMachine';
import { Category, GameView, Question } from '../types';
import { useEffectOnce } from './utils';

export const useGameService = () => {
const context = useContext(GameContext);
Expand All @@ -27,7 +28,7 @@ export const useGameActor = () => {

export const useGameStatus = () => {
const service = useGameService();
const loaded = useSelector(service, (state) => state.matches('game'));
const loaded = useSelector(service, (state) => !state.matches('load'));
return loaded;
};

Expand All @@ -36,7 +37,7 @@ export const useGameData = () => {
const { contestants, currentContestant, currentQuestion, currentRound, name, rounds, style, winner } = state.context;
const round = useMemo(() => rounds?.[currentRound], [rounds, currentRound]);
const numRounds = useMemo(() => rounds?.length, [rounds]);
const nextRound = useCallback(
const setRound = useCallback(
(round: 1 | -1 = 1) => {
const nextRound =
(currentRound + round) % numRounds === 0 ? 0 : currentRound + round < 0 ? numRounds - 1 : currentRound + round;
Expand All @@ -55,7 +56,7 @@ export const useGameData = () => {
winner,
round,
numRounds,
nextRound,
setRound,
};
};

Expand Down Expand Up @@ -95,3 +96,41 @@ export const useGameSelector = <T>(selector: (state: StateFrom<typeof gameMachin

export const useValue = <K extends keyof GameMachineContext, V extends GameMachineContext[K]>(key: K) =>
useGameSelector<V>((state) => state.context[key] as V);

export const useGameControls = () => {
const send = useSendEvent();
const { setRound, contestants = [] } = useGameData();
console.log('CONTE', contestants);
useEffect(() => {
const listener = (event: KeyboardEvent) => {
switch (event.key) {
case 'Escape':
case 'q':
case 'Q':
send({ type: 'CLOSE_QUESTION' });
break;
case 'c':
case 'C':
send({ type: 'TOGGLE_CONTESTANTS' });
break;
case 'ArrowRight':
setRound(1);
break;
case 'ArrowLeft':
setRound(-1);
break;
case '1':
case '2':
case '3':
case '4':
send({ type: 'SET_WINNER', winner: contestants[Number(event.key) - 1] });
break;
case '0':
send({ type: 'SET_WINNER', winner: undefined });
break;
}
};
globalThis.document.addEventListener('keyup', listener);
return () => globalThis.document.removeEventListener('keyup', listener);
}, [contestants]);
};
5 changes: 5 additions & 0 deletions src/hooks/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EffectCallback, useEffect } from 'react';

export const useEffectOnce = (effect: EffectCallback) => {
useEffect(effect, []);
};
12 changes: 8 additions & 4 deletions src/machines/gameMachine.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { assign, createMachine } from 'xstate';
import { Category, Contestant, Game, Question, Round } from '../types';
import { Category, Contestant, Game, GameStyle, Question, Round } from '../types';

export interface GameMachineContext {
name: string;
style: string;
style: GameStyle;
currentRound: number;
currentContestant?: Contestant | null;
contestants: Contestant[];
Expand All @@ -27,7 +27,7 @@ export type GameMachineEvent =
}
| { type: 'TOGGLE_ANSWER' | 'TOGGLE_CONTESTANTS' | 'CLOSE_QUESTION' }
| { type: 'SET_ROUND'; data: { round: number } }
| { type: 'SET_WINNER'; data: { winner: Contestant } }
| { type: 'SET_WINNER'; winner: Contestant | undefined }
| { type: 'INCREMENT_SCORE' | 'DECREMENT_SCORE'; handle: string; value: number }
| { type: 'SET_CURRENT_CONTESTANT'; data: { contestant: Contestant } };

Expand Down Expand Up @@ -110,7 +110,11 @@ export const gameMachine = createMachine<GameMachineContext, GameMachineEvent>(
SET_WINNER: {
target: 'winner',
actions: assign({
winner: (_context, event) => event.data.winner,
winner: (_context, event) => {
const { winner } = event;
console.log('WINNER', winner, event);
return winner;
},
}),
},
},
Expand Down

0 comments on commit ab1ce55

Please sign in to comment.