Compare commits

...

31 Commits
0.6.4 ... 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
Viktor Rådberg
0f86928cb3 Merge pull request #32 from Vikeo/better-colors
Better colors
2024-03-16 10:42:13 +01:00
Viktor Rådberg
efbfb7719c tsc 2024-03-16 10:40:18 +01:00
Viktor Rådberg
71e5614f52 bump to new version 2024-03-16 10:38:23 +01:00
Viktor Rådberg
677fd79bee fix long press down 2024-03-16 10:23:15 +01:00
Viktor Rådberg
1bff41bc10 remove colorful 2024-03-16 10:04:35 +01:00
Viktor Rådberg
7852520f8e minus plus icon color 2024-03-16 09:59:40 +01:00
Viktor Rådberg
04c3d60967 use normal picker again 2024-03-16 09:31:59 +01:00
Viktor Rådberg
664e2e5688 round color picker 2024-02-19 07:38:17 +01:00
Viktor Rådberg
6eb7ac9f50 Merge branch 'main' into better-colors 2024-02-18 16:08:09 +01:00
Viktor Rådberg
8b33a2a38a wip 2024-01-28 17:04:30 +01:00
Viktor Rådberg
cc915dff36 better color picker 2024-01-28 11:54:37 +01:00
27 changed files with 859 additions and 346 deletions

View File

@@ -1,8 +1,12 @@
index.html,1705225256081,6ef0d7e2de82bf64addbb9294fb28845fd06daaa544b010a47422c12ae3ad97f
robots.txt,1705225255906,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.json,1705225255906,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
assets/index-08359bdb.css,1705225256081,d2766260d28230d960d75362810713efaddf40687205e697432b52869f162af7
logo192.png,1705225255905,3b0fcf91fe2128f493de0bce2f6e2d35520a4260a04e05b8d855181359b3d3fe
favicon.ico,1705225255905,75661e6187b524767554b4f28ec09a64bc72b0bb102a0b453aaead88519d9ed3
logo512.png,1705225255906,cf49739c9e6890bbfcd4157f299dde425df60759b7320ae9188d7ab9dc51e8ca
assets/index-20658f4b.js,1705225256081,742f2c10740beea3a23f269aa6266b3c288d1fd9c7e20b6829034e8a898bf1e1
index.html,1711189442688,fa2549e32940c356ac5cee88c8db61076ad62fb4e599858c8e45cfc68cd901c4
manifest.json,1711189442512,7ff5111aa04a42adff3b38924ec467b6f345ed0309dba1486dc9b783b60c2a9d
registerSW.js,1711189442688,5b6445b5215737c53ef0d379c151d57de165a056de2d1c5812ed614f158ebcbd
sw.js,1711189443521,9c09d33ea573bb818864bfad526fa911839637171773eca8e31905458679846d
robots.txt,1711189442512,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.webmanifest,1711189442688,f2bf253209f6e292a6b0dbfa06fb4ac188eb5f2dba568c3ad5511b9ed52c1f51
workbox-3e911b1d.js,1711189443521,d5dbc868a5c07af633d29de7ba3ffe37542aaaabdf33713b4298df31f92f11ff
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",
"private": true,
"version": "0.6.4",
"version": "0.8.1",
"type": "commonjs",
"engines": {
"node": ">=18",

View File

@@ -66,8 +66,7 @@ export const CommanderDamage = ({
}: CommanderDamageButtonComponentProps) => {
const { updatePlayer } = usePlayers();
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false);
const [downLongPressed, setDownLongPressed] = useState(false);
const downPositionRef = useRef({ x: 0, y: 0 });
const handleCommanderDamageChange = (
@@ -109,16 +108,16 @@ export const CommanderDamage = ({
const handleDownInput = ({ opponentIndex, isPartner, event }: InputProps) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
setTimeoutFinished(false);
setHasPressedDown(true);
setDownLongPressed(false);
timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true);
setDownLongPressed(true);
handleCommanderDamageChange(opponentIndex, -1, isPartner);
}, decrementTimeoutMs);
};
const handleUpInput = ({ opponentIndex, isPartner, event }: InputProps) => {
if (!(hasPressedDown && !timeoutFinished)) {
if (downLongPressed) {
return;
}
@@ -134,14 +133,14 @@ export const CommanderDamage = ({
return;
}
clearTimeout(timeoutRef.current);
handleCommanderDamageChange(opponentIndex, 1, isPartner);
setHasPressedDown(false);
};
const handleLeaveInput = () => {
setTimeoutFinished(true);
setDownLongPressed(true);
clearTimeout(timeoutRef.current);
setHasPressedDown(false);
};
const opponentIndex = opponent.index;

View File

@@ -1,8 +1,9 @@
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { TwcComponentProps, twc } from 'react-twc';
import { lifeLongPressMultiplier } from '../../Data/constants';
import { Rotation } from '../../Types/Player';
import { Player, Rotation } from '../../Types/Player';
import { MAX_TAP_MOVE_DISTANCE } from './CommanderDamage';
import { checkContrast } from '../../Utils/checkContrast';
type RotationButtonProps = TwcComponentProps<'div'> & {
$align?: string;
@@ -13,7 +14,6 @@ const LifeCounterButtonTwc = twc.button`
h-full
w-full
flex
text-lifeCounter-text
font-semibold
bg-transparent
border-none
@@ -40,17 +40,15 @@ const TextContainer = twc.div<RotationButtonProps>((props) => [
]);
type LifeCounterButtonProps = {
lifeTotal: number;
player: Player;
setLifeTotal: (lifeTotal: number) => void;
rotation: number;
operation: 'add' | 'subtract';
increment: number;
};
const LifeCounterButton = ({
lifeTotal,
player,
setLifeTotal,
rotation,
operation,
increment,
}: LifeCounterButtonProps) => {
@@ -59,8 +57,20 @@ const LifeCounterButton = ({
const [hasPressedDown, setHasPressedDown] = useState(false);
const downPositionRef = useRef({ x: 0, y: 0 });
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(player.color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [player.color]);
const handleLifeChange = (increment: number) => {
setLifeTotal(lifeTotal + increment);
setLifeTotal(player.lifeTotal + increment);
};
const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => {
@@ -102,7 +112,8 @@ const LifeCounterButton = ({
};
const fontSize =
rotation === Rotation.SideFlipped || rotation === Rotation.Side
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? '8vmax'
: '12vmin';
@@ -118,8 +129,11 @@ const LifeCounterButton = ({
aria-label={`${operation === 'add' ? 'Add' : 'Subtract'} life`}
>
<TextContainer
$rotation={rotation}
$rotation={player.settings.rotation}
$align={operation === 'add' ? 'right' : 'left'}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark
data-[contrast=light]:text-icons-light"
>
{operation === 'add' ? '\u002B' : '\u2212'}
</TextContainer>

View File

@@ -7,7 +7,7 @@ const LoseButton = twc.div<RotationDivProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-interface-loseButton-background rounded-lg select-none z-[1] webkit-user-select-none py-2 px-4 ',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `left-[19%]`
? `left-[21%]`
: 'top-[21%]',
]);

View File

@@ -10,6 +10,8 @@ import {
import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter';
import { useEffect, useState } from 'react';
import { checkContrast } from '../../Utils/checkContrast';
const Container = twc.div<RotationDivProps>((props) => [
'flex',
@@ -19,9 +21,9 @@ const Container = 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
? 'flex-col-reverse h-full w-auto bottom-auto'
? 'flex-col-reverse h-full w-auto bottom-auto right-0'
: 'w-full bottom-0',
]);
@@ -31,6 +33,17 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers();
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(player.color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [player.color]);
const handleCounterChange = (
updatedCounterTotal: number,
@@ -93,7 +106,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useCommanderDamage && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={<CommanderTax size={iconSize} opacity="0.5" color="black" />}
Icon={
<CommanderTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.CommanderTax}
counterTotal={
player.extraCounters?.find(
@@ -108,7 +127,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{Boolean(useCommanderDamage && usePartner) && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={<PartnerTax size={iconSize} opacity="0.5" color="black" />}
Icon={
<PartnerTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.PartnerTax}
counterTotal={
player.extraCounters?.find(
@@ -123,7 +148,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{usePoison && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={<Poison size={iconSize} opacity="0.5" color="black" />}
Icon={
<Poison
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Poison}
counterTotal={
player.extraCounters?.find((counter) => counter.type === 'poison')
@@ -137,7 +168,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useEnergy && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={<Energy size={iconSize} opacity="0.5" color="black" />}
Icon={
<Energy
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Energy}
counterTotal={
player.extraCounters?.find((counter) => counter.type === 'energy')
@@ -151,7 +188,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useExperience && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={<Experience size={iconSize} opacity="0.5" color="black" />}
Icon={
<Experience
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Experience}
counterTotal={
player.extraCounters?.find(

View File

@@ -53,23 +53,9 @@ const Health = ({
differenceKey,
recentDifference,
}: HealthProps) => {
const [showStartingPlayer, setShowStartingPlayer] = useState(
localStorage.getItem('playing') === 'true'
);
const [fontSize, setFontSize] = useState(16);
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(() => {
if (!textContainerRef.current) {
return;
@@ -118,9 +104,8 @@ const Health = ({
return (
<LifeContainer $rotation={player.settings.rotation}>
<LifeCounterButton
lifeTotal={player.lifeTotal}
player={player}
setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="subtract"
increment={-1}
/>
@@ -148,9 +133,8 @@ const Health = ({
</LifeCounterTextContainer>
</TextWrapper>
<LifeCounterButton
lifeTotal={player.lifeTotal}
player={player}
setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="add"
increment={1}
/>

View File

@@ -1,16 +1,65 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc';
import { baseColors } from '../../../tailwind.config';
import { useAnalytics } from '../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Cog } from '../../Icons/generated';
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 CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar';
import PlayerMenu from '../Player/PlayerMenu';
import { Paragraph } from '../Misc/TextComponents';
import PlayerMenu from '../Players/PlayerMenu';
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`
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`,
]);
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) => [
'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
@@ -62,13 +109,19 @@ 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<NodeJS.Timeout | undefined>(undefined);
const recentDifferenceTimerRef = useRef<NodeJS.Timeout | undefined>(
undefined
);
const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0);
@@ -85,12 +138,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);
},
@@ -98,41 +149,63 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
onSwiping: (e) => e.event.stopPropagation(),
rotationAngle,
});
const analytics = useAnalytics();
useEffect(() => {
const timer = setTimeout(() => {
if (recentDifference === 0) {
clearTimeout(recentDifferenceTimerRef.current);
return;
}
recentDifferenceTimerRef.current = setTimeout(() => {
analytics.trackEvent('life_changed', {
lifeChangedAmount: recentDifference,
});
setRecentDifference(0);
}, RECENT_DIFFERENCE_TTL);
return () => {
clearTimeout(recentDifferenceTimerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference]);
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
if (document.body.clientWidth > document.body.clientHeight)
setIsLandscape(true);
else setIsLandscape(false);
return;
return () => {
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
});
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
}, [recentDifference, document.body.clientHeight, document.body.clientWidth]);
}, [document.body.clientHeight, document.body.clientWidth]);
useEffect(() => {
if (player.showStartingPlayer) {
const playingTimer = setTimeout(() => {
localStorage.setItem('playing', 'true');
player.showStartingPlayer = false;
updatePlayer(player);
}, 3_000);
return () => clearTimeout(playingTimer);
if (
player.isStartingPlayer &&
((!playing &&
settings.useRandomStartingPlayerInterval &&
stopPlayerRandomization) ||
(!settings.useRandomStartingPlayerInterval && !playing))
) {
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.Side;
@@ -160,6 +233,8 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
? player.settings.rotation - 180
: player.settings.rotation;
const amountOfPlayers = opponents.length + 1;
return (
<LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper
@@ -167,31 +242,72 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
style={{ rotate: `${calcRotation}deg` }}
{...handlers}
>
{settings.showStartingPlayer &&
player.isStartingPlayer &&
player.showStartingPlayer && (
<StartingPlayerNoticeWrapper
style={{ rotate: `${calcRotation}deg` }}
{amountOfPlayers > 1 &&
!playing &&
settings.showStartingPlayer &&
player.isStartingPlayer && (
<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
style={{
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>
</StartingPlayerNoticeWrapper>
</div>
)}
{player.hasLost && (
<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
opponents={opponents}
player={player}
key={player.index}
handleLifeChange={handleLifeChange}
/>
{settings.showPlayerMenuCog && (
<SettingsButton
onClick={() => {
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}
color={player.color}
/>
)}
{playerCanLose(player) && (
<LoseGameButton
rotation={player.settings.rotation}

View File

@@ -2,6 +2,7 @@ import { Modal } from '@mui/material';
import { twc } from 'react-twc';
import { Separator } from './Separator';
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]`;
@@ -18,12 +19,12 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
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
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>
</div>
<ModalWrapper>
@@ -60,25 +61,34 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
</li>
</ul>
<h3 className="text-lg font-bold mb-2">Other</h3>
<Paragraph className="mb-4">
When a player is <strong>at or below 0 life</strong>, has taken{' '}
<strong>21 or more Commander Damage</strong> or has{' '}
<strong>10 or more poison counters</strong>, a button with a skull
will appear on that player's card. Tapping it will dim the
player's card.
</Paragraph>
<h3 className="text-lg font-bold mb-2">Other functionality</h3>
<ul className="list-disc ml-6">
<li>
<Paragraph className="mb-1">
When a player is <strong>at or below 0 life</strong>, has
taken <strong>21 or more Commander Damage</strong> or has{' '}
<strong>10 or more poison counters</strong>, a button with a
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 className="text-center mt-4">
Visit my
Visit my{' '}
<a
href="https://github.com/Vikeo/LifeTrinket"
target="_blank"
className="text-text-secondary underline"
>
{' '}
GitHub{' '}
</a>
GitHub
</a>{' '}
for more info about this web app.
</div>
</ModalWrapper>

View File

@@ -5,10 +5,11 @@ import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
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`;
@@ -66,14 +67,18 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}, [isOpen]);
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
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>
</div>
<ModalWrapper>
@@ -98,6 +103,44 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
start first if this is enabled.
</Description>
</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>
<ToggleContainer>
<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 { twc } from 'react-twc';
import { theme } from '../../Data/theme';
@@ -6,6 +6,7 @@ import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import {
Cross,
Energy,
Exit,
Experience,
@@ -18,8 +19,6 @@ import {
import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
const CheckboxContainer = twc.div``;
const PlayerMenuWrapper = twc.div`
flex
flex-col
@@ -50,7 +49,6 @@ const TogglesSection = twc.div`
flex-row
flex-wrap
relative
gap-2
h-full
justify-evenly
items-center
@@ -59,28 +57,26 @@ const TogglesSection = twc.div`
const ButtonsSections = twc.div`
flex
max-w-full
gap-4
justify-between
p-[3%]
justify-evenly
items-center
flex-wrap
mt-0
px-2
`;
const ColorPicker = twc.input`
const ColorPickerButton = twc.div`
h-[8vmax]
w-[8vmax]
relative
max-h-12
max-w-11
border-none
outline-none
max-w-12
rounded-full
cursor-pointer
bg-transparent
user-select-none
text-common-white
overflow-hidden
`;
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
? 'flex-col'
: 'flex-row',
@@ -98,14 +94,23 @@ const PlayerMenu = ({
isShown,
}: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const dialogRef = useRef<HTMLDialogElement | null>(null);
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
const endGameDialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({
rotation: player.settings.rotation,
containerRef: settingsContainerRef,
});
const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
const {
fullscreen,
wakeLock,
goToStart,
settings,
setPlaying,
setStopPlayerRandomization,
} = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -123,6 +128,13 @@ const PlayerMenu = ({
const handleResetGame = () => {
resetCurrentGame();
setShowPlayerMenu(false);
setPlaying(false);
setStopPlayerRandomization(false);
};
const handleGoToStart = () => {
goToStart();
setStopPlayerRandomization(false);
};
const toggleFullscreen = () => {
@@ -160,17 +172,25 @@ const PlayerMenu = ({
}}
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>
<TogglesSection>
<ColorPicker
type="color"
value={player.color}
onChange={handleColorChange}
role="button"
aria-label="Color picker"
/>
<ColorPickerButton aria-label="Color picker">
<input
onChange={handleColorChange}
type="color"
className="size-[200%] absolute -left-2 -top-2"
value={player.color}
/>
</ColorPickerButton>
{player.settings.useCommanderDamage && (
<CheckboxContainer>
<div>
<Checkbox
name="usePartner"
checked={player.settings.usePartner}
@@ -195,10 +215,9 @@ const PlayerMenu = ({
aria-checked={player.settings.usePartner}
aria-label="Partner"
/>
</CheckboxContainer>
</div>
)}
<CheckboxContainer>
<div>
<Checkbox
name="usePoison"
checked={player.settings.usePoison}
@@ -223,9 +242,8 @@ const PlayerMenu = ({
aria-checked={player.settings.usePoison}
aria-label="Poison"
/>
</CheckboxContainer>
<CheckboxContainer>
</div>
<div>
<Checkbox
name="useEnergy"
checked={player.settings.useEnergy}
@@ -250,9 +268,8 @@ const PlayerMenu = ({
aria-checked={player.settings.useEnergy}
aria-label="Energy"
/>
</CheckboxContainer>
<CheckboxContainer>
</div>
<div>
<Checkbox
name="useExperience"
checked={player.settings.useExperience}
@@ -277,21 +294,22 @@ const PlayerMenu = ({
aria-checked={player.settings.useExperience}
aria-label="Experience"
/>
</CheckboxContainer>
</div>
</TogglesSection>
<ButtonsSections className="mt-4">
<Button
variant="text"
style={{
cursor: 'pointer',
userSelect: 'none',
}}
onClick={goToStart}
<ButtonsSections>
<button
className="text-primary-main cursor-pointer webkit-user-select-none"
onClick={() => endGameDialogRef.current?.show()}
aria-label="Back to start"
>
<Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button>
<CheckboxContainer>
</button>
<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
name="fullscreen"
checked={document.fullscreenElement ? true : false}
@@ -306,63 +324,114 @@ const PlayerMenu = ({
role="checkbox"
aria-checked={document.fullscreenElement ? true : false}
aria-label="Fullscreen"
style={{ padding: '4px' }}
/>
</CheckboxContainer>
</div>
<Button
variant={wakeLock.active ? 'contained' : 'outlined'}
<button
data-wake-lock-active={settings.keepAwake}
style={{
cursor: 'pointer',
userSelect: 'none',
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"
aria-checked={wakeLock.active}
aria-checked={settings.keepAwake}
aria-label="Keep awake"
>
Keep Awake
</Button>
</button>
<Button
<button
style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize,
padding: '4px',
padding: '2px',
}}
onClick={() => dialogRef.current?.show()}
className="text-primary-main"
onClick={() => resetGameDialogRef.current?.show()}
role="checkbox"
aria-checked={wakeLock.active}
aria-label="Reset Game"
>
<ResetGame size={iconSize} />
</Button>
</button>
</ButtonsSections>
</BetterRowContainer>
<dialog
ref={dialogRef}
className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute bottom-[20%]"
ref={resetGameDialogRef}
className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => resetGameDialogRef.current?.close()}
>
<div className="flex flex-col p-4 gap-2">
<h1 className="text-center">Reset Game?</h1>
<div className="flex justify-evenly gap-4">
<Button
variant="contained"
onClick={() => dialogRef.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 }}
>
No
</Button>
<Button
variant="contained"
onClick={() => {
handleResetGame();
dialogRef.current?.close();
}}
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()}
>
No
</button>
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => {
handleResetGame();
resetGameDialogRef.current?.close();
}}
style={{ fontSize: iconSize }}
>
Yes
</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 }}
>
Yes
</Button>
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>
</dialog>

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 { 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;
}

View File

@@ -3,10 +3,12 @@ import React from 'react';
import { theme } from '../../../Data/theme';
import {
FivePlayers,
FivePlayersSide,
FourPlayers,
FourPlayersSide,
OnePlayerPortrait,
SixPlayers,
SixPlayersSide,
ThreePlayers,
ThreePlayersSide,
TwoPlayersOppositeLandscape,
@@ -40,7 +42,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
switch (numberOfPlayers) {
case 1:
return (
<div>
<>
<FormControlLabel
value={Orientation.Landscape}
control={
@@ -89,7 +91,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
}
label=""
/>
</div>
</>
);
case 2:
return (
@@ -303,20 +305,21 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
}
label=""
/>
{/* <FormControlLabel
value={GridTemplateAreas.FivePlayersSide}
<FormControlLabel
value={Orientation.Portrait}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FivePlayersSide
height={iconHeight}
height={iconHeight}
width={iconWidth}
fill={theme.palette.secondary.main}
/>
}
checkedIcon={
<FivePlayersSide
height={iconHeight}
height={iconHeight}
width={iconWidth}
fill={theme.palette.primary.main}
/>
@@ -325,7 +328,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
label=""
/> */}
/>
</>
);
@@ -356,20 +359,21 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
}
label=""
/>
{/* <FormControlLabel
value={GridTemplateAreas.SixPlayersSide}
<FormControlLabel
value={Orientation.Portrait}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<SixPlayersSide
height={iconHeight}
height={iconHeight}
width={iconWidth}
fill={theme.palette.secondary.main}
/>
}
checkedIcon={
<SixPlayersSide
height={iconHeight}
height={iconHeight}
width={iconWidth}
fill={theme.palette.primary.main}
/>
@@ -378,7 +382,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
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 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`;
@@ -262,18 +262,6 @@ const Start = () => {
</ToggleButtonsWrapper>
<FormLabel>Layout</FormLabel>
{/* <LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
gridAreas={playerOptions.gridAreas}
onChange={(gridAreas) =>
setPlayerOptions({
...playerOptions,
gridAreas,
//TODO fix the layout selection
orientation: Orientation.Portrait,
})
}
/> */}
<LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation}

View File

@@ -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;
};

View File

@@ -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<PlayersContextType | null>(null);

View File

@@ -127,15 +127,15 @@ const getOrientationRotations = (
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.Side;
return Rotation.Flipped;
case 1:
return Rotation.Side;
return Rotation.Flipped;
case 2:
return Rotation.SideFlipped;
return Rotation.Side;
case 3:
return Rotation.SideFlipped;
return Rotation.Normal;
case 4:
return Rotation.SideFlipped;
return Rotation.Normal;
default:
return Rotation.Normal;
}
@@ -163,17 +163,17 @@ const getOrientationRotations = (
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.Side;
return Rotation.SideFlipped;
case 1:
return Rotation.Side;
return Rotation.Flipped;
case 2:
return Rotation.Side;
return Rotation.Flipped;
case 3:
return Rotation.SideFlipped;
return Rotation.Side;
case 4:
return Rotation.SideFlipped;
return Rotation.Normal;
case 5:
return Rotation.SideFlipped;
return Rotation.Normal;
default:
return Rotation.Normal;
}
@@ -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,
};

View File

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

View File

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

View File

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

View File

@@ -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<number>(
savedStartingPlayerIndex ? parseInt(savedStartingPlayerIndex) : -1
);
const setStartingPlayerIndexAndLocalStorage = (index: number) => {
setStartingPlayerIndex(index);
localStorage.setItem('startingPlayerIndex', String(index));
};
const [players, setPlayers] = useState<Player[]>(
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 (
<PlayersContext.Provider value={ctxValue}>

View File

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

View File

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

View File

@@ -0,0 +1,87 @@
type RGBA = {
red: number;
green: number;
blue: number;
alpha: number;
};
export const hexToRgb = (hex: string): RGBA => {
hex = hex.replace(/^#/, '');
let alpha = 255;
if (hex.length === 8) {
alpha = parseInt(hex.slice(6, 8), 16);
hex = hex.substring(0, 6);
}
if (hex.length === 4) {
alpha = parseInt(hex.slice(3, 4).repeat(2), 16);
hex = hex.substring(0, 3);
}
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const num = parseInt(hex, 16);
const red = num >> 16;
const green = (num >> 8) & 255;
const blue = num & 255;
return { red, green, blue, alpha };
};
export const luminance = (a: number, b: number) => {
const l1 = Math.max(a, b);
const l2 = Math.min(a, b);
return (l1 + 0.05) / (l2 + 0.05);
};
export const rgbContrast = (a: RGBA, b: RGBA) => {
return luminance(relativeLuminance(a), relativeLuminance(b));
};
// calculate the color contrast ratio
export const checkContrast = (hexC1: string, hexC2: string) => {
const color1rgb = hexToRgb(hexC1);
const color2rgb = hexToRgb(hexC2);
const contrast = rgbContrast(color1rgb, color2rgb);
if (contrast >= 7) {
return 'AAA';
}
if (contrast >= 4.5) {
return 'AA';
}
if (contrast >= 3) {
return 'AA Large';
}
return 'Fail';
};
// red, green, and blue coefficients
const rc = 0.2126;
const gc = 0.7152;
const bc = 0.0722;
// low-gamma adjust coefficient
const lowc = 1 / 12.92;
function adjustGamma(input: number) {
return Math.pow((input + 0.055) / 1.055, 2.4);
}
export const relativeLuminance = (rgb: RGBA) => {
const rsrgb = rgb.red / 255;
const gsrgb = rgb.green / 255;
const bsrgb = rgb.blue / 255;
const r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb);
const g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb);
const b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb);
return r * rc + g * gc + b * bc;
};

View File

@@ -2,6 +2,50 @@
import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas';
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} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
@@ -36,49 +80,11 @@ export default {
],
sixPlayers: ['player0 player1 player2', 'player3 player4 player5'],
sixPlayersSide: [
'player0 player1 player1 player1 player1 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3',
'player0 player1 player1 player1 player1 player1 player1 player2 player2 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 player4 player4 player5 player5 player5 player5 player5 player5 player3',
],
},
colors: {
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)',
},
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',
},
},
},
colors: baseColors,
keyframes: {
fadeOut: {
'0%': {