Compare commits

...

32 Commits
0.5.5 ... 0.6.7

Author SHA1 Message Date
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
ef06e0d125 bump 2024-02-09 23:04:29 +01:00
Viktor Rådberg
ae9f5707b2 update blur 2024-02-09 23:04:14 +01:00
Viktor Rådberg
a18c253624 bump 2024-01-31 23:12:46 +01:00
Viktor Rådberg
3f319c4f3c add some blur to settings 2024-01-31 23:12:31 +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
Viktor Rådberg
db80e563f2 bump 2024-01-27 18:05:54 +01:00
Viktor Rådberg
573af42b75 fix taps and some settings stuff 2024-01-27 18:05:18 +01:00
Viktor Rådberg
89e1eaff4e bump 2024-01-27 16:25:40 +01:00
Viktor Rådberg
0f4e896342 Merge pull request #31 from Vikeo/swipable-settings
Swipable settings
2024-01-27 16:23:54 +01:00
Viktor Rådberg
dc1d5fe01d tsc 2024-01-27 16:20:09 +01:00
Viktor Rådberg
41e73d2c0c swipe 2024-01-27 11:05:54 +01:00
Viktor Rådberg
724dcf086c is side 2024-01-27 09:32:00 +01:00
Viktor Rådberg
51f9c4d20e initial test 2024-01-26 21:24:40 +01:00
Viktor Rådberg
354c0dbbb2 bump 2024-01-20 11:11:03 +01:00
Viktor Rådberg
3770d13beb fix some styling 2024-01-20 10:56:53 +01:00
Viktor Rådberg
13733242a2 bump 2024-01-14 14:39:20 +01:00
Viktor Rådberg
81f3891b20 add better pwa support 2024-01-14 14:38:56 +01:00
Viktor Rådberg
e153de9093 Release 0.5.51 2024-01-14 13:42:11 +01:00
Viktor Rådberg
07775f85d2 fix start menu style 2024-01-14 13:41:51 +01:00
25 changed files with 2939 additions and 642 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{ {
"name": "life-trinket", "name": "life-trinket",
"private": true, "private": true,
"version": "0.5.5", "version": "0.6.7",
"type": "commonjs", "type": "commonjs",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
@@ -22,6 +22,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-screen-wake-lock": "^3.0.2", "react-screen-wake-lock": "^3.0.2",
"react-swipeable": "^7.0.1",
"react-twc": "^1.3.0", "react-twc": "^1.3.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
@@ -43,8 +44,9 @@
"install": "^0.13.0", "install": "^0.13.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "2.8.8", "prettier": "2.8.8",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.1",
"typescript": "^5.0.2", "typescript": "^5.3.3",
"vite": "^4.4.5" "vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.4"
} }
} }

View File

@@ -17,6 +17,8 @@ export type RotationButtonProps = TwcComponentProps<'button'> & {
$rotation?: number; $rotation?: number;
}; };
export const MAX_TAP_MOVE_DISTANCE = 20;
const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [ const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow', 'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
@@ -38,7 +40,7 @@ const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
: '', : '',
]); ]);
const PartnerDamageSeperator = twc.div<RotationDivProps>((props) => [ const PartnerDamageSeparator = twc.div<RotationDivProps>((props) => [
'bg-black', 'bg-black',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-full h-px' ? 'w-full h-px'
@@ -54,6 +56,7 @@ type CommanderDamageButtonComponentProps = {
type InputProps = { type InputProps = {
opponentIndex: number; opponentIndex: number;
isPartner: boolean; isPartner: boolean;
event: React.PointerEvent<HTMLButtonElement>;
}; };
export const CommanderDamage = ({ export const CommanderDamage = ({
@@ -63,12 +66,8 @@ export const CommanderDamage = ({
}: CommanderDamageButtonComponentProps) => { }: CommanderDamageButtonComponentProps) => {
const { updatePlayer } = usePlayers(); const { updatePlayer } = usePlayers();
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false); const [downLongPressed, setDownLongPressed] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); const downPositionRef = useRef({ x: 0, y: 0 });
const isSide =
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped;
const handleCommanderDamageChange = ( const handleCommanderDamageChange = (
index: number, index: number,
@@ -107,34 +106,47 @@ export const CommanderDamage = ({
handleLifeChange(player.lifeTotal - increment); handleLifeChange(player.lifeTotal - increment);
}; };
const handleDownInput = ({ opponentIndex, isPartner }: InputProps) => { const handleDownInput = ({ opponentIndex, isPartner, event }: InputProps) => {
setTimeoutFinished(false); downPositionRef.current = { x: event.clientX, y: event.clientY };
setHasPressedDown(true); setDownLongPressed(false);
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true); setDownLongPressed(true);
handleCommanderDamageChange(opponentIndex, -1, isPartner); handleCommanderDamageChange(opponentIndex, -1, isPartner);
}, decrementTimeoutMs); }, decrementTimeoutMs);
}; };
const handleUpInput = ({ opponentIndex, isPartner }: InputProps) => { const handleUpInput = ({ opponentIndex, isPartner, event }: InputProps) => {
if (!(hasPressedDown && !timeoutFinished)) { if (downLongPressed) {
return; return;
} }
const upPosition = { x: event.clientX, y: event.clientY };
const hasMoved =
Math.abs(upPosition.x - downPositionRef.current.x) >
MAX_TAP_MOVE_DISTANCE ||
Math.abs(upPosition.y - downPositionRef.current.y) >
MAX_TAP_MOVE_DISTANCE;
if (hasMoved) {
return;
}
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
handleCommanderDamageChange(opponentIndex, 1, isPartner); handleCommanderDamageChange(opponentIndex, 1, isPartner);
setHasPressedDown(false);
}; };
const handleLeaveInput = () => { const handleLeaveInput = () => {
setTimeoutFinished(true); setDownLongPressed(true);
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
setHasPressedDown(false);
}; };
const opponentIndex = opponent.index; const opponentIndex = opponent.index;
const fontSize = isSide ? '4vmax' : '7vmin'; const fontSize = player.isSide ? '4vmax' : '7vmin';
const fontWeight = 'bold'; const fontWeight = 'bold';
const strokeWidth = isSide ? '0.4vmax' : '0.7vmin'; const strokeWidth = player.isSide ? '0.4vmax' : '0.7vmin';
return ( return (
<CommanderDamageContainer <CommanderDamageContainer
@@ -145,10 +157,12 @@ export const CommanderDamage = ({
<CommanderDamageButton <CommanderDamageButton
key={opponentIndex} key={opponentIndex}
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
onPointerDown={() => onPointerDown={(e) =>
handleDownInput({ opponentIndex, isPartner: false }) handleDownInput({ opponentIndex, isPartner: false, event: e })
}
onPointerUp={(e) =>
handleUpInput({ opponentIndex, isPartner: false, event: e })
} }
onPointerUp={() => handleUpInput({ opponentIndex, isPartner: false })}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
@@ -171,15 +185,15 @@ export const CommanderDamage = ({
{opponent.settings.usePartner && ( {opponent.settings.usePartner && (
<> <>
<PartnerDamageSeperator $rotation={player.settings.rotation} /> <PartnerDamageSeparator $rotation={player.settings.rotation} />
<CommanderDamageButton <CommanderDamageButton
key={opponentIndex} key={opponentIndex}
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
onPointerDown={() => onPointerDown={(e) =>
handleDownInput({ opponentIndex, isPartner: true }) handleDownInput({ opponentIndex, isPartner: true, event: e })
} }
onPointerUp={() => onPointerUp={(e) =>
handleUpInput({ opponentIndex, isPartner: true }) handleUpInput({ opponentIndex, isPartner: true, event: e })
} }
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
onContextMenu={( onContextMenu={(

View File

@@ -3,7 +3,7 @@ import { twc } from 'react-twc';
import { decrementTimeoutMs } from '../../Data/constants'; import { decrementTimeoutMs } from '../../Data/constants';
import { CounterType, Rotation } from '../../Types/Player'; import { CounterType, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
import { RotationDivProps } from './CommanderDamage'; import { MAX_TAP_MOVE_DISTANCE, RotationDivProps } from './CommanderDamage';
const ExtraCounterContainer = twc.div` const ExtraCounterContainer = twc.div`
flex flex
@@ -47,6 +47,7 @@ type ExtraCounterProps = {
type: CounterType; type: CounterType;
setCounterTotal: (updatedCounterTotal: number, type: CounterType) => void; setCounterTotal: (updatedCounterTotal: number, type: CounterType) => void;
rotation: number; rotation: number;
isSide: boolean;
playerIndex: number; playerIndex: number;
}; };
@@ -56,14 +57,13 @@ const ExtraCounter = ({
setCounterTotal, setCounterTotal,
type, type,
rotation, rotation,
isSide,
playerIndex, playerIndex,
}: ExtraCounterProps) => { }: ExtraCounterProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false); const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); const [hasPressedDown, setHasPressedDown] = useState(false);
const downPositionRef = useRef({ x: 0, y: 0 });
const isSide =
rotation === Rotation.Side || rotation === Rotation.SideFlipped;
const handleCountChange = (increment: number) => { const handleCountChange = (increment: number) => {
if (!counterTotal) { if (!counterTotal) {
@@ -73,7 +73,8 @@ const ExtraCounter = ({
setCounterTotal(counterTotal + increment, type); setCounterTotal(counterTotal + increment, type);
}; };
const handleDownInput = () => { const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
setTimeoutFinished(false); setTimeoutFinished(false);
setHasPressedDown(true); setHasPressedDown(true);
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
@@ -82,10 +83,23 @@ const ExtraCounter = ({
}, decrementTimeoutMs); }, decrementTimeoutMs);
}; };
const handleUpInput = () => { const handleUpInput = (event: React.PointerEvent<HTMLButtonElement>) => {
if (!(hasPressedDown && !timeoutFinished)) { if (!(hasPressedDown && !timeoutFinished)) {
return; return;
} }
const upPosition = { x: event.clientX, y: event.clientY };
const hasMoved =
Math.abs(upPosition.x - downPositionRef.current.x) >
MAX_TAP_MOVE_DISTANCE ||
Math.abs(upPosition.y - downPositionRef.current.y) >
MAX_TAP_MOVE_DISTANCE;
if (hasMoved) {
return;
}
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
handleCountChange(1); handleCountChange(1);
setHasPressedDown(false); setHasPressedDown(false);

View File

@@ -1,7 +1,9 @@
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { TwcComponentProps, twc } from 'react-twc'; import { TwcComponentProps, twc } from 'react-twc';
import { lifeLongPressMultiplier } from '../../Data/constants'; 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'> & { type RotationButtonProps = TwcComponentProps<'div'> & {
$align?: string; $align?: string;
@@ -12,7 +14,6 @@ const LifeCounterButtonTwc = twc.button`
h-full h-full
w-full w-full
flex flex
text-lifeCounter-text
font-semibold font-semibold
bg-transparent bg-transparent
border-none border-none
@@ -39,29 +40,41 @@ const TextContainer = twc.div<RotationButtonProps>((props) => [
]); ]);
type LifeCounterButtonProps = { type LifeCounterButtonProps = {
lifeTotal: number; player: Player;
setLifeTotal: (lifeTotal: number) => void; setLifeTotal: (lifeTotal: number) => void;
rotation: number;
operation: 'add' | 'subtract'; operation: 'add' | 'subtract';
increment: number; increment: number;
}; };
const LifeCounterButton = ({ const LifeCounterButton = ({
lifeTotal, player,
setLifeTotal, setLifeTotal,
rotation,
operation, operation,
increment, increment,
}: LifeCounterButtonProps) => { }: LifeCounterButtonProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false); const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); 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) => { const handleLifeChange = (increment: number) => {
setLifeTotal(lifeTotal + increment); setLifeTotal(player.lifeTotal + increment);
}; };
const handleDownInput = () => { const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
setTimeoutFinished(false); setTimeoutFinished(false);
setHasPressedDown(true); setHasPressedDown(true);
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
@@ -70,10 +83,23 @@ const LifeCounterButton = ({
}, 500); }, 500);
}; };
const handleUpInput = () => { const handleUpInput = (event: React.PointerEvent<HTMLButtonElement>) => {
if (!(hasPressedDown && !timeoutFinished)) { if (!(hasPressedDown && !timeoutFinished)) {
return; return;
} }
const upPosition = { x: event.clientX, y: event.clientY };
const hasMoved =
Math.abs(upPosition.x - downPositionRef.current.x) >
MAX_TAP_MOVE_DISTANCE ||
Math.abs(upPosition.y - downPositionRef.current.y) >
MAX_TAP_MOVE_DISTANCE;
if (hasMoved) {
return;
}
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
handleLifeChange(operation === 'add' ? 1 : -1); handleLifeChange(operation === 'add' ? 1 : -1);
setHasPressedDown(false); setHasPressedDown(false);
@@ -86,7 +112,8 @@ const LifeCounterButton = ({
}; };
const fontSize = const fontSize =
rotation === Rotation.SideFlipped || rotation === Rotation.Side player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? '8vmax' ? '8vmax'
: '12vmin'; : '12vmin';
@@ -102,8 +129,11 @@ const LifeCounterButton = ({
aria-label={`${operation === 'add' ? 'Add' : 'Subtract'} life`} aria-label={`${operation === 'add' ? 'Add' : 'Subtract'} life`}
> >
<TextContainer <TextContainer
$rotation={rotation} $rotation={player.settings.rotation}
$align={operation === 'add' ? 'right' : 'left'} $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'} {operation === 'add' ? '\u002B' : '\u2212'}
</TextContainer> </TextContainer>

View File

@@ -4,13 +4,11 @@ import { Rotation } from '../../Types/Player';
import { RotationDivProps } from './CommanderDamage'; import { RotationDivProps } from './CommanderDamage';
const LoseButton = twc.div<RotationDivProps>((props) => [ 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', '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.SideFlipped || props.$rotation === Rotation.Side
? `right-auto top-[15%] left-[27%]` ? `left-[21%]`
: props.$rotation === Rotation.Side : 'top-[21%]',
? `right-auto top-[15%] left-[27%]`
: 'right-[15%] top-1/4',
]); ]);
type LoseButtonProps = { type LoseButtonProps = {
@@ -24,6 +22,8 @@ export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
? rotation ? rotation
: rotation === Rotation.Side : rotation === Rotation.Side
? rotation - 180 ? rotation - 180
: rotation === Rotation.Flipped
? rotation - 180
: rotation; : rotation;
return ( return (
@@ -33,7 +33,7 @@ export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
aria-label={`Lose Game`} aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }} style={{ rotate: `${calcRotation}deg` }}
> >
<Skull size="5vmin" color="black" opacity={0.5} /> <Skull size="8vmin" color="black" opacity={0.5} />
</LoseButton> </LoseButton>
); );
}; };

View File

@@ -1,30 +0,0 @@
import { twc } from 'react-twc';
import { Cog } from '../../Icons/generated';
import { Rotation } from '../../Types/Player';
import { RotationButtonProps } from './CommanderDamage';
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;
};
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => {
return (
<SettingsButtonTwc
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<Cog size="5vmin" color="black" opacity="0.3" />
</SettingsButtonTwc>
);
};
export default SettingsButton;

View File

@@ -10,6 +10,8 @@ import {
import { CounterType, Player, Rotation } from '../../Types/Player'; import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter'; import ExtraCounter from '../Buttons/ExtraCounter';
import { useEffect, useState } from 'react';
import { checkContrast } from '../../Utils/checkContrast';
const Container = twc.div<RotationDivProps>((props) => [ const Container = twc.div<RotationDivProps>((props) => [
'flex', 'flex',
@@ -31,6 +33,17 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers(); 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 = ( const handleCounterChange = (
updatedCounterTotal: number, updatedCounterTotal: number,
@@ -93,13 +106,20 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useCommanderDamage && ( {useCommanderDamage && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} 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} type={CounterType.CommanderTax}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'commanderTax' (counter) => counter.type === 'commanderTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -107,13 +127,20 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{Boolean(useCommanderDamage && usePartner) && ( {Boolean(useCommanderDamage && usePartner) && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} 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} type={CounterType.PartnerTax}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'partnerTax' (counter) => counter.type === 'partnerTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -121,12 +148,19 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{usePoison && ( {usePoison && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} 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} type={CounterType.Poison}
counterTotal={ counterTotal={
player.extraCounters?.find((counter) => counter.type === 'poison') player.extraCounters?.find((counter) => counter.type === 'poison')
?.value ?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -134,12 +168,19 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useEnergy && ( {useEnergy && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} 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} type={CounterType.Energy}
counterTotal={ counterTotal={
player.extraCounters?.find((counter) => counter.type === 'energy') player.extraCounters?.find((counter) => counter.type === 'energy')
?.value ?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -147,13 +188,20 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useExperience && ( {useExperience && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} 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} type={CounterType.Experience}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'experience' (counter) => counter.type === 'experience'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />

View File

@@ -8,7 +8,7 @@ import {
import LifeCounterButton from '../Buttons/LifeCounterButton'; import LifeCounterButton from '../Buttons/LifeCounterButton';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
const LifeCountainer = twc.div<RotationDivProps>((props) => [ const LifeContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow relative w-full h-full justify-between items-center', 'flex flex-grow relative w-full h-full justify-between items-center',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse' ? 'flex-col-reverse'
@@ -49,28 +49,13 @@ type HealthProps = {
const Health = ({ const Health = ({
player, player,
rotation,
handleLifeChange, handleLifeChange,
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;
@@ -99,12 +84,13 @@ const Health = ({
}, [textContainerRef]); }, [textContainerRef]);
const calculateFontSize = (container: HTMLDivElement) => { const calculateFontSize = (container: HTMLDivElement) => {
const isSide = const widthRatio = player.isSide
rotation === Rotation.SideFlipped || rotation === Rotation.Side; ? container.clientHeight
: container.clientWidth;
const widthRatio = isSide ? container.clientHeight : container.clientWidth; const heightRatio = player.isSide
? container.clientWidth
const heightRatio = isSide ? container.clientWidth : container.clientHeight; : container.clientHeight;
const minRatio = Math.min(widthRatio, heightRatio); const minRatio = Math.min(widthRatio, heightRatio);
@@ -116,11 +102,10 @@ const Health = ({
}; };
return ( return (
<LifeCountainer $rotation={player.settings.rotation}> <LifeContainer $rotation={player.settings.rotation}>
<LifeCounterButton <LifeCounterButton
lifeTotal={player.lifeTotal} player={player}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="subtract" operation="subtract"
increment={-1} increment={-1}
/> />
@@ -148,13 +133,12 @@ const Health = ({
</LifeCounterTextContainer> </LifeCounterTextContainer>
</TextWrapper> </TextWrapper>
<LifeCounterButton <LifeCounterButton
lifeTotal={player.lifeTotal} player={player}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="add" operation="add"
increment={1} increment={1}
/> />
</LifeCountainer> </LifeContainer>
); );
}; };

View File

@@ -1,15 +1,45 @@
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
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 {
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton'; import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar'; import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar'; import ExtraCountersBar from '../Counters/ExtraCountersBar';
import { Paragraph } from '../Misc/TextComponents';
import PlayerMenu from '../Player/PlayerMenu'; import PlayerMenu from '../Player/PlayerMenu';
import Health from './Health'; import Health from './Health';
import { baseColors } from '../../../tailwind.config';
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;
};
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => {
return (
<SettingsButtonTwc
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<Cog size="5vmin" color="black" opacity="0.3" />
</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,10 +51,8 @@ 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', '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
? `rotate-[${props.$rotation - 90}deg]` ? `rotate-[${props.$rotation - 90}deg]`
: '', : '',
@@ -64,34 +92,71 @@ type LifeCounterProps = {
opponents: Player[]; opponents: Player[];
}; };
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 } = useGlobalSettings();
const playingTimerRef = 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);
const [differenceKey, setDifferenceKey] = useState(Date.now()); const [differenceKey, setDifferenceKey] = useState(Date.now());
const [isLandscape, setIsLandscape] = useState(false);
const calcRot = player.isSide
? player.settings.rotation - 180
: player.settings.rotation;
const rotationAngle = isLandscape ? calcRot : calcRot + 90;
const handlers = useSwipeable({
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);
},
swipeDuration: 500,
onSwiping: (e) => e.event.stopPropagation(),
rotationAngle,
});
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setRecentDifference(0); setRecentDifference(0);
}, 3_000); }, RECENT_DIFFERENCE_TTL);
return () => clearTimeout(timer); const resizeObserver = new ResizeObserver(() => {
}, [recentDifference]); if (document.body.clientWidth > document.body.clientHeight)
setIsLandscape(true);
else setIsLandscape(false);
return;
});
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]);
useEffect(() => { useEffect(() => {
if (player.showStartingPlayer) { playingTimerRef.current = setTimeout(() => {
const playingTimer = setTimeout(() => { localStorage.setItem('playing', 'true');
localStorage.setItem('playing', 'true'); }, 10_000);
player.showStartingPlayer = false;
updatePlayer(player);
}, 3_000);
return () => clearTimeout(playingTimer); return () => clearTimeout(playingTimerRef.current);
} }, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [player.showStartingPlayer]);
player.settings.rotation === Rotation.SideFlipped || player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side; player.settings.rotation === Rotation.Side;
@@ -124,21 +189,36 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
<LifeCounterWrapper <LifeCounterWrapper
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }} style={{ rotate: `${calcRotation}deg` }}
{...handlers}
> >
{settings.showStartingPlayer && {settings.showStartingPlayer &&
player.isStartingPlayer && player.isStartingPlayer &&
player.showStartingPlayer && ( player.showStartingPlayer && (
<StartingPlayerNoticeWrapper <div
style={{ rotate: `${calcRotation}deg` }} className="z-10 flex absolute w-full h-full justify-center items-center select-none cursor-pointer webkit-user-select-none"
style={{
rotate: `${calcRotation}deg`,
backgroundImage: `radial-gradient(circle at center, ${player.color}, ${baseColors.primary.main})`,
}}
onClick={() => {
clearTimeout(playingTimerRef.current);
localStorage.setItem('playing', 'true');
player.showStartingPlayer = false;
updatePlayer(player);
}}
> >
<DynamicText <DynamicText
style={{ style={{
rotate: `${calcTextRotation}deg`, rotate: `${calcTextRotation}deg`,
}} }}
> >
You start! <div className="flex flex-col justify-center items-center">
<Paragraph>👑</Paragraph>
<Paragraph>You start!</Paragraph>
<Paragraph className="text-xl">(Press to hide)</Paragraph>
</div>
</DynamicText> </DynamicText>
</StartingPlayerNoticeWrapper> </div>
)} )}
{player.hasLost && ( {player.hasLost && (
@@ -150,12 +230,14 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
key={player.index} key={player.index}
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<SettingsButton {settings.showPlayerMenuCog && (
onClick={() => { <SettingsButton
setShowPlayerMenu(!showPlayerMenu); onClick={() => {
}} setShowPlayerMenu(!showPlayerMenu);
rotation={player.settings.rotation} }}
/> rotation={player.settings.rotation}
/>
)}
{playerCanLose(player) && ( {playerCanLose(player) && (
<LoseGameButton <LoseGameButton
rotation={player.settings.rotation} rotation={player.settings.rotation}
@@ -170,9 +252,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<ExtraCountersBar player={player} /> <ExtraCountersBar player={player} />
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} /> <PlayerMenu
)} isShown={showPlayerMenu}
player={player}
setShowPlayerMenu={setShowPlayerMenu}
/>
</LifeCounterWrapper> </LifeCounterWrapper>
</LifeCounterContentWrapper> </LifeCounterContentWrapper>
); );

View File

@@ -2,8 +2,9 @@ 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 w-[80vw] h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary`; 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]`;
type InfoModalProps = { type InfoModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -12,63 +13,86 @@ type InfoModalProps = {
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => { export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal
<ModalWrapper> open={isOpen}
<div> onClose={closeModal}
<h2 className="text-2xl text-center mb-4">📋 Usage Guide</h2> style={{ display: 'flex', justifyContent: 'center' }}
<Separator height="1px" /> >
<Paragraph className="my-4"> <>
There are some controls that you might not know about, so here's a <div className="flex justify-center items-center relative w-full max-w-[532px]">
short list of them. <button
</Paragraph> onClick={closeModal}
<h3 className="text-lg font-bold mb-2">Life counter</h3> 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"
<ul className="list-disc ml-6 mb-4">
<li>
<strong>Tap</strong> on a player's + or - button to add or
subtract <strong>1 life</strong>.
</li>
<li>
<strong>Long press</strong> on a player's + or - button to add or
subtract <strong>10 life</strong>.
</li>
</ul>
<h3 className="text-lg font-bold mb-2">
Commander damage and other counters
</h3>
<ul className="list-disc ml-6 mb-4">
<li>
<strong>Tap</strong> on the counter to add{' '}
<strong>1 counter</strong>.
</li>
<li>
<strong>Long press</strong> on the counter to subtract{' '}
<strong>1 counter</strong>.
</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>
</div>
<div className="text-center mt-4">
Visit my
<a
href="https://github.com/Vikeo/LifeTrinket"
target="_blank"
className="text-text-secondary underline"
> >
{' '} <Cross size="16px" className="text-text-primary " />
GitHub{' '} </button>
</a>
for more info about this web app.
</div> </div>
</ModalWrapper> <ModalWrapper>
<div>
<h2 className="text-2xl text-center mb-4">📋 Usage Guide</h2>
<Separator height="1px" />
<Paragraph className="my-4">
There are some controls that you might not know about, so here's a
short list of them.
</Paragraph>
<h3 className="text-lg font-bold mb-2">Life counter</h3>
<ul className="list-disc ml-6 mb-4">
<li>
<strong>Tap</strong> on a player's + or - button to add or
subtract <strong>1 life</strong>.
</li>
<li>
<strong>Long press</strong> on a player's + or - button to add
or subtract <strong>10 life</strong>.
</li>
</ul>
<h3 className="text-lg font-bold mb-2">
Commander damage and other counters
</h3>
<ul className="list-disc ml-6 mb-4">
<li>
<strong>Tap</strong> on the counter to add{' '}
<strong>1 counter</strong>.
</li>
<li>
<strong>Long press</strong> on the counter to subtract{' '}
<strong>1 counter</strong>.
</li>
</ul>
<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{' '}
<a
href="https://github.com/Vikeo/LifeTrinket"
target="_blank"
className="text-text-secondary underline"
>
GitHub
</a>{' '}
for more info about this web app.
</div>
</ModalWrapper>
</>
</Modal> </Modal>
); );
}; };

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,117 +67,152 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal
<ModalWrapper> open={isOpen}
<Container> onClose={closeModal}
<h2 className="text-center text-2xl mb-2"> Settings </h2> className="w-full flex justify-center"
<Separator height="1px" /> >
<SettingContainer> <>
<ToggleContainer> <div className="flex justify-center items-center relative w-full max-w-[532px]">
<FormLabel>Show Start Player</FormLabel> <button
<Switch onClick={closeModal}
checked={settings.showStartingPlayer} 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"
onChange={() => { >
setSettings({ <Cross size="16px" className="text-text-primary " />
...settings, </button>
showStartingPlayer: !settings.showStartingPlayer, </div>
}); <ModalWrapper>
}} <Container>
/> <h2 className="text-center text-2xl mb-2"> Settings </h2>
</ToggleContainer> <Separator height="1px" />
<Description> <SettingContainer>
On start or reset of game, will pick a random player who will <ToggleContainer>
start first if this is enabled. <FormLabel>Show Start Player</FormLabel>
</Description> <Switch
</SettingContainer> checked={settings.showStartingPlayer}
<SettingContainer> onChange={() => {
<ToggleContainer> setSettings({
<FormLabel>Keep Awake</FormLabel> ...settings,
<Switch showStartingPlayer: !settings.showStartingPlayer,
checked={settings.keepAwake} });
onChange={() => { }}
setSettings({ ...settings, keepAwake: !settings.keepAwake }); />
}} </ToggleContainer>
/> <Description>
</ToggleContainer> On start or reset of game, will pick a random player who will
<Description> start first if this is enabled.
Will prevent device from going to sleep while this app is open if </Description>
this is enabled. </SettingContainer>
</Description> <SettingContainer>
</SettingContainer> <ToggleContainer>
<SettingContainer> <FormLabel>Show Player Menu Cog</FormLabel>
<ToggleContainer> <Switch
<FormLabel>Go fullscreen on start (Android only)</FormLabel> checked={settings.showPlayerMenuCog}
<Switch onChange={() => {
checked={settings.goFullscreenOnStart} setSettings({
onChange={() => { ...settings,
setSettings({ showPlayerMenuCog: !settings.showPlayerMenuCog,
...settings, });
goFullscreenOnStart: !settings.goFullscreenOnStart, }}
}); />
}} </ToggleContainer>
/> <Description>
</ToggleContainer> A cog on the top right of each player's card will be shown if
<Description> this is enabled.
Will enter fullscreen mode when starting a game if this is </Description>
enabled. </SettingContainer>
</Description> <SettingContainer>
</SettingContainer> <ToggleContainer>
{!isPWA && ( <FormLabel>Keep Awake</FormLabel>
<> <Switch
<Separator height="1px" /> checked={settings.keepAwake}
<SettingContainer> onChange={() => {
<ToggleContainer> setSettings({
<Paragraph> ...settings,
<b>Tip:</b> You can{' '} keepAwake: !settings.keepAwake,
<b>add this webapp to your home page on iOS</b> or{' '} });
<b>install it on Android</b> to have it act just like a }}
normal app! />
</Paragraph> </ToggleContainer>
</ToggleContainer> <Description>
<Description className="mt-1"> Will prevent device from going to sleep while this app is open
If you do, this app will work offline and the toolbar will be if this is enabled.
automatically hidden. </Description>
</Description> </SettingContainer>
</SettingContainer> <SettingContainer>
</> <ToggleContainer>
)} <FormLabel>Go fullscreen on start (Android only)</FormLabel>
<Separator height="1px" /> <Switch
<SettingContainer> checked={settings.goFullscreenOnStart}
<Paragraph> onChange={() => {
{/* @ts-expect-error is defined in vite.config.ts*/} setSettings({
Current version: {APP_VERSION}{' '} ...settings,
{isLatestVersion && ( goFullscreenOnStart: !settings.goFullscreenOnStart,
<span className="text-sm text-text-secondary">(latest)</span> });
)} }}
</Paragraph> />
{!isLatestVersion && newVersion && ( </ToggleContainer>
<Paragraph className="text-text-secondary text-lg text-center"> <Description>
New version ({newVersion}) is available!{' '} Will enter fullscreen mode when starting a game if this is
</Paragraph> enabled.
</Description>
</SettingContainer>
{!isPWA && (
<>
<Separator height="1px" />
<SettingContainer>
<ToggleContainer>
<Paragraph>
<b>Tip:</b> You can{' '}
<b>add this webapp to your home page on iOS</b> or{' '}
<b>install it on Android</b> to have it act just like a
normal app!
</Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will
be automatically hidden.
</Description>
</SettingContainer>
</>
)} )}
</SettingContainer> <Separator height="1px" />
{!isLatestVersion && newVersion && ( <SettingContainer>
<Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
)}
</SettingContainer>
{!isLatestVersion && newVersion && (
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)}
<Separator height="1px" />
<Button <Button
variant="contained" variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }} onClick={closeModal}
onClick={() => window?.location?.reload()} style={{ marginTop: '0.25rem' }}
> >
<span>Update</span> Save and Close
<span className="text-xs">&nbsp;(reload app)</span>
</Button> </Button>
)} </Container>
<Separator height="1px" /> </ModalWrapper>
</>
<Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close
</Button>
</Container>
</ModalWrapper>
</Modal> </Modal>
); );
}; };

View File

@@ -17,10 +17,7 @@ import {
ResetGame, ResetGame,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { import { RotationDivProps } from '../Buttons/CommanderDamage';
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
const CheckboxContainer = twc.div``; const CheckboxContainer = twc.div``;
@@ -31,10 +28,12 @@ const PlayerMenuWrapper = twc.div`
w-full w-full
h-full h-full
bg-background-settings bg-background-settings
backdrop-blur-[3px]
items-center items-center
justify-center justify-center
z-[2] z-[2]
webkit-user-select-none webkit-user-select-none
transition-all
`; `;
const BetterRowContainer = twc.div` const BetterRowContainer = twc.div`
@@ -43,16 +42,19 @@ const BetterRowContainer = twc.div`
flex-grow flex-grow
w-full w-full
h-full h-full
justify-end justify-between
items-stretch items-stretch
`; `;
const TogglesSection = twc.div` const TogglesSection = twc.div`
flex flex
relative
flex-row flex-row
flex-wrap
relative
gap-2 gap-2
h-full
justify-evenly justify-evenly
items-center
`; `;
const ButtonsSections = twc.div` const ButtonsSections = twc.div`
@@ -62,20 +64,18 @@ const ButtonsSections = twc.div`
justify-between justify-between
p-[3%] p-[3%]
items-center items-center
flex-wrap
`; `;
const ColorPicker = twc.input` const ColorPickerButton = twc.div`
absolute
top-[5%]
left-[5%]
h-[8vmax] h-[8vmax]
w-[8vmax] w-[8vmax]
border-none relative
outline-none max-h-12
max-w-12
rounded-full
cursor-pointer cursor-pointer
bg-transparent overflow-hidden
user-select-none
text-common-white
`; `;
const SettingsContainer = twc.div<RotationDivProps>((props) => [ const SettingsContainer = twc.div<RotationDivProps>((props) => [
@@ -85,33 +85,26 @@ const SettingsContainer = twc.div<RotationDivProps>((props) => [
: 'flex-row', : 'flex-row',
]); ]);
const CloseButton = twc.button<RotationButtonProps>((props) => [
'absolute border-none outline-none cursor-pointer bg-transparent z-[99]',
props.$rotation === Rotation.Side
? `top-[5%] right-auto left-[5%]`
: props.$rotation === Rotation.SideFlipped
? 'top-auto left-auto bottom-[5%] right-[5%]'
: 'top-[15%] right-[5%]',
]);
type PlayerMenuProps = { type PlayerMenuProps = {
player: Player; player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void; setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean;
}; };
const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => { const PlayerMenu = ({
player,
setShowPlayerMenu,
isShown,
}: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null); const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const dialogRef = useRef<HTMLDialogElement | null>(null); const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({ const { isSide } = useSafeRotate({
rotation: player.settings.rotation, rotation: player.settings.rotation,
containerRef: settingsContainerRef, containerRef: settingsContainerRef,
}); });
const handleOnClick = () => { const { fullscreen, wakeLock, goToStart, settings } = useGlobalSettings();
setShowPlayerMenu(false);
};
const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers(); const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -142,7 +135,6 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const buttonFontSize = isSide ? '1.5vmax' : '3vmin'; const buttonFontSize = isSide ? '1.5vmax' : '3vmin';
const iconSize = isSide ? '6vmin' : '3vmax'; const iconSize = isSide ? '6vmin' : '3vmax';
const extraCountersSize = isSide ? '8vmin' : '4vmax'; const extraCountersSize = isSide ? '8vmin' : '4vmax';
const closeButtonSize = isSide ? '6vmin' : '3vmax';
const calcRotation = const calcRotation =
player.settings.rotation === Rotation.Side player.settings.rotation === Rotation.Side
@@ -156,33 +148,10 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
//TODO: Fix hacky solution to rotation for SideFlipped //TODO: Fix hacky solution to rotation for SideFlipped
style={{ style={{
rotate: rotate:
player.settings.rotation === Rotation.SideFlipped ? '180deg' : '', player.settings.rotation === Rotation.SideFlipped ? `180deg` : '',
translate: isShown ? '' : player.isSide ? `-100%` : `0 -100%`,
}} }}
> >
<CloseButton
$rotation={player.settings.rotation}
style={{
rotate:
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '',
}}
>
<Button
variant="text"
onClick={handleOnClick}
style={{
margin: 0,
padding: 0,
height: closeButtonSize,
width: closeButtonSize,
}}
>
<Cross size={closeButtonSize} />
</Button>
</CloseButton>
<SettingsContainer <SettingsContainer
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{ style={{
@@ -190,15 +159,24 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
}} }}
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<ColorPicker {settings.showPlayerMenuCog && (
type="color" <button
value={player.color} onClick={() => setShowPlayerMenu(false)}
onChange={handleColorChange} className="flex absolute top-0 right-2 z-10 w-8 h-8 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2"
role="button" >
aria-label="Color picker" <Cross size="16px" className="text-primary-main " />
/> </button>
)}
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
<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 && ( {player.settings.useCommanderDamage && (
<CheckboxContainer> <CheckboxContainer>
<Checkbox <Checkbox
@@ -227,7 +205,6 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
)} )}
<CheckboxContainer> <CheckboxContainer>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
@@ -254,7 +231,6 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
aria-label="Poison" aria-label="Poison"
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer> <CheckboxContainer>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
@@ -281,7 +257,6 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
aria-label="Energy" aria-label="Energy"
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer> <CheckboxContainer>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
@@ -362,7 +337,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
fontSize: buttonFontSize, fontSize: buttonFontSize,
padding: '4px', padding: '4px',
}} }}
onClick={() => dialogRef.current?.show()} onClick={() => resetGameDialogRef.current?.show()}
role="checkbox" role="checkbox"
aria-checked={wakeLock.active} aria-checked={wakeLock.active}
aria-label="Reset Game" aria-label="Reset Game"
@@ -372,27 +347,30 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
</ButtonsSections> </ButtonsSections>
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={dialogRef} ref={resetGameDialogRef}
className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute top-[10%]" className="z-[999] size-full bg-background-settings"
onClick={() => resetGameDialogRef.current?.close()}
> >
<div className="h-full flex flex-col p-4 gap-2"> <div className="flex size-full items-center justify-center">
<h1 className="text-center">Reset Game?</h1> <div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-2xl border-none">
<div className="flex justify-evenly gap-4"> <h1 className="text-center text-text-primary">Reset Game?</h1>
<Button <div className="flex justify-evenly gap-4">
variant="contained" <Button
onClick={() => dialogRef.current?.close()} variant="contained"
> onClick={() => resetGameDialogRef.current?.close()}
No >
</Button> No
<Button </Button>
variant="contained" <Button
onClick={() => { variant="contained"
handleResetGame(); onClick={() => {
dialogRef.current?.close(); handleResetGame();
}} resetGameDialogRef.current?.close();
> }}
Yes >
</Button> Yes
</Button>
</div>
</div> </div>
</div> </div>
</dialog> </dialog>

View File

@@ -18,7 +18,7 @@ import { twc } from 'react-twc';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape'; import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
import { Orientation } from '../../../Types/Settings'; import { Orientation } from '../../../Types/Settings';
const LayoutWrapper = twc.div`flex flex-row justify-between self-center`; const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
type LayoutOptionsProps = { type LayoutOptionsProps = {
numberOfPlayers: number; numberOfPlayers: number;
@@ -31,14 +31,16 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
selectedOrientation, selectedOrientation,
onChange, onChange,
}) => { }) => {
const iconHeight = '30vmin'; const iconWidth = '21vmin';
const iconWidth = '20vmin'; const iconHeight = '40vmin';
const iconMaxWidth = '124px';
const iconMaxHeight = '196px';
const renderLayoutOptions = () => { const renderLayoutOptions = () => {
switch (numberOfPlayers) { switch (numberOfPlayers) {
case 1: case 1:
return ( return (
<> <div>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
@@ -58,6 +60,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
@@ -81,11 +84,12 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
</> </div>
); );
case 2: case 2:
return ( return (
@@ -94,6 +98,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersSameSide <TwoPlayersSameSide
height={iconHeight} height={iconHeight}
@@ -117,6 +122,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositePortrait <TwoPlayersOppositePortrait
height={iconHeight} height={iconHeight}
@@ -140,6 +146,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.OppositeLandscape} value={Orientation.OppositeLandscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositeLandscape <TwoPlayersOppositeLandscape
height={iconHeight} height={iconHeight}
@@ -168,6 +175,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayers <ThreePlayers
height={iconHeight} height={iconHeight}
@@ -191,6 +199,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayersSide <ThreePlayersSide
height={iconHeight} height={iconHeight}
@@ -220,6 +229,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayers <FourPlayers
height={iconHeight} height={iconHeight}
@@ -243,6 +253,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayersSide <FourPlayersSide
height={iconHeight} height={iconHeight}
@@ -272,6 +283,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayers <FivePlayers
height={iconHeight} height={iconHeight}
@@ -324,6 +336,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayers <SixPlayers
height={iconHeight} height={iconHeight}

View File

@@ -20,7 +20,9 @@ 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 ToggleButtonsWrapper = twc.div`flex flex-row justify-between items-center`; const ToggleButtonsWrapper = twc.div`flex flex-row justify-between items-center`;
@@ -174,44 +176,49 @@ const Start = () => {
Life Trinket Life Trinket
</h1> </h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] mb-8 px-4"> <div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
<FormControl focused={false}> <FormControl focused={false} style={{ width: '100%' }}>
<FormLabel>Number of Players</FormLabel> <FormLabel>Number of Players</FormLabel>
<Slider <SliderWrapper>
title="Number of Players" <Slider
max={6} title="Number of Players"
min={1} max={6}
aria-label="Custom marks" min={1}
value={playerOptions?.numberOfPlayers ?? 4} aria-label="Custom marks"
getAriaValueText={valuetext} value={playerOptions?.numberOfPlayers ?? 4}
step={null} getAriaValueText={valuetext}
marks={playerMarks} step={null}
onChange={(_e, value) => { marks={playerMarks}
setPlayerOptions({ onChange={(_e, value) => {
...playerOptions, setPlayerOptions({
numberOfPlayers: value as number, ...playerOptions,
orientation: Orientation.Landscape, numberOfPlayers: value as number,
}); orientation: Orientation.Landscape,
}} });
/> }}
/>
</SliderWrapper>
<FormLabel className="mt-[0.7rem]">Starting Health</FormLabel> <FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
<Slider <SliderWrapper>
title="Starting Health" <Slider
max={60} title="Starting Health"
min={20} max={60}
aria-label="Custom marks" min={20}
value={playerOptions?.startingLifeTotal ?? 40} aria-label="Custom marks"
getAriaValueText={valuetext} value={playerOptions?.startingLifeTotal ?? 40}
step={10} getAriaValueText={valuetext}
marks={healthMarks} step={10}
onChange={(_e, value) => marks={healthMarks}
setPlayerOptions({ onChange={(_e, value) =>
...playerOptions, setPlayerOptions({
startingLifeTotal: value as number, ...playerOptions,
orientation: Orientation.Landscape, startingLifeTotal: value as number,
}) orientation: Orientation.Landscape,
} })
/> }
/>
</SliderWrapper>
<ToggleButtonsWrapper className="mt-4"> <ToggleButtonsWrapper className="mt-4">
<ToggleContainer> <ToggleContainer>

View File

@@ -229,6 +229,7 @@ export const createInitialPlayers = ({
extraCounters: [], extraCounters: [],
commanderDamage, commanderDamage,
hasLost: false, hasLost: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
}; };
players.push(player); players.push(player);

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
export interface OrientationState {
angle: number;
type: string;
}
const defaultState: OrientationState = {
angle: 0,
type: 'landscape-primary',
};
export default function useOrientation(
initialState: OrientationState = defaultState
) {
const [state, setState] = useState(initialState);
const [isLandscape, setIsLandscape] = useState(false);
useEffect(() => {
const screen = window.screen;
let mounted = true;
const onChange = () => {
if (mounted) {
const { orientation } = screen;
console.log(orientation);
if (orientation) {
const { angle, type } = orientation;
setState({ angle, type });
if (type.includes('landscape')) {
setIsLandscape(true);
} else if (type.includes('portrait')) {
setIsLandscape(false);
}
} else if (window.orientation !== undefined) {
setState({
angle:
typeof window.orientation === 'number' ? window.orientation : 0,
type: '',
});
}
}
};
onChange();
return () => {
mounted = false;
};
}, [isLandscape]);
return { state, isLandscape };
}

View File

@@ -34,7 +34,12 @@ 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,
}
); );
const removeLocalStorage = async () => { const removeLocalStorage = async () => {

View File

@@ -8,6 +8,7 @@ export type Player = {
isStartingPlayer: boolean; isStartingPlayer: boolean;
showStartingPlayer: boolean; showStartingPlayer: boolean;
hasLost: boolean; hasLost: boolean;
isSide: boolean;
}; };
export type PlayerSettings = { export type PlayerSettings = {

View File

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

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,8 +2,18 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html {
overflow: hidden;
}
body {
overflow: auto;
}
html, html,
body { body {
height: 100%;
position: relative;
background-color: theme('colors.background.default'); background-color: theme('colors.background.default');
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@@ -2,10 +2,57 @@
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}'],
theme: { theme: {
screens: {
modalSm: '548px',
},
extend: { extend: {
gridTemplateAreas: { gridTemplateAreas: {
onePlayerLandscape: ['player0 player0'], onePlayerLandscape: ['player0 player0'],
@@ -37,45 +84,7 @@ export default {
'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3', 'player0 player4 player4 player4 player4 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)',
},
text: {
primary: '#F5F5F5',
secondary: '#b3b39b',
},
action: {
disabled: '#5E714C',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#00000070',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
},
keyframes: { keyframes: {
fadeOut: { fadeOut: {
'0%': { '0%': {

View File

@@ -1,9 +1,19 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
clientsClaim: true,
skipWaiting: true,
},
}),
],
build: { build: {
minify: 'esbuild', minify: 'esbuild',
rollupOptions: { rollupOptions: {

2231
yarn.lock

File diff suppressed because it is too large Load Diff