diff --git a/src/Components/LifeCounter/LifeCounter.tsx b/src/Components/LifeCounter/LifeCounter.tsx index 0e5be1b..866b6b7 100644 --- a/src/Components/LifeCounter/LifeCounter.tsx +++ b/src/Components/LifeCounter/LifeCounter.tsx @@ -13,7 +13,7 @@ import { LoseGameButton } from '../Buttons/LoseButton'; import CommanderDamageBar from '../Counters/CommanderDamageBar'; import ExtraCountersBar from '../Counters/ExtraCountersBar'; import { Paragraph } from '../Misc/TextComponents'; -import PlayerMenu from '../Player/PlayerMenu'; +import PlayerMenu from '../Players/PlayerMenu'; import Health from './Health'; import { baseColors } from '../../../tailwind.config'; @@ -90,13 +90,15 @@ const playerCanLose = (player: Player) => { type LifeCounterProps = { player: Player; opponents: Player[]; + isStartingPlayer?: boolean; }; const RECENT_DIFFERENCE_TTL = 3_000; const LifeCounter = ({ player, opponents }: LifeCounterProps) => { const { updatePlayer, updateLifeTotal } = usePlayers(); - const { settings } = useGlobalSettings(); + const { settings, playing, setPlaying, stopPlayerRandomization } = + useGlobalSettings(); const playingTimerRef = useRef(undefined); const [showPlayerMenu, setShowPlayerMenu] = useState(false); @@ -114,12 +116,10 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => { trackMouse: true, onSwipedDown: (e) => { e.event.stopPropagation(); - console.log(`User DOWN Swiped on player ${player.index}`); setShowPlayerMenu(true); }, onSwipedUp: (e) => { e.event.stopPropagation(); - console.log(`User UP Swiped on player ${player.index}`); setShowPlayerMenu(false); }, @@ -151,12 +151,26 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => { }, [recentDifference, document.body.clientHeight, document.body.clientWidth]); useEffect(() => { - playingTimerRef.current = setTimeout(() => { - localStorage.setItem('playing', 'true'); - }, 10_000); + if ( + player.isStartingPlayer && + ((!playing && + settings.useRandomStartingPlayerInterval && + stopPlayerRandomization) || + (!settings.useRandomStartingPlayerInterval && !playing)) + ) { + playingTimerRef.current = setTimeout(() => { + setPlaying(true); + }, 10_000); + } return () => clearTimeout(playingTimerRef.current); - }, []); + }, [ + player.isStartingPlayer, + playing, + setPlaying, + settings.useRandomStartingPlayerInterval, + stopPlayerRandomization, + ]); player.settings.rotation === Rotation.SideFlipped || player.settings.rotation === Rotation.Side; @@ -191,39 +205,52 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => { style={{ rotate: `${calcRotation}deg` }} {...handlers} > - {settings.showStartingPlayer && - player.isStartingPlayer && - player.showStartingPlayer && ( -
{ + clearTimeout(playingTimerRef.current); + setPlaying(true); + }} + > + { - clearTimeout(playingTimerRef.current); - localStorage.setItem('playing', 'true'); - player.showStartingPlayer = false; - updatePlayer(player); + rotate: `${calcTextRotation}deg`, }} > - -
- 👑 - You start! - (Press to hide) -
-
-
- )} +
+ 👑 + {(stopPlayerRandomization || + !settings.useRandomStartingPlayerInterval) && ( + <> + You start! + (Press to hide) + + )} +
+ + + )} {player.hasLost && ( )} + {settings.useRandomStartingPlayerInterval && + !stopPlayerRandomization && + !playing && ( +
+ )} { this is enabled. + + + Randomize starting player with interval + { + setSettings({ + ...settings, + useRandomStartingPlayerInterval: + !settings.useRandomStartingPlayerInterval, + }); + }} + /> + + + Will randomize between all players at when starting a game, + pressing the screen aborts the interval and chooses the player + that has the crown. + + Keep Awake diff --git a/src/Components/Player/Player.tsx b/src/Components/Player/Player.tsx deleted file mode 100644 index ee009c1..0000000 --- a/src/Components/Player/Player.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import LifeCounter from '../LifeCounter/LifeCounter'; -import { Player as PlayerType } from '../../Types/Player'; -import { twc } from 'react-twc'; - -const getGridArea = (player: PlayerType) => { - switch (player.index) { - case 0: - return 'grid-in-player0'; - case 1: - return 'grid-in-player1'; - case 2: - return 'grid-in-player2'; - case 3: - return 'grid-in-player3'; - case 4: - return 'grid-in-player4'; - case 5: - return 'grid-in-player5'; - default: - throw new Error('Invalid player index'); - } -}; - -const PlayerWrapper = twc.div`w-full h-full bg-black`; - -export const Player = (players: PlayerType[], gridClasses: string) => { - return ( - -
- {players.map((player) => { - const gridArea = getGridArea(player); - - return ( -
- opponent.index !== player.index - )} - /> -
- ); - })} -
-
- ); -}; diff --git a/src/Components/Player/PlayerMenu.tsx b/src/Components/Players/PlayerMenu.tsx similarity index 97% rename from src/Components/Player/PlayerMenu.tsx rename to src/Components/Players/PlayerMenu.tsx index 0fb792c..ab9b49c 100644 --- a/src/Components/Player/PlayerMenu.tsx +++ b/src/Components/Players/PlayerMenu.tsx @@ -104,7 +104,14 @@ const PlayerMenu = ({ containerRef: settingsContainerRef, }); - const { fullscreen, wakeLock, goToStart, settings } = useGlobalSettings(); + const { + fullscreen, + wakeLock, + goToStart, + settings, + setPlaying, + setStopPlayerRandomization, + } = useGlobalSettings(); const { updatePlayer, resetCurrentGame } = usePlayers(); const handleColorChange = (event: React.ChangeEvent) => { @@ -122,6 +129,13 @@ const PlayerMenu = ({ const handleResetGame = () => { resetCurrentGame(); setShowPlayerMenu(false); + setPlaying(false); + setStopPlayerRandomization(false); + }; + + const handleGoToStart = () => { + goToStart(); + setStopPlayerRandomization(false); }; const toggleFullscreen = () => { @@ -291,7 +305,7 @@ const PlayerMenu = ({ cursor: 'pointer', userSelect: 'none', }} - onClick={goToStart} + onClick={handleGoToStart} aria-label="Back to start" > diff --git a/src/Components/Players/Players.tsx b/src/Components/Players/Players.tsx new file mode 100644 index 0000000..92a6427 --- /dev/null +++ b/src/Components/Players/Players.tsx @@ -0,0 +1,144 @@ +import { useEffect, useRef } from 'react'; +import { twc } from 'react-twc'; +import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; +import { usePlayers } from '../../Hooks/usePlayers'; +import { Player as PlayerType } from '../../Types/Player'; +import LifeCounter from '../LifeCounter/LifeCounter'; + +const getGridArea = (player: PlayerType) => { + switch (player.index) { + case 0: + return 'grid-in-player0'; + case 1: + return 'grid-in-player1'; + case 2: + return 'grid-in-player2'; + case 3: + return 'grid-in-player3'; + case 4: + return 'grid-in-player4'; + case 5: + return 'grid-in-player5'; + default: + throw new Error('Invalid player index'); + } +}; + +const PlayersWrapper = twc.div`w-full h-full bg-black`; + +export const Players = (players: PlayerType[], gridClasses: string) => { + const randomIntervalRef = useRef(null); + + const prevRandomIndexRef = useRef(-1); + + const { + settings, + stopPlayerRandomization, + setStopPlayerRandomization, + playing, + } = useGlobalSettings(); + + const { setPlayers } = usePlayers(); + + useEffect(() => { + if ( + settings.useRandomStartingPlayerInterval && + !stopPlayerRandomization && + !playing + ) { + randomIntervalRef.current = setInterval(() => { + let randomIndex: number; + + do { + randomIndex = Math.floor(Math.random() * players.length); + } while (randomIndex === prevRandomIndexRef.current); + + prevRandomIndexRef.current = randomIndex; + setPlayers( + players.map((p) => + p.index === prevRandomIndexRef.current + ? { + ...p, + isStartingPlayer: true, + } + : { + ...p, + isStartingPlayer: false, + } + ) + ); + }, 100); + } + + if (!settings.useRandomStartingPlayerInterval) { + const randomPlayerIndex = Math.floor(Math.random() * players.length); + setPlayers( + players.map((p) => + p.index === randomPlayerIndex + ? { + ...p, + isStartingPlayer: true, + } + : { + ...p, + isStartingPlayer: false, + } + ) + ); + } + return () => { + if (randomIntervalRef.current) { + clearInterval(randomIntervalRef.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + players.length, + playing, + setPlayers, + settings.useRandomStartingPlayerInterval, + stopPlayerRandomization, + ]); + + return ( + + {settings.useRandomStartingPlayerInterval && + !stopPlayerRandomization && + !playing && ( +
{ + if (randomIntervalRef.current) { + clearInterval(randomIntervalRef.current); + randomIntervalRef.current = null; + } + setStopPlayerRandomization(true); + }} + > +
+ PRESS TO SELECT PLAYER +
+
+ )} +
+ {players.map((player) => { + const gridArea = getGridArea(player); + + return ( +
+ opponent.index !== player.index + )} + /> +
+ ); + })} +
+
+ ); +}; diff --git a/src/Components/Views/Play.tsx b/src/Components/Views/Play.tsx index c52dc2a..17c6e1e 100644 --- a/src/Components/Views/Play.tsx +++ b/src/Components/Views/Play.tsx @@ -1,7 +1,7 @@ import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { usePlayers } from '../../Hooks/usePlayers'; import { Orientation } from '../../Types/Settings'; -import { Player } from '../Player/Player'; +import { Players } from '../Players/Players'; import { twc } from 'react-twc'; const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`; @@ -14,52 +14,52 @@ export const Play = () => { switch (players.length) { case 1: if (initialGameSettings?.orientation === Orientation.Portrait) { - Layout = Player(players, 'grid-areas-onePlayerPortrait'); + Layout = Players(players, 'grid-areas-onePlayerPortrait'); } - Layout = Player(players, 'grid-areas-onePlayerLandscape'); + Layout = Players(players, 'grid-areas-onePlayerLandscape'); break; case 2: switch (initialGameSettings?.orientation) { case Orientation.Portrait: - Layout = Player(players, 'grid-areas-twoPlayersOppositePortrait'); + Layout = Players(players, 'grid-areas-twoPlayersOppositePortrait'); break; default: case Orientation.Landscape: - Layout = Player(players, 'grid-areas-twoPlayersSameSideLandscape'); + Layout = Players(players, 'grid-areas-twoPlayersSameSideLandscape'); break; case Orientation.OppositeLandscape: - Layout = Player(players, 'grid-areas-twoPlayersOppositeLandscape'); + Layout = Players(players, 'grid-areas-twoPlayersOppositeLandscape'); break; } break; case 3: if (initialGameSettings?.orientation === Orientation.Portrait) { - Layout = Player(players, 'grid-areas-threePlayersSide'); + Layout = Players(players, 'grid-areas-threePlayersSide'); break; } - Layout = Player(players, 'grid-areas-threePlayers'); + Layout = Players(players, 'grid-areas-threePlayers'); break; default: case 4: if (initialGameSettings?.orientation === Orientation.Portrait) { - Layout = Player(players, 'grid-areas-fourPlayerPortrait'); + Layout = Players(players, 'grid-areas-fourPlayerPortrait'); break; } - Layout = Player(players, 'grid-areas-fourPlayer'); + Layout = Players(players, 'grid-areas-fourPlayer'); break; case 5: if (initialGameSettings?.orientation === Orientation.Portrait) { - Layout = Player(players, 'grid-areas-fivePlayersSide'); + Layout = Players(players, 'grid-areas-fivePlayersSide'); break; } - Layout = Player(players, 'grid-areas-fivePlayers'); + Layout = Players(players, 'grid-areas-fivePlayers'); break; case 6: if (initialGameSettings?.orientation === Orientation.Portrait) { - Layout = Player(players, 'grid-areas-sixPlayersSide'); + Layout = Players(players, 'grid-areas-sixPlayersSide'); break; } - Layout = Player(players, 'grid-areas-sixPlayers'); + Layout = Players(players, 'grid-areas-sixPlayers'); break; } diff --git a/src/Contexts/GlobalSettingsContext.tsx b/src/Contexts/GlobalSettingsContext.tsx index aee0e38..fa06150 100644 --- a/src/Contexts/GlobalSettingsContext.tsx +++ b/src/Contexts/GlobalSettingsContext.tsx @@ -22,6 +22,10 @@ export type GlobalSettingsContextType = { setInitialGameSettings: (initialGameSettings: InitialGameSettings) => void; settings: Settings; setSettings: (settings: Settings) => void; + playing: boolean; + setPlaying: (playing: boolean) => void; + stopPlayerRandomization: boolean; + setStopPlayerRandomization: (stopRandom: boolean) => void; isPWA: boolean; }; diff --git a/src/Contexts/PlayersContext.tsx b/src/Contexts/PlayersContext.tsx index a1e9479..9da2124 100644 --- a/src/Contexts/PlayersContext.tsx +++ b/src/Contexts/PlayersContext.tsx @@ -7,6 +7,8 @@ export type PlayersContextType = { updatePlayer: (updatedPlayer: Player) => void; updateLifeTotal: (player: Player, updatedLifeTotal: number) => number; resetCurrentGame: () => void; + startingPlayerIndex: number; + setStartingPlayerIndex: (index: number) => void; }; export const PlayersContext = createContext(null); diff --git a/src/Data/getInitialPlayers.ts b/src/Data/getInitialPlayers.ts index 3584f43..d64ebab 100644 --- a/src/Data/getInitialPlayers.ts +++ b/src/Data/getInitialPlayers.ts @@ -191,10 +191,8 @@ export const createInitialPlayers = ({ }: InitialGameSettings): Player[] => { const players: Player[] = []; const availableColors = [...presetColors]; // Create a copy of the colors array - const firstPlayerIndex = Math.floor(Math.random() * numberOfPlayers); for (let i = 0; i <= numberOfPlayers - 1; i++) { - const isStartingPlayer = i === firstPlayerIndex; const colorIndex = Math.floor(Math.random() * availableColors.length); const color = availableColors[colorIndex]; @@ -224,11 +222,10 @@ export const createInitialPlayers = ({ usePoison: false, rotation, }, - isStartingPlayer, - showStartingPlayer: isStartingPlayer, extraCounters: [], commanderDamage, hasLost: false, + isStartingPlayer: false, isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped, }; diff --git a/src/Providers/GlobalSettingsProvider.tsx b/src/Providers/GlobalSettingsProvider.tsx index bd2b794..4dba78e 100644 --- a/src/Providers/GlobalSettingsProvider.tsx +++ b/src/Providers/GlobalSettingsProvider.tsx @@ -21,11 +21,23 @@ export const GlobalSettingsProvider = ({ const savedShowPlay = localStorage.getItem('showPlay'); const savedGameSettings = localStorage.getItem('initialGameSettings'); const savedSettings = localStorage.getItem('settings'); + const savedPlaying = localStorage.getItem('playing'); + + const [playing, setPlaying] = useState( + savedPlaying ? savedPlaying === 'true' : false + ); + const setPlayingAndLocalStorage = (playing: boolean) => { + setPlaying(playing); + localStorage.setItem('playing', String(playing)); + }; const [showPlay, setShowPlay] = useState( savedShowPlay ? savedShowPlay === 'true' : false ); + const [stopPlayerRandomization, setStopPlayerRandomization] = + useState(false); + const [initialGameSettings, setInitialGameSettings] = useState( savedGameSettings ? JSON.parse(savedGameSettings) : null @@ -39,6 +51,7 @@ export const GlobalSettingsProvider = ({ keepAwake: true, showStartingPlayer: true, showPlayerMenuCog: true, + useRandomStartingPlayerInterval: false, } ); @@ -47,6 +60,8 @@ export const GlobalSettingsProvider = ({ localStorage.removeItem('players'); localStorage.removeItem('playing'); localStorage.removeItem('showPlay'); + + setPlaying(false); setShowPlay(false); }; @@ -153,10 +168,14 @@ export const GlobalSettingsProvider = ({ goToStart, showPlay, setShowPlay, + playing, + setPlaying: setPlayingAndLocalStorage, initialGameSettings, setInitialGameSettings, settings, setSettings, + stopPlayerRandomization, + setStopPlayerRandomization, isPWA: window?.matchMedia('(display-mode: standalone)').matches, }; }, [ @@ -165,10 +184,12 @@ export const GlobalSettingsProvider = ({ initialGameSettings, isFullscreen, isSupported, + playing, release, request, settings, showPlay, + stopPlayerRandomization, type, ]); diff --git a/src/Providers/PlayersProvider.tsx b/src/Providers/PlayersProvider.tsx index 7e27b5e..b5048ae 100644 --- a/src/Providers/PlayersProvider.tsx +++ b/src/Providers/PlayersProvider.tsx @@ -7,6 +7,17 @@ import { InitialGameSettings } from '../Types/Settings'; export const PlayersProvider = ({ children }: { children: ReactNode }) => { const savedPlayers = localStorage.getItem('players'); + const savedStartingPlayerIndex = localStorage.getItem('startingPlayerIndex'); + + const [startingPlayerIndex, setStartingPlayerIndex] = useState( + savedStartingPlayerIndex ? parseInt(savedStartingPlayerIndex) : -1 + ); + + const setStartingPlayerIndexAndLocalStorage = (index: number) => { + setStartingPlayerIndex(index); + localStorage.setItem('startingPlayerIndex', String(index)); + }; + const [players, setPlayers] = useState( savedPlayers ? JSON.parse(savedPlayers) : [] ); @@ -50,9 +61,7 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => { return; } - const startingPlayerIndex = Math.floor( - Math.random() * initialGameSettings.numberOfPlayers - ); + const newStartingPlayerIndex = Math.floor(Math.random() * players.length); players.forEach((player: Player) => { player.commanderDamage.map((damage) => { @@ -65,16 +74,9 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => { }); player.lifeTotal = initialGameSettings.startingLifeTotal; - player.hasLost = false; - const isStartingPlayer = player.index === startingPlayerIndex; - - player.isStartingPlayer = isStartingPlayer; - - if (player.isStartingPlayer) { - player.showStartingPlayer = true; - } + player.isStartingPlayer = newStartingPlayerIndex === player.index; updatePlayer(player); }); @@ -87,8 +89,10 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => { updatePlayer, updateLifeTotal, resetCurrentGame, + startingPlayerIndex, + setStartingPlayerIndex: setStartingPlayerIndexAndLocalStorage, }; - }, [players]); + }, [players, startingPlayerIndex]); return ( diff --git a/src/Types/Player.ts b/src/Types/Player.ts index 93ecd3f..2082975 100644 --- a/src/Types/Player.ts +++ b/src/Types/Player.ts @@ -6,7 +6,6 @@ export type Player = { commanderDamage: CommanderDamage[]; extraCounters: ExtraCounter[]; isStartingPlayer: boolean; - showStartingPlayer: boolean; hasLost: boolean; isSide: boolean; }; diff --git a/src/Types/Settings.ts b/src/Types/Settings.ts index 48e025c..e697560 100644 --- a/src/Types/Settings.ts +++ b/src/Types/Settings.ts @@ -17,6 +17,7 @@ export type Settings = { showStartingPlayer: boolean; showPlayerMenuCog: boolean; goFullscreenOnStart: boolean; + useRandomStartingPlayerInterval: boolean; }; export type InitialGameSettings = {