Compare commits

...

20 Commits
0.6.5 ... 0.8.1

Author SHA1 Message Date
Viktor Rådberg
fa5829b402 fix keep away state 2024-03-23 16:23:03 +01:00
Viktor Rådberg
71f26d0dc5 fix keep awake toggle and layout styling 2024-03-23 16:22:48 +01:00
Viktor Rådberg
3a568fc3ab Better scaling on small devices 2024-03-23 16:05:29 +01:00
Viktor Rådberg
355f4bd4cd track only on prod, and add life changed amount tracking 2024-03-23 12:39:07 +01:00
Viktor Rådberg
17e174bfe1 new deploy 2024-03-23 11:24:14 +01:00
Viktor Rådberg
e1e8da858b remove prod check 2024-03-23 11:23:46 +01:00
Viktor Rådberg
e02f071415 new deploy 2024-03-23 11:23:06 +01:00
Viktor Rådberg
e04f31bb67 prod log 2024-03-23 11:22:12 +01:00
Viktor Rådberg
e5386d08a4 fix settings cog color 2024-03-17 19:01:15 +01:00
Viktor Rådberg
d6cd678e9f fix random interval wrapper showing if enabled but show player is disabled 2024-03-17 18:41:58 +01:00
Viktor Rådberg
334b46db6e bump 2024-03-16 22:29:25 +01:00
Viktor Rådberg
e03ecc6f51 Merge pull request #33 from Vikeo/random-player-interval
random player interval
2024-03-16 22:28:49 +01:00
Viktor Rådberg
d4dc44076d fix lint 2024-03-16 22:28:37 +01:00
Viktor Rådberg
a1b5cfd871 fix tsc 2024-03-16 22:26:06 +01:00
Viktor Rådberg
f11eea5e53 better styling 2024-03-16 22:23:03 +01:00
Viktor Rådberg
905912a7fd fix random interval 2024-03-16 21:59:24 +01:00
Viktor Rådberg
a90dd7c9ea wip 2024-03-16 14:40:18 +01:00
Viktor Rådberg
ef1310d674 bump 2024-03-16 13:22:47 +01:00
Viktor Rådberg
fe3bb6c78c show starting player untill press 2024-03-16 13:22:03 +01:00
Viktor Rådberg
6d2b3b6a6f Add option to show player menu cog 2024-03-16 12:29:16 +01:00
23 changed files with 663 additions and 294 deletions

View File

@@ -1,8 +1,12 @@
index.html,1705225256081,6ef0d7e2de82bf64addbb9294fb28845fd06daaa544b010a47422c12ae3ad97f index.html,1711189442688,fa2549e32940c356ac5cee88c8db61076ad62fb4e599858c8e45cfc68cd901c4
robots.txt,1705225255906,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 manifest.json,1711189442512,7ff5111aa04a42adff3b38924ec467b6f345ed0309dba1486dc9b783b60c2a9d
manifest.json,1705225255906,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a registerSW.js,1711189442688,5b6445b5215737c53ef0d379c151d57de165a056de2d1c5812ed614f158ebcbd
assets/index-08359bdb.css,1705225256081,d2766260d28230d960d75362810713efaddf40687205e697432b52869f162af7 sw.js,1711189443521,9c09d33ea573bb818864bfad526fa911839637171773eca8e31905458679846d
logo192.png,1705225255905,3b0fcf91fe2128f493de0bce2f6e2d35520a4260a04e05b8d855181359b3d3fe robots.txt,1711189442512,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
favicon.ico,1705225255905,75661e6187b524767554b4f28ec09a64bc72b0bb102a0b453aaead88519d9ed3 manifest.webmanifest,1711189442688,f2bf253209f6e292a6b0dbfa06fb4ac188eb5f2dba568c3ad5511b9ed52c1f51
logo512.png,1705225255906,cf49739c9e6890bbfcd4157f299dde425df60759b7320ae9188d7ab9dc51e8ca workbox-3e911b1d.js,1711189443521,d5dbc868a5c07af633d29de7ba3ffe37542aaaabdf33713b4298df31f92f11ff
assets/index-20658f4b.js,1705225256081,742f2c10740beea3a23f269aa6266b3c288d1fd9c7e20b6829034e8a898bf1e1 assets/index-WLCHZTqE.css,1711189442688,877e5ea9bfd3a1ca0e6449e8213da8a3c7717e530370f12669bb5c70dd21e700
favicon.ico,1711189442511,8cefe5adbf00d337d8633fb744b9f2c4914f769b319be4bb7e184b7a4aa17160
logo192.png,1711189442511,3d1e2e6f064d4fd325828f21bb6493ff0dbf2390b0e7d2aba9f2b6def4829799
logo512.png,1711189442511,892a4da1cc5434929a83a71fcbcb0d0d80aa82f44e3c21e9b20ffe9267197133
assets/index-OHs0lOr7.js,1711189442688,aa0dca732cd5b6f621ecb7c6dbcbfdbccde78941cfad954f6626d4ff83040c7f

View File

@@ -1,7 +1,7 @@
{ {
"name": "life-trinket", "name": "life-trinket",
"private": true, "private": true,
"version": "0.6.5", "version": "0.8.1",
"type": "commonjs", "type": "commonjs",
"engines": { "engines": {
"node": ">=18", "node": ">=18",

View File

@@ -21,9 +21,9 @@ const Container = twc.div<RotationDivProps>((props) => [
]); ]);
export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [ export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [
'flex absolute flex-row flex-grow pointer-events-none', 'flex absolute flex-row flex-grow pointer-events-none overflow-x-scroll overflow-y-hidden',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse h-full w-auto bottom-auto' ? 'flex-col-reverse h-full w-auto bottom-auto right-0'
: 'w-full bottom-0', : 'w-full bottom-0',
]); ]);

View File

@@ -53,23 +53,9 @@ const Health = ({
differenceKey, differenceKey,
recentDifference, recentDifference,
}: HealthProps) => { }: HealthProps) => {
const [showStartingPlayer, setShowStartingPlayer] = useState(
localStorage.getItem('playing') === 'true'
);
const [fontSize, setFontSize] = useState(16); const [fontSize, setFontSize] = useState(16);
const textContainerRef = useRef<HTMLDivElement | null>(null); const textContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!showStartingPlayer) {
const playingTimer = setTimeout(() => {
localStorage.setItem('playing', 'true');
setShowStartingPlayer(localStorage.getItem('playing') === 'true');
}, 3_000);
return () => clearTimeout(playingTimer);
}
}, [showStartingPlayer]);
useEffect(() => { useEffect(() => {
if (!textContainerRef.current) { if (!textContainerRef.current) {
return; return;

View File

@@ -1,16 +1,65 @@
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable'; import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { baseColors } from '../../../tailwind.config';
import { useAnalytics } from '../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { Cog } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { checkContrast } from '../../Utils/checkContrast';
import {
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton'; import { LoseGameButton } from '../Buttons/LoseButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar'; import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar'; import ExtraCountersBar from '../Counters/ExtraCountersBar';
import PlayerMenu from '../Player/PlayerMenu'; import { Paragraph } from '../Misc/TextComponents';
import PlayerMenu from '../Players/PlayerMenu';
import Health from './Health'; import Health from './Health';
const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none',
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped
? `right-auto top-[1vmax] left-[27%]`
: 'top-1/4 right-[1vmax]',
]);
type SettingsButtonProps = {
onClick: () => void;
rotation: Rotation;
color: string;
};
const SettingsButton = ({ onClick, rotation, color }: SettingsButtonProps) => {
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [color]);
return (
<SettingsButtonTwc
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<Cog
size="5vmin"
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
</SettingsButtonTwc>
);
};
const LifeCounterContentWrapper = twc.div` const LifeCounterContentWrapper = twc.div`
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`; relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
@@ -21,8 +70,6 @@ const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [
: `flex-col`, : `flex-col`,
]); ]);
const StartingPlayerNoticeWrapper = twc.div`z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-primary-main`;
const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [ const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [
'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper opacity-75', 'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper opacity-75',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
@@ -62,13 +109,19 @@ const playerCanLose = (player: Player) => {
type LifeCounterProps = { type LifeCounterProps = {
player: Player; player: Player;
opponents: Player[]; opponents: Player[];
isStartingPlayer?: boolean;
}; };
const RECENT_DIFFERENCE_TTL = 3_000; const RECENT_DIFFERENCE_TTL = 3_000;
const LifeCounter = ({ player, opponents }: LifeCounterProps) => { const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
const { updatePlayer, updateLifeTotal } = usePlayers(); const { updatePlayer, updateLifeTotal } = usePlayers();
const { settings } = useGlobalSettings(); const { settings, playing, setPlaying, stopPlayerRandomization } =
useGlobalSettings();
const playingTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const recentDifferenceTimerRef = useRef<NodeJS.Timeout | undefined>(
undefined
);
const [showPlayerMenu, setShowPlayerMenu] = useState(false); const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0); const [recentDifference, setRecentDifference] = useState(0);
@@ -85,12 +138,10 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
trackMouse: true, trackMouse: true,
onSwipedDown: (e) => { onSwipedDown: (e) => {
e.event.stopPropagation(); e.event.stopPropagation();
console.log(`User DOWN Swiped on player ${player.index}`);
setShowPlayerMenu(true); setShowPlayerMenu(true);
}, },
onSwipedUp: (e) => { onSwipedUp: (e) => {
e.event.stopPropagation(); e.event.stopPropagation();
console.log(`User UP Swiped on player ${player.index}`);
setShowPlayerMenu(false); setShowPlayerMenu(false);
}, },
@@ -98,41 +149,63 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
onSwiping: (e) => e.event.stopPropagation(), onSwiping: (e) => e.event.stopPropagation(),
rotationAngle, rotationAngle,
}); });
const analytics = useAnalytics();
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { if (recentDifference === 0) {
clearTimeout(recentDifferenceTimerRef.current);
return;
}
recentDifferenceTimerRef.current = setTimeout(() => {
analytics.trackEvent('life_changed', {
lifeChangedAmount: recentDifference,
});
setRecentDifference(0); setRecentDifference(0);
}, RECENT_DIFFERENCE_TTL); }, RECENT_DIFFERENCE_TTL);
return () => {
clearTimeout(recentDifferenceTimerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference]);
useEffect(() => {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (document.body.clientWidth > document.body.clientHeight) if (document.body.clientWidth > document.body.clientHeight)
setIsLandscape(true); setIsLandscape(true);
else setIsLandscape(false); else setIsLandscape(false);
return; return () => {
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
}); });
resizeObserver.observe(document.body); resizeObserver.observe(document.body);
return () => {
clearTimeout(timer);
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference, document.body.clientHeight, document.body.clientWidth]); }, [document.body.clientHeight, document.body.clientWidth]);
useEffect(() => { useEffect(() => {
if (player.showStartingPlayer) { if (
const playingTimer = setTimeout(() => { player.isStartingPlayer &&
localStorage.setItem('playing', 'true'); ((!playing &&
player.showStartingPlayer = false; settings.useRandomStartingPlayerInterval &&
updatePlayer(player); stopPlayerRandomization) ||
}, 3_000); (!settings.useRandomStartingPlayerInterval && !playing))
) {
return () => clearTimeout(playingTimer); playingTimerRef.current = setTimeout(() => {
setPlaying(true);
}, 10_000);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [player.showStartingPlayer]); return () => clearTimeout(playingTimerRef.current);
}, [
player.isStartingPlayer,
playing,
setPlaying,
settings.useRandomStartingPlayerInterval,
stopPlayerRandomization,
]);
player.settings.rotation === Rotation.SideFlipped || player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side; player.settings.rotation === Rotation.Side;
@@ -160,6 +233,8 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
? player.settings.rotation - 180 ? player.settings.rotation - 180
: player.settings.rotation; : player.settings.rotation;
const amountOfPlayers = opponents.length + 1;
return ( return (
<LifeCounterContentWrapper style={{ background: player.color }}> <LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper <LifeCounterWrapper
@@ -167,31 +242,72 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
style={{ rotate: `${calcRotation}deg` }} style={{ rotate: `${calcRotation}deg` }}
{...handlers} {...handlers}
> >
{settings.showStartingPlayer && {amountOfPlayers > 1 &&
player.isStartingPlayer && !playing &&
player.showStartingPlayer && ( settings.showStartingPlayer &&
<StartingPlayerNoticeWrapper player.isStartingPlayer && (
style={{ rotate: `${calcRotation}deg` }} <div
className="z-20 flex absolute w-full h-full justify-center items-center select-none cursor-pointer webkit-user-select-none"
style={{
rotate: `${calcRotation}deg`,
backgroundImage:
stopPlayerRandomization ||
!settings.useRandomStartingPlayerInterval
? `radial-gradient(circle at center, ${player.color}, ${baseColors.primary.main})`
: 'none',
}}
onClick={() => {
clearTimeout(playingTimerRef.current);
setPlaying(true);
}}
> >
<DynamicText <DynamicText
style={{ style={{
rotate: `${calcTextRotation}deg`, rotate: `${calcTextRotation}deg`,
}} }}
> >
You start! <div className="flex flex-col justify-center items-center">
<Paragraph>👑</Paragraph>
{(stopPlayerRandomization ||
!settings.useRandomStartingPlayerInterval) && (
<>
<Paragraph>You start!</Paragraph>
<Paragraph className="text-xl">(Press to hide)</Paragraph>
</>
)}
</div>
</DynamicText> </DynamicText>
</StartingPlayerNoticeWrapper> </div>
)} )}
{player.hasLost && ( {player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} /> <PlayerLostWrapper $rotation={player.settings.rotation} />
)} )}
{amountOfPlayers > 1 &&
settings.showStartingPlayer &&
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing && (
<div
className="flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none z-10"
style={{ backgroundColor: player.color }}
/>
)}
<CommanderDamageBar <CommanderDamageBar
opponents={opponents} opponents={opponents}
player={player} player={player}
key={player.index} key={player.index}
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
{settings.showPlayerMenuCog && (
<SettingsButton
onClick={() => {
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}
color={player.color}
/>
)}
{playerCanLose(player) && ( {playerCanLose(player) && (
<LoseGameButton <LoseGameButton
rotation={player.settings.rotation} rotation={player.settings.rotation}

View File

@@ -2,6 +2,7 @@ import { Modal } from '@mui/material';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { Cross } from '../../Icons/generated';
export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`; export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`;
@@ -18,12 +19,12 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
style={{ display: 'flex', justifyContent: 'center' }} style={{ display: 'flex', justifyContent: 'center' }}
> >
<> <>
<div className="flex relative w-full max-w-[548px]"> <div className="flex justify-center items-center relative w-full max-w-[532px]">
<button <button
onClick={closeModal} onClick={closeModal}
className="flex absolute top-10 right-0 z-10 w-10 h-10 text-common-white bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2" className="flex absolute top-12 right-0 z-10 w-10 h-10 bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
> >
X <Cross size="16px" className="text-text-primary " />
</button> </button>
</div> </div>
<ModalWrapper> <ModalWrapper>
@@ -60,25 +61,34 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
</li> </li>
</ul> </ul>
<h3 className="text-lg font-bold mb-2">Other</h3> <h3 className="text-lg font-bold mb-2">Other functionality</h3>
<Paragraph className="mb-4"> <ul className="list-disc ml-6">
When a player is <strong>at or below 0 life</strong>, has taken{' '} <li>
<strong>21 or more Commander Damage</strong> or has{' '} <Paragraph className="mb-1">
<strong>10 or more poison counters</strong>, a button with a skull When a player is <strong>at or below 0 life</strong>, has
will appear on that player's card. Tapping it will dim the taken <strong>21 or more Commander Damage</strong> or has{' '}
player's card. <strong>10 or more poison counters</strong>, a button with a
</Paragraph> skull will appear on that player's card. Tapping it will dim
the player's card.
</Paragraph>
</li>
<li>
<Paragraph className="mb-4">
Swiping <strong>down</strong> on a player's card will show
that player's settings menu.
</Paragraph>
</li>
</ul>
</div> </div>
<div className="text-center mt-4"> <div className="text-center mt-4">
Visit my Visit my{' '}
<a <a
href="https://github.com/Vikeo/LifeTrinket" href="https://github.com/Vikeo/LifeTrinket"
target="_blank" target="_blank"
className="text-text-secondary underline" className="text-text-secondary underline"
> >
{' '} GitHub
GitHub{' '} </a>{' '}
</a>
for more info about this web app. for more info about this web app.
</div> </div>
</ModalWrapper> </ModalWrapper>

View File

@@ -5,10 +5,11 @@ import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cross } from '../../Icons/generated';
const SettingContainer = twc.div`w-full flex flex-col`; const SettingContainer = twc.div`w-full flex flex-col mb-2`;
const ToggleContainer = twc.div`flex flex-row justify-between items-center`; const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`;
const Container = twc.div`flex flex-col items-center w-full`; const Container = twc.div`flex flex-col items-center w-full`;
@@ -66,14 +67,18 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal
open={isOpen}
onClose={closeModal}
className="w-full flex justify-center"
>
<> <>
<div className="flex relative w-full max-w-[548px]"> <div className="flex justify-center items-center relative w-full max-w-[532px]">
<button <button
onClick={closeModal} onClick={closeModal}
className="flex absolute top-10 right-0 z-10 w-10 h-10 text-common-white bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2" className="flex absolute top-12 right-0 z-10 w-10 h-10 bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
> >
X <Cross size="16px" className="text-text-primary " />
</button> </button>
</div> </div>
<ModalWrapper> <ModalWrapper>
@@ -98,6 +103,44 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
start first if this is enabled. start first if this is enabled.
</Description> </Description>
</SettingContainer> </SettingContainer>
<SettingContainer>
<ToggleContainer>
<FormLabel>Show Player Menu Cog</FormLabel>
<Switch
checked={settings.showPlayerMenuCog}
onChange={() => {
setSettings({
...settings,
showPlayerMenuCog: !settings.showPlayerMenuCog,
});
}}
/>
</ToggleContainer>
<Description>
A cog on the top right of each player's card will be shown if
this is enabled.
</Description>
</SettingContainer>
<SettingContainer>
<ToggleContainer>
<FormLabel>Randomize starting player with interval</FormLabel>
<Switch
checked={settings.useRandomStartingPlayerInterval}
onChange={() => {
setSettings({
...settings,
useRandomStartingPlayerInterval:
!settings.useRandomStartingPlayerInterval,
});
}}
/>
</ToggleContainer>
<Description>
Will randomize between all players at when starting a game,
pressing the screen aborts the interval and chooses the player
that has the crown.
</Description>
</SettingContainer>
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Keep Awake</FormLabel> <FormLabel>Keep Awake</FormLabel>

View File

@@ -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 (
<PlayerWrapper>
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => {
const gridArea = getGridArea(player);
return (
<div
key={player.index}
className={`flex justify-center items-center align-middle ${gridArea}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</div>
);
})}
</div>
</PlayerWrapper>
);
};

View File

@@ -1,4 +1,4 @@
import { Button, Checkbox } from '@mui/material'; import { Checkbox } from '@mui/material';
import { useRef } from 'react'; import { useRef } from 'react';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme'; import { theme } from '../../Data/theme';
@@ -6,6 +6,7 @@ import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate'; import { useSafeRotate } from '../../Hooks/useSafeRotate';
import { import {
Cross,
Energy, Energy,
Exit, Exit,
Experience, Experience,
@@ -18,8 +19,6 @@ import {
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { RotationDivProps } from '../Buttons/CommanderDamage';
const CheckboxContainer = twc.div``;
const PlayerMenuWrapper = twc.div` const PlayerMenuWrapper = twc.div`
flex flex
flex-col flex-col
@@ -50,7 +49,6 @@ const TogglesSection = twc.div`
flex-row flex-row
flex-wrap flex-wrap
relative relative
gap-2
h-full h-full
justify-evenly justify-evenly
items-center items-center
@@ -59,11 +57,11 @@ const TogglesSection = twc.div`
const ButtonsSections = twc.div` const ButtonsSections = twc.div`
flex flex
max-w-full max-w-full
gap-4 justify-evenly
justify-between
p-[3%]
items-center items-center
flex-wrap flex-wrap
mt-0
px-2
`; `;
const ColorPickerButton = twc.div` const ColorPickerButton = twc.div`
@@ -78,7 +76,7 @@ const ColorPickerButton = twc.div`
`; `;
const SettingsContainer = twc.div<RotationDivProps>((props) => [ const SettingsContainer = twc.div<RotationDivProps>((props) => [
'flex flex-wrap h-full w-full', 'flex flex-wrap h-full w-full overflow-y-scroll',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col' ? 'flex-col'
: 'flex-row', : 'flex-row',
@@ -97,13 +95,22 @@ const PlayerMenu = ({
}: PlayerMenuProps) => { }: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null); const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null); const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
const endGameDialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({ const { isSide } = useSafeRotate({
rotation: player.settings.rotation, rotation: player.settings.rotation,
containerRef: settingsContainerRef, containerRef: settingsContainerRef,
}); });
const { fullscreen, wakeLock, goToStart } = useGlobalSettings(); const {
fullscreen,
wakeLock,
goToStart,
settings,
setPlaying,
setStopPlayerRandomization,
} = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers(); const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -121,6 +128,13 @@ const PlayerMenu = ({
const handleResetGame = () => { const handleResetGame = () => {
resetCurrentGame(); resetCurrentGame();
setShowPlayerMenu(false); setShowPlayerMenu(false);
setPlaying(false);
setStopPlayerRandomization(false);
};
const handleGoToStart = () => {
goToStart();
setStopPlayerRandomization(false);
}; };
const toggleFullscreen = () => { const toggleFullscreen = () => {
@@ -158,6 +172,13 @@ const PlayerMenu = ({
}} }}
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<button
onClick={() => setShowPlayerMenu(false)}
className="flex absolute top-0 right-2 z-10 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2 p-[0.2rem]"
>
<Cross size={buttonFontSize} className="text-primary-main " />
</button>
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
<ColorPickerButton aria-label="Color picker"> <ColorPickerButton aria-label="Color picker">
@@ -169,7 +190,7 @@ const PlayerMenu = ({
/> />
</ColorPickerButton> </ColorPickerButton>
{player.settings.useCommanderDamage && ( {player.settings.useCommanderDamage && (
<CheckboxContainer> <div>
<Checkbox <Checkbox
name="usePartner" name="usePartner"
checked={player.settings.usePartner} checked={player.settings.usePartner}
@@ -194,9 +215,9 @@ const PlayerMenu = ({
aria-checked={player.settings.usePartner} aria-checked={player.settings.usePartner}
aria-label="Partner" aria-label="Partner"
/> />
</CheckboxContainer> </div>
)} )}
<CheckboxContainer> <div>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
checked={player.settings.usePoison} checked={player.settings.usePoison}
@@ -221,8 +242,8 @@ const PlayerMenu = ({
aria-checked={player.settings.usePoison} aria-checked={player.settings.usePoison}
aria-label="Poison" aria-label="Poison"
/> />
</CheckboxContainer> </div>
<CheckboxContainer> <div>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
checked={player.settings.useEnergy} checked={player.settings.useEnergy}
@@ -247,8 +268,8 @@ const PlayerMenu = ({
aria-checked={player.settings.useEnergy} aria-checked={player.settings.useEnergy}
aria-label="Energy" aria-label="Energy"
/> />
</CheckboxContainer> </div>
<CheckboxContainer> <div>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
checked={player.settings.useExperience} checked={player.settings.useExperience}
@@ -273,21 +294,22 @@ const PlayerMenu = ({
aria-checked={player.settings.useExperience} aria-checked={player.settings.useExperience}
aria-label="Experience" aria-label="Experience"
/> />
</CheckboxContainer> </div>
</TogglesSection> </TogglesSection>
<ButtonsSections className="mt-4"> <ButtonsSections>
<Button <button
variant="text" className="text-primary-main cursor-pointer webkit-user-select-none"
style={{ onClick={() => endGameDialogRef.current?.show()}
cursor: 'pointer',
userSelect: 'none',
}}
onClick={goToStart}
aria-label="Back to start" aria-label="Back to start"
> >
<Exit size={iconSize} style={{ rotate: '180deg' }} /> <Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button> </button>
<CheckboxContainer> <div
data-fullscreen={document.fullscreenElement ? true : false}
className="flex
data-[fullscreen=true]:bg-secondary-dark rounded-lg border border-transparent
data-[fullscreen=true]:border-primary-main"
>
<Checkbox <Checkbox
name="fullscreen" name="fullscreen"
checked={document.fullscreenElement ? true : false} checked={document.fullscreenElement ? true : false}
@@ -302,65 +324,113 @@ const PlayerMenu = ({
role="checkbox" role="checkbox"
aria-checked={document.fullscreenElement ? true : false} aria-checked={document.fullscreenElement ? true : false}
aria-label="Fullscreen" aria-label="Fullscreen"
style={{ padding: '4px' }}
/> />
</CheckboxContainer> </div>
<Button <button
variant={wakeLock.active ? 'contained' : 'outlined'} data-wake-lock-active={settings.keepAwake}
style={{ style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize, fontSize: buttonFontSize,
padding: '0 4px 0 4px',
}} }}
onClick={wakeLock.toggleWakeLock} className="text-primary-main px-1 webkit-user-select-none cursor-pointer
data-[wake-lock-active=true]:bg-secondary-dark rounded-lg border border-transparent
data-[wake-lock-active=true]:border-primary-main
"
onClick={() => {
wakeLock.toggleWakeLock();
}}
role="checkbox" role="checkbox"
aria-checked={wakeLock.active} aria-checked={settings.keepAwake}
aria-label="Keep awake" aria-label="Keep awake"
> >
Keep Awake Keep Awake
</Button> </button>
<Button <button
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
userSelect: 'none', userSelect: 'none',
fontSize: buttonFontSize, fontSize: buttonFontSize,
padding: '4px', padding: '2px',
}} }}
className="text-primary-main"
onClick={() => resetGameDialogRef.current?.show()} onClick={() => resetGameDialogRef.current?.show()}
role="checkbox" role="checkbox"
aria-checked={wakeLock.active}
aria-label="Reset Game" aria-label="Reset Game"
> >
<ResetGame size={iconSize} /> <ResetGame size={iconSize} />
</Button> </button>
</ButtonsSections> </ButtonsSections>
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={resetGameDialogRef} ref={resetGameDialogRef}
className="z-[999] size-full bg-background-settings" className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => resetGameDialogRef.current?.close()} onClick={() => resetGameDialogRef.current?.close()}
> >
<div className="flex size-full items-center justify-center"> <div className="flex size-full items-center justify-center">
<div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-2xl border-none"> <div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-xl border-none">
<h1 className="text-center text-text-primary">Reset Game?</h1> <h1
<div className="flex justify-evenly gap-4"> className="text-center text-text-primary"
<Button style={{ fontSize: extraCountersSize }}
variant="contained" >
Reset Game?
</h1>
<div className="flex justify-evenly gap-2">
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
style={{ fontSize: iconSize }}
onClick={() => resetGameDialogRef.current?.close()} onClick={() => resetGameDialogRef.current?.close()}
> >
No No
</Button> </button>
<Button <button
variant="contained" className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => { onClick={() => {
handleResetGame(); handleResetGame();
resetGameDialogRef.current?.close(); resetGameDialogRef.current?.close();
}} }}
style={{ fontSize: iconSize }}
> >
Yes Yes
</Button> </button>
</div>
</div>
</div>
</dialog>
<dialog
ref={endGameDialogRef}
className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => endGameDialogRef.current?.close()}
>
<div className="flex size-full items-center justify-center">
<div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-xl border-none">
<h1
className="text-center text-text-primary"
style={{ fontSize: extraCountersSize }}
>
End Game?
</h1>
<div className="flex justify-evenly gap-2">
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
style={{ fontSize: iconSize }}
onClick={() => endGameDialogRef.current?.close()}
>
No
</button>
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => {
handleGoToStart();
endGameDialogRef.current?.close();
}}
style={{ fontSize: iconSize }}
>
Yes
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,158 @@
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<NodeJS.Timeout | null>(null);
const prevRandomIndexRef = useRef<number>(-1);
const {
settings,
stopPlayerRandomization,
setStopPlayerRandomization,
playing,
} = useGlobalSettings();
const { setPlayers } = usePlayers();
useEffect(() => {
if (
players.length > 1 &&
settings.showStartingPlayer &&
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,
]);
const gradientColors = players.map((player) => player.color).join(', ');
return (
<PlayersWrapper>
{players.length > 1 &&
settings.showStartingPlayer &&
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing && (
<div
className="absolute flex justify-center items-center h-screen w-screen portrait:h-[100vw] portrait:w-[100vh] z-50 cursor-pointer text-5xl"
onClick={() => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
randomIntervalRef.current = null;
}
setStopPlayerRandomization(true);
}}
>
<div className="absolute flex top-[30%] justify-center items-center px-8 py-4">
<div
className="absolute size-full blur-[3px] rounded-2xl opacity-90 saturate-150"
style={{
backgroundImage: `linear-gradient(60deg, ${gradientColors})`,
}}
/>
<p className="relative z-10 text-[5vmax]">
PRESS TO SELECT PLAYER
</p>
</div>
</div>
)}
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => {
const gridArea = getGridArea(player);
return (
<div
key={player.index}
className={`flex justify-center items-center align-middle ${gridArea}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</div>
);
})}
</div>
</PlayersWrapper>
);
};

View File

@@ -1,7 +1,7 @@
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { Orientation } from '../../Types/Settings'; import { Orientation } from '../../Types/Settings';
import { Player } from '../Player/Player'; import { Players } from '../Players/Players';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`; const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`;
@@ -14,52 +14,52 @@ export const Play = () => {
switch (players.length) { switch (players.length) {
case 1: case 1:
if (initialGameSettings?.orientation === Orientation.Portrait) { 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; break;
case 2: case 2:
switch (initialGameSettings?.orientation) { switch (initialGameSettings?.orientation) {
case Orientation.Portrait: case Orientation.Portrait:
Layout = Player(players, 'grid-areas-twoPlayersOppositePortrait'); Layout = Players(players, 'grid-areas-twoPlayersOppositePortrait');
break; break;
default: default:
case Orientation.Landscape: case Orientation.Landscape:
Layout = Player(players, 'grid-areas-twoPlayersSameSideLandscape'); Layout = Players(players, 'grid-areas-twoPlayersSameSideLandscape');
break; break;
case Orientation.OppositeLandscape: case Orientation.OppositeLandscape:
Layout = Player(players, 'grid-areas-twoPlayersOppositeLandscape'); Layout = Players(players, 'grid-areas-twoPlayersOppositeLandscape');
break; break;
} }
break; break;
case 3: case 3:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-threePlayersSide'); Layout = Players(players, 'grid-areas-threePlayersSide');
break; break;
} }
Layout = Player(players, 'grid-areas-threePlayers'); Layout = Players(players, 'grid-areas-threePlayers');
break; break;
default: default:
case 4: case 4:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-fourPlayerPortrait'); Layout = Players(players, 'grid-areas-fourPlayerPortrait');
break; break;
} }
Layout = Player(players, 'grid-areas-fourPlayer'); Layout = Players(players, 'grid-areas-fourPlayer');
break; break;
case 5: case 5:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-fivePlayersSide'); Layout = Players(players, 'grid-areas-fivePlayersSide');
break; break;
} }
Layout = Player(players, 'grid-areas-fivePlayers'); Layout = Players(players, 'grid-areas-fivePlayers');
break; break;
case 6: case 6:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-sixPlayersSide'); Layout = Players(players, 'grid-areas-sixPlayersSide');
break; break;
} }
Layout = Player(players, 'grid-areas-sixPlayers'); Layout = Players(players, 'grid-areas-sixPlayers');
break; break;
} }

View File

@@ -3,10 +3,12 @@ import React from 'react';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { import {
FivePlayers, FivePlayers,
FivePlayersSide,
FourPlayers, FourPlayers,
FourPlayersSide, FourPlayersSide,
OnePlayerPortrait, OnePlayerPortrait,
SixPlayers, SixPlayers,
SixPlayersSide,
ThreePlayers, ThreePlayers,
ThreePlayersSide, ThreePlayersSide,
TwoPlayersOppositeLandscape, TwoPlayersOppositeLandscape,
@@ -40,7 +42,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
switch (numberOfPlayers) { switch (numberOfPlayers) {
case 1: case 1:
return ( return (
<div> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
@@ -89,7 +91,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
} }
label="" label=""
/> />
</div> </>
); );
case 2: case 2:
return ( return (
@@ -303,20 +305,21 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
} }
label="" label=""
/> />
{/* <FormControlLabel <FormControlLabel
value={GridTemplateAreas.FivePlayersSide} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayersSide <FivePlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.secondary.main} fill={theme.palette.secondary.main}
/> />
} }
checkedIcon={ checkedIcon={
<FivePlayersSide <FivePlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.primary.main} fill={theme.palette.primary.main}
/> />
@@ -325,7 +328,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
label="" label=""
/> */} />
</> </>
); );
@@ -356,20 +359,21 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
} }
label="" label=""
/> />
{/* <FormControlLabel <FormControlLabel
value={GridTemplateAreas.SixPlayersSide} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayersSide <SixPlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.secondary.main} fill={theme.palette.secondary.main}
/> />
} }
checkedIcon={ checkedIcon={
<SixPlayersSide <SixPlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.primary.main} fill={theme.palette.primary.main}
/> />
@@ -378,7 +382,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
label="" label=""
/> */} />
</> </>
); );

View File

@@ -20,7 +20,7 @@ import { LayoutOptions } from './LayoutOptions';
const MainWrapper = twc.div`w-[100dvw] h-fit pb-14 overflow-hidden items-center flex flex-col`; const MainWrapper = twc.div`w-[100dvw] h-fit pb-14 overflow-hidden items-center flex flex-col`;
const StartButtonFooter = twc.div`w-full max-w-[548px] fixed bottom-4 z-1 items-center flex flex-col px-4`; const StartButtonFooter = twc.div`w-full max-w-[548px] fixed bottom-4 z-1 items-center flex flex-col px-4 z-10`;
const SliderWrapper = twc.div`mx-8`; const SliderWrapper = twc.div`mx-8`;
@@ -262,18 +262,6 @@ const Start = () => {
</ToggleButtonsWrapper> </ToggleButtonsWrapper>
<FormLabel>Layout</FormLabel> <FormLabel>Layout</FormLabel>
{/* <LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
gridAreas={playerOptions.gridAreas}
onChange={(gridAreas) =>
setPlayerOptions({
...playerOptions,
gridAreas,
//TODO fix the layout selection
orientation: Orientation.Portrait,
})
}
/> */}
<LayoutOptions <LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers} numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation} selectedOrientation={playerOptions.orientation}

View File

@@ -22,6 +22,10 @@ export type GlobalSettingsContextType = {
setInitialGameSettings: (initialGameSettings: InitialGameSettings) => void; setInitialGameSettings: (initialGameSettings: InitialGameSettings) => void;
settings: Settings; settings: Settings;
setSettings: (settings: Settings) => void; setSettings: (settings: Settings) => void;
playing: boolean;
setPlaying: (playing: boolean) => void;
stopPlayerRandomization: boolean;
setStopPlayerRandomization: (stopRandom: boolean) => void;
isPWA: boolean; isPWA: boolean;
}; };

View File

@@ -7,6 +7,8 @@ export type PlayersContextType = {
updatePlayer: (updatedPlayer: Player) => void; updatePlayer: (updatedPlayer: Player) => void;
updateLifeTotal: (player: Player, updatedLifeTotal: number) => number; updateLifeTotal: (player: Player, updatedLifeTotal: number) => number;
resetCurrentGame: () => void; resetCurrentGame: () => void;
startingPlayerIndex: number;
setStartingPlayerIndex: (index: number) => void;
}; };
export const PlayersContext = createContext<PlayersContextType | null>(null); export const PlayersContext = createContext<PlayersContextType | null>(null);

View File

@@ -127,15 +127,15 @@ const getOrientationRotations = (
case Orientation.Portrait: case Orientation.Portrait:
switch (index) { switch (index) {
case 0: case 0:
return Rotation.Side; return Rotation.Flipped;
case 1: case 1:
return Rotation.Side; return Rotation.Flipped;
case 2: case 2:
return Rotation.SideFlipped; return Rotation.Side;
case 3: case 3:
return Rotation.SideFlipped; return Rotation.Normal;
case 4: case 4:
return Rotation.SideFlipped; return Rotation.Normal;
default: default:
return Rotation.Normal; return Rotation.Normal;
} }
@@ -163,17 +163,17 @@ const getOrientationRotations = (
case Orientation.Portrait: case Orientation.Portrait:
switch (index) { switch (index) {
case 0: case 0:
return Rotation.Side; return Rotation.SideFlipped;
case 1: case 1:
return Rotation.Side; return Rotation.Flipped;
case 2: case 2:
return Rotation.Side; return Rotation.Flipped;
case 3: case 3:
return Rotation.SideFlipped; return Rotation.Side;
case 4: case 4:
return Rotation.SideFlipped; return Rotation.Normal;
case 5: case 5:
return Rotation.SideFlipped; return Rotation.Normal;
default: default:
return Rotation.Normal; return Rotation.Normal;
} }
@@ -191,10 +191,8 @@ export const createInitialPlayers = ({
}: InitialGameSettings): Player[] => { }: InitialGameSettings): Player[] => {
const players: Player[] = []; const players: Player[] = [];
const availableColors = [...presetColors]; // Create a copy of the colors array 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++) { for (let i = 0; i <= numberOfPlayers - 1; i++) {
const isStartingPlayer = i === firstPlayerIndex;
const colorIndex = Math.floor(Math.random() * availableColors.length); const colorIndex = Math.floor(Math.random() * availableColors.length);
const color = availableColors[colorIndex]; const color = availableColors[colorIndex];
@@ -224,11 +222,10 @@ export const createInitialPlayers = ({
usePoison: false, usePoison: false,
rotation, rotation,
}, },
isStartingPlayer,
showStartingPlayer: isStartingPlayer,
extraCounters: [], extraCounters: [],
commanderDamage, commanderDamage,
hasLost: false, hasLost: false,
isStartingPlayer: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped, isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
}; };

View File

@@ -18,6 +18,11 @@ export const useAnalytics = () => {
eventName: string, eventName: string,
eventParams?: { [key: string]: unknown } eventParams?: { [key: string]: unknown }
) => { ) => {
if (process.env.NODE_ENV === 'development') {
console.info('Event not tracked:', { eventName, eventParams });
return;
}
logEvent(analytics, eventName, eventParams); logEvent(analytics, eventName, eventParams);
}; };

View File

@@ -23,7 +23,6 @@ export default function useOrientation(
const onChange = () => { const onChange = () => {
if (mounted) { if (mounted) {
const { orientation } = screen; const { orientation } = screen;
console.log(orientation);
if (orientation) { if (orientation) {
const { angle, type } = orientation; const { angle, type } = orientation;

View File

@@ -21,11 +21,23 @@ export const GlobalSettingsProvider = ({
const savedShowPlay = localStorage.getItem('showPlay'); const savedShowPlay = localStorage.getItem('showPlay');
const savedGameSettings = localStorage.getItem('initialGameSettings'); const savedGameSettings = localStorage.getItem('initialGameSettings');
const savedSettings = localStorage.getItem('settings'); const savedSettings = localStorage.getItem('settings');
const savedPlaying = localStorage.getItem('playing');
const [playing, setPlaying] = useState<boolean>(
savedPlaying ? savedPlaying === 'true' : false
);
const setPlayingAndLocalStorage = (playing: boolean) => {
setPlaying(playing);
localStorage.setItem('playing', String(playing));
};
const [showPlay, setShowPlay] = useState<boolean>( const [showPlay, setShowPlay] = useState<boolean>(
savedShowPlay ? savedShowPlay === 'true' : false savedShowPlay ? savedShowPlay === 'true' : false
); );
const [stopPlayerRandomization, setStopPlayerRandomization] =
useState<boolean>(false);
const [initialGameSettings, setInitialGameSettings] = const [initialGameSettings, setInitialGameSettings] =
useState<InitialGameSettings | null>( useState<InitialGameSettings | null>(
savedGameSettings ? JSON.parse(savedGameSettings) : null savedGameSettings ? JSON.parse(savedGameSettings) : null
@@ -34,7 +46,13 @@ export const GlobalSettingsProvider = ({
const [settings, setSettings] = useState<Settings>( const [settings, setSettings] = useState<Settings>(
savedSettings savedSettings
? JSON.parse(savedSettings) ? JSON.parse(savedSettings)
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true } : {
goFullscreenOnStart: true,
keepAwake: true,
showStartingPlayer: true,
showPlayerMenuCog: true,
useRandomStartingPlayerInterval: false,
}
); );
const removeLocalStorage = async () => { const removeLocalStorage = async () => {
@@ -42,6 +60,8 @@ export const GlobalSettingsProvider = ({
localStorage.removeItem('players'); localStorage.removeItem('players');
localStorage.removeItem('playing'); localStorage.removeItem('playing');
localStorage.removeItem('showPlay'); localStorage.removeItem('showPlay');
setPlaying(false);
setShowPlay(false); setShowPlay(false);
}; };
@@ -148,10 +168,14 @@ export const GlobalSettingsProvider = ({
goToStart, goToStart,
showPlay, showPlay,
setShowPlay, setShowPlay,
playing,
setPlaying: setPlayingAndLocalStorage,
initialGameSettings, initialGameSettings,
setInitialGameSettings, setInitialGameSettings,
settings, settings,
setSettings, setSettings,
stopPlayerRandomization,
setStopPlayerRandomization,
isPWA: window?.matchMedia('(display-mode: standalone)').matches, isPWA: window?.matchMedia('(display-mode: standalone)').matches,
}; };
}, [ }, [
@@ -160,10 +184,12 @@ export const GlobalSettingsProvider = ({
initialGameSettings, initialGameSettings,
isFullscreen, isFullscreen,
isSupported, isSupported,
playing,
release, release,
request, request,
settings, settings,
showPlay, showPlay,
stopPlayerRandomization,
type, type,
]); ]);

View File

@@ -7,6 +7,17 @@ import { InitialGameSettings } from '../Types/Settings';
export const PlayersProvider = ({ children }: { children: ReactNode }) => { export const PlayersProvider = ({ children }: { children: ReactNode }) => {
const savedPlayers = localStorage.getItem('players'); const savedPlayers = localStorage.getItem('players');
const savedStartingPlayerIndex = localStorage.getItem('startingPlayerIndex');
const [startingPlayerIndex, setStartingPlayerIndex] = useState<number>(
savedStartingPlayerIndex ? parseInt(savedStartingPlayerIndex) : -1
);
const setStartingPlayerIndexAndLocalStorage = (index: number) => {
setStartingPlayerIndex(index);
localStorage.setItem('startingPlayerIndex', String(index));
};
const [players, setPlayers] = useState<Player[]>( const [players, setPlayers] = useState<Player[]>(
savedPlayers ? JSON.parse(savedPlayers) : [] savedPlayers ? JSON.parse(savedPlayers) : []
); );
@@ -50,9 +61,7 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
return; return;
} }
const startingPlayerIndex = Math.floor( const newStartingPlayerIndex = Math.floor(Math.random() * players.length);
Math.random() * initialGameSettings.numberOfPlayers
);
players.forEach((player: Player) => { players.forEach((player: Player) => {
player.commanderDamage.map((damage) => { player.commanderDamage.map((damage) => {
@@ -65,16 +74,9 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
}); });
player.lifeTotal = initialGameSettings.startingLifeTotal; player.lifeTotal = initialGameSettings.startingLifeTotal;
player.hasLost = false; player.hasLost = false;
const isStartingPlayer = player.index === startingPlayerIndex; player.isStartingPlayer = newStartingPlayerIndex === player.index;
player.isStartingPlayer = isStartingPlayer;
if (player.isStartingPlayer) {
player.showStartingPlayer = true;
}
updatePlayer(player); updatePlayer(player);
}); });
@@ -87,8 +89,10 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
updatePlayer, updatePlayer,
updateLifeTotal, updateLifeTotal,
resetCurrentGame, resetCurrentGame,
startingPlayerIndex,
setStartingPlayerIndex: setStartingPlayerIndexAndLocalStorage,
}; };
}, [players]); }, [players, startingPlayerIndex]);
return ( return (
<PlayersContext.Provider value={ctxValue}> <PlayersContext.Provider value={ctxValue}>

View File

@@ -6,7 +6,6 @@ export type Player = {
commanderDamage: CommanderDamage[]; commanderDamage: CommanderDamage[];
extraCounters: ExtraCounter[]; extraCounters: ExtraCounter[];
isStartingPlayer: boolean; isStartingPlayer: boolean;
showStartingPlayer: boolean;
hasLost: boolean; hasLost: boolean;
isSide: boolean; isSide: boolean;
}; };

View File

@@ -15,7 +15,9 @@ export enum GameFormat {
export type Settings = { export type Settings = {
keepAwake: boolean; keepAwake: boolean;
showStartingPlayer: boolean; showStartingPlayer: boolean;
showPlayerMenuCog: boolean;
goFullscreenOnStart: boolean; goFullscreenOnStart: boolean;
useRandomStartingPlayerInterval: boolean;
}; };
export type InitialGameSettings = { export type InitialGameSettings = {

View File

@@ -2,6 +2,50 @@
import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas'; import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas';
import type { Config } from 'tailwindcss'; import type { Config } from 'tailwindcss';
export const baseColors = {
primary: {
main: '#3E7D78',
dark: '#2D5F5B',
},
secondary: {
main: '#284F4C',
dark: '#1B3B38',
},
background: {
default: '#08253B',
backdrop: 'rgba(0, 0, 0, 0.3)',
settings: 'rgba(20, 20, 0, 0.9)',
},
icons: {
dark: '#00000080',
light: '#ffffff4f',
},
text: {
primary: '#F5F5F5',
secondary: '#76A6A5',
},
action: {
disabled: '#234A47',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#000000',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
};
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
@@ -36,53 +80,11 @@ export default {
], ],
sixPlayers: ['player0 player1 player2', 'player3 player4 player5'], sixPlayers: ['player0 player1 player2', 'player3 player4 player5'],
sixPlayersSide: [ sixPlayersSide: [
'player0 player1 player1 player1 player1 player2 player2 player2 player2 player3', 'player0 player1 player1 player1 player1 player1 player1 player2 player2 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3', 'player0 player4 player4 player4 player4 player4 player4 player5 player5 player5 player5 player5 player5 player3',
], ],
}, },
colors: { colors: baseColors,
primary: {
main: '#3E7D78',
dark: '#2D5F5B',
},
secondary: {
main: '#284F4C',
dark: '#1B3B38',
},
background: {
default: '#08253B',
backdrop: 'rgba(0, 0, 0, 0.3)',
settings: 'rgba(20, 20, 0, 0.9)',
},
icons: {
dark: '#00000080',
light: '#ffffff4f',
},
text: {
primary: '#F5F5F5',
secondary: '#76A6A5',
},
action: {
disabled: '#234A47',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#000000',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
},
keyframes: { keyframes: {
fadeOut: { fadeOut: {
'0%': { '0%': {