KillFeed.jsx 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { useEffect, useState } from "react";
  2. import {
  3. dispatch,
  4. useGameId,
  5. useIsMuted,
  6. usePlayerName,
  7. } from "../../../domain/gameStore";
  8. import { usePlayers } from "../../../hooks/useGameInfo";
  9. import styles from "./KillFeed.module.css";
  10. import hitmarker from "../../../assets/hitmarker.svg";
  11. import hitsound from "../../../assets/hitsound.wav";
  12. // okay, in an ideal world this would be part of the game store or something
  13. // and it would get properly managed by reactive state
  14. // but also, this totally works as is, and the only downside is it might potentially grow too big
  15. // but that only happens if someone plays that many gun games without ever leaving the window
  16. const shownItems = new Set();
  17. const KillFeed = () => {
  18. const muted = useIsMuted();
  19. const playerName = usePlayerName();
  20. const gameId = useGameId();
  21. const players = usePlayers();
  22. useEffect(() => {
  23. if (players?.find(({ currentRound }) => currentRound === null)) {
  24. dispatch.goToSummary();
  25. }
  26. }, [players]);
  27. const [shownItemsState, setShownItemsState] = useState(shownItems);
  28. const [display, setDisplay] = useState([]);
  29. useEffect(() => {
  30. const toDisplay =
  31. players
  32. ?.filter(({ name }) => name !== playerName)
  33. ?.flatMap(({ name, guesses }) =>
  34. Object.entries(guesses).map(([round, { score }]) => ({
  35. name,
  36. round,
  37. score,
  38. }))
  39. )
  40. ?.filter(
  41. ({ name, round }) =>
  42. !shownItemsState.has(`${gameId}-${name}-${round}`)
  43. ) ?? [];
  44. setDisplay(toDisplay);
  45. const timeout = setTimeout(() => {
  46. toDisplay.forEach(({ name, round }) => {
  47. shownItems.add(`${gameId}-${name}-${round}`);
  48. });
  49. setShownItemsState(new Set(shownItems));
  50. }, 5000);
  51. return () => {
  52. clearTimeout(timeout);
  53. };
  54. }, [shownItemsState, gameId, players, playerName]);
  55. useEffect(() => {
  56. if (!muted) {
  57. display.forEach(() => {
  58. const audio = new Audio(hitsound);
  59. audio.volume = 0.5;
  60. // delay up to half a second so overlapping sounds better
  61. const delayedPlay = () =>
  62. setTimeout(() => audio.play(), Math.random() * 500);
  63. audio.addEventListener("canplaythrough", delayedPlay);
  64. // clean up after ourselves in the hopes that the browser actually deletes this audio element
  65. audio.addEventListener("ended", () =>
  66. audio.removeEventListener("canplaythrough", delayedPlay)
  67. );
  68. });
  69. }
  70. }, [display, muted]);
  71. return (
  72. <>
  73. {display.map(() => (
  74. <img
  75. alt="hitmarker"
  76. className={styles.hitmarker}
  77. style={{
  78. top: `${10 + Math.random() * 80}vh`,
  79. left: `${10 + Math.random() * 80}vw`,
  80. }}
  81. src={hitmarker}
  82. />
  83. ))}
  84. <div className={styles.feed}>
  85. {display.map(({ name, round, score }) => (
  86. <span className={styles.item}>
  87. <span className={styles.name}>{name}</span>{" "}
  88. {score >= 4950 ? "🎯" : "🖱️"} 🗺️ {round}
  89. </span>
  90. ))}
  91. </div>
  92. </>
  93. );
  94. };
  95. export default KillFeed;