Compare commits

...

11 Commits
0.6.5 ... 0.6.9

Author SHA1 Message Date
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
18 changed files with 464 additions and 196 deletions

View File

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

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;

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 { 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 {
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';
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`
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
@@ -21,8 +51,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 +90,16 @@ 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 [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0);
@@ -85,12 +116,10 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
trackMouse: true,
onSwipedDown: (e) => {
e.event.stopPropagation();
console.log(`User DOWN Swiped on player ${player.index}`);
setShowPlayerMenu(true);
},
onSwipedUp: (e) => {
e.event.stopPropagation();
console.log(`User UP Swiped on player ${player.index}`);
setShowPlayerMenu(false);
},
@@ -122,17 +151,26 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
}, [recentDifference, 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;
@@ -167,31 +205,67 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
style={{ rotate: `${calcRotation}deg` }}
{...handlers}
>
{settings.showStartingPlayer &&
player.isStartingPlayer &&
player.showStartingPlayer && (
<StartingPlayerNoticeWrapper
style={{ rotate: `${calcRotation}deg` }}
{!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`,
}}
>
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
>
You start!
</DynamicText>
</StartingPlayerNoticeWrapper>
)}
<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>
</div>
)}
{player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} />
)}
{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}
/>
)}
{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

@@ -6,6 +6,7 @@ import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import {
Cross,
Energy,
Exit,
Experience,
@@ -103,7 +104,14 @@ const PlayerMenu = ({
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>) => {
@@ -121,6 +129,13 @@ const PlayerMenu = ({
const handleResetGame = () => {
resetCurrentGame();
setShowPlayerMenu(false);
setPlaying(false);
setStopPlayerRandomization(false);
};
const handleGoToStart = () => {
goToStart();
setStopPlayerRandomization(false);
};
const toggleFullscreen = () => {
@@ -158,6 +173,14 @@ const PlayerMenu = ({
}}
ref={settingsContainerRef}
>
{settings.showPlayerMenuCog && (
<button
onClick={() => setShowPlayerMenu(false)}
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"
>
<Cross size="16px" className="text-primary-main " />
</button>
)}
<BetterRowContainer>
<TogglesSection>
<ColorPickerButton aria-label="Color picker">
@@ -282,7 +305,7 @@ const PlayerMenu = ({
cursor: 'pointer',
userSelect: 'none',
}}
onClick={goToStart}
onClick={handleGoToStart}
aria-label="Back to start"
>
<Exit size={iconSize} style={{ rotate: '180deg' }} />

View File

@@ -0,0 +1,146 @@
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 (
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,
]);
return (
<PlayersWrapper>
{settings.showStartingPlayer &&
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing && (
<div
className="absolute flex justify-center items-center bg-black bg-opacity-40 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="bg-primary-main px-8 py-2 rounded-2xl opacity-70 text-[5vmax]">
PRESS TO SELECT PLAYER
</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

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

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

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

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

@@ -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}'],
@@ -40,49 +84,7 @@ export default {
'player0 player4 player4 player4 player4 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)',
},
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',
},
},
},
colors: baseColors,
keyframes: {
fadeOut: {
'0%': {