Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Rådberg
23ab7c4e46 wip 2023-10-21 15:36:53 +02:00
49 changed files with 2173 additions and 11393 deletions

View File

@@ -1,12 +1,8 @@
index.html,1711189442688,fa2549e32940c356ac5cee88c8db61076ad62fb4e599858c8e45cfc68cd901c4
manifest.json,1711189442512,7ff5111aa04a42adff3b38924ec467b6f345ed0309dba1486dc9b783b60c2a9d
registerSW.js,1711189442688,5b6445b5215737c53ef0d379c151d57de165a056de2d1c5812ed614f158ebcbd
sw.js,1711189443521,9c09d33ea573bb818864bfad526fa911839637171773eca8e31905458679846d
robots.txt,1711189442512,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.webmanifest,1711189442688,f2bf253209f6e292a6b0dbfa06fb4ac188eb5f2dba568c3ad5511b9ed52c1f51
workbox-3e911b1d.js,1711189443521,d5dbc868a5c07af633d29de7ba3ffe37542aaaabdf33713b4298df31f92f11ff
assets/index-WLCHZTqE.css,1711189442688,877e5ea9bfd3a1ca0e6449e8213da8a3c7717e530370f12669bb5c70dd21e700
favicon.ico,1711189442511,8cefe5adbf00d337d8633fb744b9f2c4914f769b319be4bb7e184b7a4aa17160
logo192.png,1711189442511,3d1e2e6f064d4fd325828f21bb6493ff0dbf2390b0e7d2aba9f2b6def4829799
logo512.png,1711189442511,892a4da1cc5434929a83a71fcbcb0d0d80aa82f44e3c21e9b20ffe9267197133
assets/index-OHs0lOr7.js,1711189442688,aa0dca732cd5b6f621ecb7c6dbcbfdbccde78941cfad954f6626d4ff83040c7f
robots.txt,1693082171694,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.json,1693082171694,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
assets/index-5265c558.css,1693082171837,08c4451946bbdf520fe337edb365417a8bbf91914c018b83866723ef52d57b43
index.html,1693082171837,09e1919fbaaa3a0bf08f43eb46c29136d62a7747b41f8b5d0f4a7ed23337c344
logo192.png,1693082171693,4309255bccbdbb341b5ab88708677e3d43b9e171d2666528ff932295a8257e4e
favicon.ico,1693082171692,48d8c1b9714dbc9bcb012d9c9f04112d229f20e6c889bda588ac159f973e6a8d
logo512.png,1693082171694,92c7c05dc98170596d04f48e5e60eaae9535f409bcaeff129fd98fef8aba9f4e
assets/index-5023e89e.js,1693082171838,8a6177168e95e1ca90e5ad8774252a8a02a9a78765bd329b7deae729c01aedf3

View File

@@ -0,0 +1,20 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
'on':
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
- run: bun install && bun run build && bun run lint
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_LIFE_TRINKET }}'
channelId: live
projectId: life-trinket

View File

@@ -1,58 +0,0 @@
name: Deploy to Firebase Hosting
'on':
push:
tags:
- '*'
jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup bun
uses: oven-sh/setup-bun@v1
- name: Build, lint, and deploy
run: |
bun install
bun run build
bun run lint
- name: Deploy to Firebase Hosting
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_LIFE_TRINKET }}'
channelId: live
projectId: life-trinket
release:
needs: build_and_deploy
runs-on: ubuntu-latest
env:
working-directory: ${{ github.workspace }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: get version
id: version
uses: notiz-dev/github-action-json-property@v0.2.0
with:
path: 'package.json'
prop_path: 'version'
- name: Create Release Note
id: create_release_note
run: echo "Release Note for version ${{ steps.version.outputs.prop }}" > release_note.txt
- name: Create Release
uses: ncipollo/release-action@v1.13.0
with:
bodyFile: release_note.txt
commit: ${{ github.sha }}
tag: '${{ steps.version.outputs.prop }}'
token: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -1,12 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] - 2024-01-13
### Changed
- Styling with Tailwind CSS instead of styled-components

BIN
bun.lockb

Binary file not shown.

1
env.d.ts vendored
View File

@@ -1 +0,0 @@
declare const APP_VERSION: string;

View File

@@ -1,7 +1,7 @@
{
"name": "life-trinket",
"private": true,
"version": "0.8.1",
"version": "0.4.0",
"type": "commonjs",
"engines": {
"node": ">=18",
@@ -13,7 +13,7 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"generate-icons": "npx @svgr/cli src/Icons/svgs",
"deploy": "bun run build && firebase deploy --only hosting"
"deploy": "bun build && firebase deploy --only hosting"
},
"dependencies": {
"@mui/material": "^5.13.6",
@@ -22,31 +22,23 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-screen-wake-lock": "^3.0.2",
"react-swipeable": "^7.0.1",
"react-twc": "^1.3.0",
"zod": "^3.22.4"
"styled-components": "^6.0.7"
},
"devDependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@savvywombat/tailwindcss-grid-areas": "^3.1.0",
"@svgr/cli": "^8.1.0",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.16",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"firebase-tools": "^12.5.2",
"install": "^0.13.0",
"postcss": "^8.4.32",
"prettier": "2.8.8",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.4"
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,12 +1,24 @@
import { createGlobalStyle } from 'styled-components';
import { ThemeProvider } from '@mui/material';
import { LifeTrinket } from './Components/LifeTrinket';
import { theme } from './Data/theme';
import { GlobalSettingsProvider } from './Providers/GlobalSettingsProvider';
import { PlayersProvider } from './Providers/PlayersProvider';
const GlobalStyles = createGlobalStyle`
html,
body {
background-color: ${theme.palette.background.default};
}
#root {
touch-action: manipulation;
}
`;
const App = () => {
return (
<ThemeProvider theme={theme}>
<GlobalStyles />
<PlayersProvider>
<GlobalSettingsProvider>
<LifeTrinket />

View File

@@ -1,51 +1,110 @@
import styled from 'styled-components';
import { css } from 'styled-components';
import { Player, Rotation } from '../../Types/Player';
import { useRef, useState } from 'react';
import { TwcComponentProps, twc } from 'react-twc';
import { OutlinedText } from '../Misc/OutlinedText';
import { decrementTimeoutMs } from '../../Data/constants';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText';
export type RotationDivProps = TwcComponentProps<'div'> & {
$rotation?: number;
};
const CommanderDamageContainer = styled.div<{
$rotation: number;
}>`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
export type RotationSpanProps = TwcComponentProps<'span'> & {
$rotation?: number;
};
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
`;
}
}}
`;
export type RotationButtonProps = TwcComponentProps<'button'> & {
$rotation?: number;
};
const CommanderDamageButton = styled.button<{
$backgroundColor?: string;
$rotation: number;
}>`
display: flex;
flex-grow: 1;
border: none;
height: 10vmin;
width: 50%;
outline: none;
cursor: pointer;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
margin: 0;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 0;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: 6vmax;
height: auto;
`;
}
}}
`;
export const MAX_TAP_MOVE_DISTANCE = 20;
const CommanderDamageTextContainer = styled.div<{
$rotation: number;
}>`
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-variant-numeric: tabular-nums;
pointer-events: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col'
: 'flex-row',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: 270deg;
`;
}
}}
`;
const CommanderDamageButton = twc.button<RotationButtonProps>((props) => [
'flex flex-grow border-none outline-none cursor-pointer m-0 p-0 webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-[6vmax] h-auto'
: 'h-[10vmin] w-1/2',
]);
const PartnerDamageSeperator = styled.div<{
$rotation: number;
}>`
width: 1px;
background-color: rgba(0, 0, 0, 1);
const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
'relative top-1/2 left-1/2 tabular-nums pointer-events-none select-none webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'rotate-[270deg]'
: '',
]);
const PartnerDamageSeparator = twc.div<RotationDivProps>((props) => [
'bg-black',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-full h-px'
: 'w-px',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: auto;
height: 1px;
`;
}
}}
`;
type CommanderDamageButtonComponentProps = {
player: Player;
@@ -56,7 +115,6 @@ type CommanderDamageButtonComponentProps = {
type InputProps = {
opponentIndex: number;
isPartner: boolean;
event: React.PointerEvent<HTMLButtonElement>;
};
export const CommanderDamage = ({
@@ -66,8 +124,12 @@ export const CommanderDamage = ({
}: CommanderDamageButtonComponentProps) => {
const { updatePlayer } = usePlayers();
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [downLongPressed, setDownLongPressed] = useState(false);
const downPositionRef = useRef({ x: 0, y: 0 });
const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false);
const isSide =
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped;
const handleCommanderDamageChange = (
index: number,
@@ -106,47 +168,34 @@ export const CommanderDamage = ({
handleLifeChange(player.lifeTotal - increment);
};
const handleDownInput = ({ opponentIndex, isPartner, event }: InputProps) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
setDownLongPressed(false);
const handleDownInput = ({ opponentIndex, isPartner }: InputProps) => {
setTimeoutFinished(false);
setHasPressedDown(true);
timeoutRef.current = setTimeout(() => {
setDownLongPressed(true);
setTimeoutFinished(true);
handleCommanderDamageChange(opponentIndex, -1, isPartner);
}, decrementTimeoutMs);
};
const handleUpInput = ({ opponentIndex, isPartner, event }: InputProps) => {
if (downLongPressed) {
const handleUpInput = ({ opponentIndex, isPartner }: InputProps) => {
if (!(hasPressedDown && !timeoutFinished)) {
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);
handleCommanderDamageChange(opponentIndex, 1, isPartner);
setHasPressedDown(false);
};
const handleLeaveInput = () => {
setDownLongPressed(true);
setTimeoutFinished(true);
clearTimeout(timeoutRef.current);
setHasPressedDown(false);
};
const opponentIndex = opponent.index;
const fontSize = player.isSide ? '4vmax' : '7vmin';
const fontSize = isSide ? '4vmax' : '7vmin';
const fontWeight = 'bold';
const strokeWidth = player.isSide ? '0.4vmax' : '0.7vmin';
const strokeWidth = isSide ? '0.4vmax' : '0.7vmin';
return (
<CommanderDamageContainer
@@ -157,18 +206,16 @@ export const CommanderDamage = ({
<CommanderDamageButton
key={opponentIndex}
$rotation={player.settings.rotation}
onPointerDown={(e) =>
handleDownInput({ opponentIndex, isPartner: false, event: e })
}
onPointerUp={(e) =>
handleUpInput({ opponentIndex, isPartner: false, event: e })
onPointerDown={() =>
handleDownInput({ opponentIndex, isPartner: false })
}
onPointerUp={() => handleUpInput({ opponentIndex, isPartner: false })}
onPointerLeave={handleLeaveInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}}
$backgroundColor={opponent.color}
aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
>
<CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText
@@ -185,15 +232,15 @@ export const CommanderDamage = ({
{opponent.settings.usePartner && (
<>
<PartnerDamageSeparator $rotation={player.settings.rotation} />
<PartnerDamageSeperator $rotation={player.settings.rotation} />
<CommanderDamageButton
key={opponentIndex}
$rotation={player.settings.rotation}
onPointerDown={(e) =>
handleDownInput({ opponentIndex, isPartner: true, event: e })
onPointerDown={() =>
handleDownInput({ opponentIndex, isPartner: true })
}
onPointerUp={(e) =>
handleUpInput({ opponentIndex, isPartner: true, event: e })
onPointerUp={() =>
handleUpInput({ opponentIndex, isPartner: true })
}
onPointerLeave={handleLeaveInput}
onContextMenu={(
@@ -201,8 +248,8 @@ export const CommanderDamage = ({
) => {
e.preventDefault();
}}
$backgroundColor={opponent.color}
aria-label={`Partner Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
>
<CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText

View File

@@ -1,44 +1,56 @@
import { ReactNode, useRef, useState } from 'react';
import { twc } from 'react-twc';
import styled from 'styled-components';
import { css } from 'styled-components';
import { decrementTimeoutMs } from '../../Data/constants';
import { CounterType, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText';
import { MAX_TAP_MOVE_DISTANCE, RotationDivProps } from './CommanderDamage';
const ExtraCounterContainer = twc.div`
flex
justify-center
items-center
pointer-events-all
flex-grow
const ExtraCounterContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
pointer-events: all;
flex-grow: 1;
`;
const ExtraCounterButton = twc.button`
flex
justify-center
items-center
relative
flex-grow
border-none
outline-none
cursor-pointer
bg-transparent
select-none
h-full
webkit-user-select-none
export const StyledExtraCounterButton = styled.button`
position: relative;
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
height: 100%;
`;
const IconContainer = twc.div<RotationDivProps>((props) => [
'w-auto',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'rotate-[-90deg]'
: '',
]);
const IconContainer = styled.div<{
$rotation: number;
}>`
width: auto;
const TextContainer = twc.div`
absolute
top-1/2
left-1/2
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: -90deg;
`;
}
}}
`;
const TextContainer = styled.div`
position: absolute;
translate: -50%;
top: 50%;
left: 50%;
`;
type ExtraCounterProps = {
@@ -47,7 +59,6 @@ type ExtraCounterProps = {
type: CounterType;
setCounterTotal: (updatedCounterTotal: number, type: CounterType) => void;
rotation: number;
isSide: boolean;
playerIndex: number;
};
@@ -57,13 +68,14 @@ const ExtraCounter = ({
setCounterTotal,
type,
rotation,
isSide,
playerIndex,
}: ExtraCounterProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = 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) => {
if (!counterTotal) {
@@ -73,8 +85,7 @@ const ExtraCounter = ({
setCounterTotal(counterTotal + increment, type);
};
const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
const handleDownInput = () => {
setTimeoutFinished(false);
setHasPressedDown(true);
timeoutRef.current = setTimeout(() => {
@@ -83,23 +94,10 @@ const ExtraCounter = ({
}, decrementTimeoutMs);
};
const handleUpInput = (event: React.PointerEvent<HTMLButtonElement>) => {
const handleUpInput = () => {
if (!(hasPressedDown && !timeoutFinished)) {
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);
handleCountChange(1);
setHasPressedDown(false);
@@ -117,7 +115,7 @@ const ExtraCounter = ({
return (
<ExtraCounterContainer>
<ExtraCounterButton
<StyledExtraCounterButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput}
@@ -138,7 +136,7 @@ const ExtraCounter = ({
</OutlinedText>
</TextContainer>
</IconContainer>
</ExtraCounterButton>
</StyledExtraCounterButton>
</ExtraCounterContainer>
);
};

View File

@@ -1,80 +1,89 @@
import { useEffect, useRef, useState } from 'react';
import { TwcComponentProps, twc } from 'react-twc';
import { useRef, useState } from 'react';
import styled from 'styled-components';
import { css } from 'styled-components';
import { lifeLongPressMultiplier } from '../../Data/constants';
import { Player, Rotation } from '../../Types/Player';
import { MAX_TAP_MOVE_DISTANCE } from './CommanderDamage';
import { checkContrast } from '../../Utils/checkContrast';
type RotationButtonProps = TwcComponentProps<'div'> & {
$align?: string;
$rotation?: number;
};
import { Rotation } from '../../Types/Player';
const LifeCounterButtonTwc = twc.button`
h-full
w-full
flex
font-semibold
bg-transparent
border-none
outline-none
cursor-pointer
justify-center
items-center
select-none
webkit-user-select-none
export const StyledLifeCounterButton = styled.button`
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
`;
const TextContainer = twc.div<RotationButtonProps>((props) => [
'relative',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? props.$align === 'right'
? '-rotate-90 bottom-1/4 top-auto'
: '-rotate-90 top-1/4'
: 'top-auto',
props.$rotation === Rotation.Flipped || props.$rotation === Rotation.Normal
? props.$align === 'right'
? 'left-1/4'
: 'right-1/4'
: '',
]);
const TextContainer = styled.div<{
$align?: string;
$rotation: number;
}>`
position: relative;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
if (props.$align === 'right') {
return css`
rotate: -90deg;
bottom: 25%;
top: auto;
`;
}
return css`
rotate: -90deg;
top: 25%;
`;
}
if (props.$align === 'right') {
return css`
left: 25%;
`;
}
return css`
right: 25%;
`;
}}
`;
type LifeCounterButtonProps = {
player: Player;
lifeTotal: number;
setLifeTotal: (lifeTotal: number) => void;
rotation: number;
operation: 'add' | 'subtract';
increment: number;
};
const LifeCounterButton = ({
player,
lifeTotal,
setLifeTotal,
rotation,
operation,
increment,
}: LifeCounterButtonProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = 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) => {
setLifeTotal(player.lifeTotal + increment);
setLifeTotal(lifeTotal + increment);
};
const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
const handleDownInput = () => {
setTimeoutFinished(false);
setHasPressedDown(true);
timeoutRef.current = setTimeout(() => {
@@ -83,23 +92,10 @@ const LifeCounterButton = ({
}, 500);
};
const handleUpInput = (event: React.PointerEvent<HTMLButtonElement>) => {
const handleUpInput = () => {
if (!(hasPressedDown && !timeoutFinished)) {
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);
handleLifeChange(operation === 'add' ? 1 : -1);
setHasPressedDown(false);
@@ -112,13 +108,12 @@ const LifeCounterButton = ({
};
const fontSize =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
rotation === Rotation.SideFlipped || rotation === Rotation.Side
? '8vmax'
: '12vmin';
return (
<LifeCounterButtonTwc
<StyledLifeCounterButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput}
@@ -129,15 +124,12 @@ const LifeCounterButton = ({
aria-label={`${operation === 'add' ? 'Add' : 'Subtract'} life`}
>
<TextContainer
$rotation={player.settings.rotation}
$rotation={rotation}
$align={operation === 'add' ? 'right' : 'left'}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark
data-[contrast=light]:text-icons-light"
>
{operation === 'add' ? '\u002B' : '\u2212'}
</TextContainer>
</LifeCounterButtonTwc>
</StyledLifeCounterButton>
);
};

View File

@@ -1,15 +1,43 @@
import { twc } from 'react-twc';
import styled, { css } from 'styled-components';
import { Skull } from '../../Icons/generated';
import { Rotation } from '../../Types/Player';
import { RotationDivProps } from './CommanderDamage';
const LoseButton = twc.div<RotationDivProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-interface-loseButton-background rounded-lg select-none z-[1] webkit-user-select-none py-2 px-4 ',
export const LoseButton = styled.button<{ $rotation: Rotation }>`
position: absolute;
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
top: 25%;
right: 15%;
background-color: #43434380;
border-radius: 8px;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
z-index: 1;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `left-[21%]`
: 'top-[21%]',
]);
${(props) => {
if (props.$rotation === Rotation.SideFlipped) {
return css`
right: auto;
top: 15%;
left: 27%;
rotate: ${props.$rotation}deg;
`;
} else if (props.$rotation === Rotation.Side) {
return css`
right: auto;
top: 15%;
left: 27%;
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
type LoseButtonProps = {
onClick: () => void;
@@ -17,23 +45,9 @@ type LoseButtonProps = {
};
export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
const calcRotation =
rotation === Rotation.SideFlipped
? rotation
: rotation === Rotation.Side
? rotation - 180
: rotation === Rotation.Flipped
? rotation - 180
: rotation;
return (
<LoseButton
$rotation={rotation}
onClick={onClick}
aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }}
>
<Skull size="8vmin" color="black" opacity={0.5} />
<LoseButton $rotation={rotation} onClick={onClick} aria-label={`Lose Game`}>
<Skull size="5vmin" color="black" opacity={0.5} />
</LoseButton>
);
};

View File

@@ -0,0 +1,53 @@
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import { Cog } from '../../Icons/generated';
export const StyledSettingsButton = styled.button<{ $rotation: Rotation }>`
position: absolute;
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
top: 25%;
right: 1vmax;
background-color: transparent;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
z-index: 1;
${(props) => {
if (
props.$rotation === Rotation.Side ||
props.$rotation === Rotation.SideFlipped
) {
return css`
right: auto;
top: 1vmax;
left: 27%;
`;
}
}}
`;
type SettingsButtonProps = {
onClick: () => void;
rotation: Rotation;
};
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => {
return (
<StyledSettingsButton
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<Cog size="5vmin" color="black" opacity="0.3" />
</StyledSettingsButton>
);
};
export default SettingsButton;

View File

@@ -1,13 +1,27 @@
import { twc } from 'react-twc';
import { Player, Rotation } from '../../Types/Player';
import { CommanderDamage, RotationDivProps } from '../Buttons/CommanderDamage';
import styled from 'styled-components';
import { css } from 'styled-components';
import { CommanderDamage } from '../Buttons/CommanderDamage';
const CommanderDamageGrid = twc.div<RotationDivProps>((props) => [
'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col h-full w-auto'
: 'flex-row w-full',
]);
const CommanderDamageGrid = styled.div<{ $rotation: number }>`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
height: 100%;
width: auto;
`;
}
}}
`;
type CommanderDamageBarProps = {
opponents: Player[];

View File

@@ -0,0 +1,59 @@
import styled from 'styled-components';
import { usePlayers } from '../../Hooks/usePlayers';
import LifeCounter from '../LifeCounter/LifeCounter';
export const CountersWrapper = styled.div`
width: 100%;
height: 100%;
background-color: black;
`;
export const CountersGrid = styled.div<{ $gridTemplateAreas: string }>`
display: grid;
gap: 4px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
height: 100%;
grid-template-areas: ${({ $gridTemplateAreas }) => $gridTemplateAreas};
`;
export const GridItemContainer = styled.div<{
$gridArea: string;
}>`
display: flex;
justify-content: center;
align-items: center;
grid-area: ${(props) => props.$gridArea};
`;
type CountersProps = {
gridAreas: string;
};
const Counters = ({ gridAreas }: CountersProps) => {
const { players } = usePlayers();
return (
<CountersWrapper>
<CountersGrid $gridTemplateAreas={gridAreas}>
{players.map((player) => {
return (
<GridItemContainer
key={player.index}
$gridArea={`player${player.index}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</GridItemContainer>
);
})}
</CountersGrid>
</CountersWrapper>
);
};
export default Counters;

View File

@@ -1,5 +1,8 @@
import { twc } from 'react-twc';
import { usePlayers } from '../../Hooks/usePlayers';
import { CounterType, Player } from '../../Types/Player';
import ExtraCounter from '../Buttons/ExtraCounter';
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import {
CommanderTax,
Energy,
@@ -7,25 +10,52 @@ import {
PartnerTax,
Poison,
} from '../../Icons/generated';
import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter';
import { useEffect, useState } from 'react';
import { checkContrast } from '../../Utils/checkContrast';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { GameFormat } from '../../Types/Settings';
const Container = twc.div<RotationDivProps>((props) => [
'flex',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'h-full w-[8vmax]'
: 'h-[20vmin] w-full',
]);
const Container = styled.div<{ $rotation: Rotation }>`
width: 100%;
height: 20vmin;
display: flex;
export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [
'flex absolute flex-row flex-grow pointer-events-none overflow-x-scroll overflow-y-hidden',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse h-full w-auto bottom-auto right-0'
: 'w-full bottom-0',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
height: 100%;
width: 9.3vmax;
`;
}
}}
`;
export const ExtraCountersGrid = styled.div<{ $rotation: Rotation }>`
display: flex;
position: absolute;
width: 100%;
flex-direction: row;
flex-grow: 1;
bottom: 0;
pointer-events: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
height: 100%;
width: auto;
bottom: auto;
right: -6px;
`;
}
}}
`;
type ExtraCountersBarProps = {
player: Player;
@@ -33,17 +63,7 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers();
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(player.color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [player.color]);
const { initialGameSettings } = useGlobalSettings();
const handleCounterChange = (
updatedCounterTotal: number,
@@ -100,26 +120,22 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
return null;
}
const isTwoHeadedGiant =
initialGameSettings?.gameFormat === GameFormat.TwoHeadedGiant;
return (
<Container $rotation={player.settings.rotation}>
<ExtraCountersGrid $rotation={player.settings.rotation}>
{useCommanderDamage && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={
<CommanderTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
Icon={<CommanderTax size={iconSize} opacity="0.5" color="black" />}
type={CounterType.CommanderTax}
counterTotal={
player.extraCounters?.find(
(counter) => counter.type === 'commanderTax'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -127,20 +143,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{Boolean(useCommanderDamage && usePartner) && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={
<PartnerTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
Icon={<PartnerTax size={iconSize} opacity="0.5" color="black" />}
type={CounterType.PartnerTax}
counterTotal={
player.extraCounters?.find(
(counter) => counter.type === 'partnerTax'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -148,19 +157,12 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{usePoison && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={
<Poison
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
Icon={<Poison size={iconSize} opacity="0.5" color="black" />}
type={CounterType.Poison}
counterTotal={
player.extraCounters?.find((counter) => counter.type === 'poison')
?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -168,19 +170,12 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useEnergy && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={
<Energy
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
Icon={<Energy size={iconSize} opacity="0.5" color="black" />}
type={CounterType.Energy}
counterTotal={
player.extraCounters?.find((counter) => counter.type === 'energy')
?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -188,20 +183,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useExperience && (
<ExtraCounter
rotation={player.settings.rotation}
Icon={
<Experience
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
Icon={<Experience size={iconSize} opacity="0.5" color="black" />}
type={CounterType.Experience}
counterTotal={
player.extraCounters?.find(
(counter) => counter.type === 'experience'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>

View File

@@ -1,43 +1,101 @@
import { useEffect, useRef, useState } from 'react';
import { twc } from 'react-twc';
import styled, { css, keyframes } from 'styled-components';
import { Player, Rotation } from '../../Types/Player';
import {
RotationDivProps,
RotationSpanProps,
} from '../Buttons/CommanderDamage';
import LifeCounterButton from '../Buttons/LifeCounterButton';
import { OutlinedText } from '../Misc/OutlinedText';
const LifeContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow relative w-full h-full justify-between items-center',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse'
: 'flex-row',
]);
const LifeCountainer = styled.div<{
$rotation: Rotation;
}>`
position: relative;
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
height: 100%;
justify-content: space-between;
align-items: center;
const LifeCounterTextContainer = twc.div<RotationDivProps>((props) => [
'absolute m-0 p-0 pointer-events-none select-none webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-full h-2/3'
: 'w-2/3 h-full',
]);
const TextWrapper = twc.div`
flex
absolute
justify-center
items-center
w-full
h-full
z-[-1]
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
`;
}
}}
`;
const RecentDifference = twc.div<RotationSpanProps>((props) => [
'absolute min-w-[20vmin] drop-shadow-none text-center bg-interface-recentDifference-background tabular-nums rounded-full p-[6px 12px] text-[8vmin] text-interface-recentDifference-text animate-fadeOut',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'top-1/3 translate-x-1/4 translate-y-1/2 rotate-[270deg]'
: 'top-1/4 left-[50%] -translate-x-1/2',
]);
const LifeCounterTextContainer = styled.div<{
$rotation: Rotation;
}>`
position: absolute;
width: 60%;
height: 100%;
margin: 0;
padding: 0;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: 100%;
height: 60%;
`;
}
}}
`;
const TextWrapper = styled.div`
display: flex;
position: absolute;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: -1;
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
33% {
opacity: 0.6;
}
100% {
opacity: 0;
}
`;
export const RecentDifference = styled.span`
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 15vmin;
text-shadow: none;
text-align: center;
background-color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
border-radius: 10vmin;
padding: 5px 10px;
font-size: 8vmin;
color: #333333;
animation: ${fadeOut} 3s 1s ease-out forwards;
`;
type HealthProps = {
player: Player;
@@ -49,13 +107,28 @@ type HealthProps = {
const Health = ({
player,
rotation,
handleLifeChange,
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;
@@ -84,28 +157,28 @@ const Health = ({
}, [textContainerRef]);
const calculateFontSize = (container: HTMLDivElement) => {
const widthRatio = player.isSide
? container.clientHeight
: container.clientWidth;
const isSide =
rotation === Rotation.SideFlipped || rotation === Rotation.Side;
const heightRatio = player.isSide
? container.clientWidth
: container.clientHeight;
const widthRatio = isSide ? container.clientHeight : container.clientWidth;
const heightRatio = isSide ? container.clientWidth : container.clientHeight;
const minRatio = Math.min(widthRatio, heightRatio);
const heightIs40PercentSmaller = heightRatio > widthRatio * 0.6;
const heightIsLarger = heightRatio > widthRatio;
const scaleFactor = heightIs40PercentSmaller ? 0.8 : 1;
const scaleFactor = heightIsLarger ? 0.8 : 1;
return minRatio * scaleFactor * 1;
};
return (
<LifeContainer $rotation={player.settings.rotation}>
<LifeCountainer $rotation={player.settings.rotation}>
<LifeCounterButton
player={player}
lifeTotal={player.lifeTotal}
setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="subtract"
increment={-1}
/>
@@ -122,10 +195,7 @@ const Health = ({
{player.lifeTotal}
</OutlinedText>
{recentDifference !== 0 && (
<RecentDifference
key={differenceKey}
$rotation={player.settings.rotation}
>
<RecentDifference key={differenceKey}>
{recentDifference > 0 ? '+' : ''}
{recentDifference}
</RecentDifference>
@@ -133,12 +203,13 @@ const Health = ({
</LifeCounterTextContainer>
</TextWrapper>
<LifeCounterButton
player={player}
lifeTotal={player.lifeTotal}
setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="add"
increment={1}
/>
</LifeContainer>
</LifeCountainer>
);
};

View File

@@ -1,83 +1,137 @@
import { useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc';
import { baseColors } from '../../../tailwind.config';
import { useAnalytics } from '../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Cog } from '../../Icons/generated';
import { useEffect, useState } from 'react';
import styled, { css, keyframes } from 'styled-components';
import { theme } from '../../Data/theme';
import { Player, Rotation } from '../../Types/Player';
import { checkContrast } from '../../Utils/checkContrast';
import {
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar';
import { Paragraph } from '../Misc/TextComponents';
import PlayerMenu from '../Players/PlayerMenu';
import PlayerMenu from '../PlayerMenu/PlayerMenu';
import Health from './Health';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none',
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped
? `right-auto top-[1vmax] left-[27%]`
: 'top-1/4 right-[1vmax]',
]);
type SettingsButtonProps = {
onClick: () => void;
rotation: Rotation;
color: string;
};
const SettingsButton = ({ onClick, rotation, color }: SettingsButtonProps) => {
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
const LifeCounterContentWrapper = styled.div<{
$backgroundColor: string;
}>`
position: relative;
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
@media (orientation: landscape) {
max-width: 100vmax;
max-height: 100vmin;
}
}, [color]);
return (
<SettingsButtonTwc
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<Cog
size="5vmin"
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
</SettingsButtonTwc>
);
};
overflow: hidden;
`;
const LifeCounterContentWrapper = twc.div`
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
const LifeCounterWrapper = styled.div<{
$rotation: Rotation;
}>`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [
'relative flex items-center w-full h-full z-[1]',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `flex-row`
: `flex-col`,
]);
z-index: 1;
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
? `rotate-[${props.$rotation - 90}deg]`
: '',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: row;
rotate: ${props.$rotation - 90}deg;
`;
}
const DynamicText = twc.div`text-[8vmin] whitespace-nowrap`;
return css`
flex-direction: column;
rotate: ${props.$rotation}deg;
`;
}}
`;
const PlayerNoticeWrapper = styled.div<{
$rotation: Rotation;
$backgroundColor: string;
}>`
z-index: 1;
display: flex;
position: absolute;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: ${(props) => props.$backgroundColor};
pointer-events: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 90}deg;
`;
}
}}
`;
const DynamicText = styled.div<{ $rotation: Rotation }>`
font-size: 8vmin;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
33% {
opacity: 0.6;
}
100% {
opacity: 0;
}
`;
export const RecentDifference = styled.span`
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
text-shadow: none;
background-color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
border-radius: 50%;
padding: 5px 10px;
font-size: 8vmin;
color: #333333;
animation: ${fadeOut} 3s 1s ease-out forwards;
`;
const hasCommanderDamageReached21 = (player: Player) => {
const commanderDamageTotals = player.commanderDamage.map(
@@ -109,103 +163,36 @@ 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, playing, setPlaying, stopPlayerRandomization } =
useGlobalSettings();
const playingTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const recentDifferenceTimerRef = useRef<NodeJS.Timeout | undefined>(
undefined
);
const { settings } = useGlobalSettings();
const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0);
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();
setShowPlayerMenu(true);
},
onSwipedUp: (e) => {
e.event.stopPropagation();
setShowPlayerMenu(false);
},
swipeDuration: 500,
onSwiping: (e) => e.event.stopPropagation(),
rotationAngle,
});
const analytics = useAnalytics();
useEffect(() => {
if (recentDifference === 0) {
clearTimeout(recentDifferenceTimerRef.current);
return;
}
recentDifferenceTimerRef.current = setTimeout(() => {
analytics.trackEvent('life_changed', {
lifeChangedAmount: recentDifference,
});
const timer = setTimeout(() => {
setRecentDifference(0);
}, RECENT_DIFFERENCE_TTL);
}, 3_000);
return () => {
clearTimeout(recentDifferenceTimerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
return () => clearTimeout(timer);
}, [recentDifference]);
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
if (document.body.clientWidth > document.body.clientHeight)
setIsLandscape(true);
else setIsLandscape(false);
return () => {
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
});
if (player.showStartingPlayer) {
const playingTimer = setTimeout(() => {
localStorage.setItem('playing', 'true');
player.showStartingPlayer = false;
updatePlayer(player);
}, 3_000);
resizeObserver.observe(document.body);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [document.body.clientHeight, document.body.clientWidth]);
useEffect(() => {
if (
player.isStartingPlayer &&
((!playing &&
settings.useRandomStartingPlayerInterval &&
stopPlayerRandomization) ||
(!settings.useRandomStartingPlayerInterval && !playing))
) {
playingTimerRef.current = setTimeout(() => {
setPlaying(true);
}, 10_000);
return () => clearTimeout(playingTimer);
}
return () => clearTimeout(playingTimerRef.current);
}, [
player.isStartingPlayer,
playing,
setPlaying,
settings.useRandomStartingPlayerInterval,
stopPlayerRandomization,
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [player.showStartingPlayer]);
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side;
@@ -221,76 +208,26 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
updatePlayer(updatedPlayer);
};
const calcRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 90
: player.settings.rotation;
const calcTextRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
const amountOfPlayers = opponents.length + 1;
return (
<LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper
<LifeCounterContentWrapper $backgroundColor={player.color}>
<LifeCounterWrapper $rotation={player.settings.rotation}>
{settings.showStartingPlayer &&
player.isStartingPlayer &&
player.showStartingPlayer && (
<PlayerNoticeWrapper
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
{...handlers}
$backgroundColor={theme.palette.primary.main}
>
{amountOfPlayers > 1 &&
!playing &&
settings.showStartingPlayer &&
player.isStartingPlayer && (
<div
className="z-20 flex absolute w-full h-full justify-center items-center select-none cursor-pointer webkit-user-select-none"
style={{
rotate: `${calcRotation}deg`,
backgroundImage:
stopPlayerRandomization ||
!settings.useRandomStartingPlayerInterval
? `radial-gradient(circle at center, ${player.color}, ${baseColors.primary.main})`
: 'none',
}}
onClick={() => {
clearTimeout(playingTimerRef.current);
setPlaying(true);
}}
>
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
>
<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 $rotation={player.settings.rotation}>
You start!
</DynamicText>
</div>
</PlayerNoticeWrapper>
)}
{player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} />
)}
{amountOfPlayers > 1 &&
settings.showStartingPlayer &&
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing && (
<div
className="flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none z-10"
style={{ backgroundColor: player.color }}
<PlayerNoticeWrapper
$rotation={player.settings.rotation}
$backgroundColor={'#00000070'}
/>
)}
<CommanderDamageBar
@@ -299,15 +236,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
key={player.index}
handleLifeChange={handleLifeChange}
/>
{settings.showPlayerMenuCog && (
<SettingsButton
onClick={() => {
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}
color={player.color}
/>
)}
{playerCanLose(player) && (
<LoseGameButton
rotation={player.settings.rotation}
@@ -322,13 +256,11 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange}
/>
<ExtraCountersBar player={player} />
<PlayerMenu
isShown={showPlayerMenu}
player={player}
setShowPlayerMenu={setShowPlayerMenu}
/>
</LifeCounterWrapper>
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)}
</LifeCounterContentWrapper>
);
};

View File

@@ -1,37 +1,47 @@
import { twc } from 'react-twc';
import { useGlobalSettings } from '../Hooks/useGlobalSettings';
import { Play } from './Views/Play';
import styled from 'styled-components';
import Play from './Views/Play';
import StartMenu from './Views/StartMenu/StartMenu';
import { useGlobalSettings } from '../Hooks/useGlobalSettings';
const StartWrapper = twc.div`max-w-fit max-h-fit`;
const StartWrapper = styled.div`
max-width: fit-content;
max-height: fit-content;
`;
const PlayWrapper = twc.div`relative z-0 max-w-fit max-h-fit portrait:rotate-90`;
const PlayWrapper = styled.div`
position: relative;
z-index: 0;
max-width: fit-content;
max-height: fit-content;
@media (orientation: portrait) {
rotate: 90deg;
}
`;
const EmergencyResetButton = () => {
const { goToStart } = useGlobalSettings();
const EmergencyResetButton = twc.button`w-[100dvmax] h-[100dvmin] absolute top-0 z-[-1] bg-background-default`;
const Paragraph = twc.p`text-[4vmax] text-text-secondary`;
return (
<EmergencyResetButton onClick={goToStart}>
<Paragraph>If you can see this, something is wrong.</Paragraph>
<Paragraph>Press screen to go to start.</Paragraph>
<br />
<Paragraph>If the issue persists, please inform me.</Paragraph>
</EmergencyResetButton>
);
};
const EmergencyResetButton = styled.button`
width: 100vmax;
height: 100vmin;
font-size: 4vmax;
position: absolute;
top: 0;
z-index: -1;
background-color: #4e6815;
`;
export const LifeTrinket = () => {
const { showPlay, initialGameSettings } = useGlobalSettings();
const { showPlay, goToStart, initialGameSettings } = useGlobalSettings();
return (
<>
{showPlay && initialGameSettings ? (
<PlayWrapper>
<Play />
<EmergencyResetButton />
<Play gridAreas={initialGameSettings?.gridAreas} />
<EmergencyResetButton onClick={goToStart}>
<p>If you can see this, something is wrong.</p>
<p>Press screen to go to start.</p>
<br />
<p>If the issue persists, please inform me.</p>
</EmergencyResetButton>
</PlayWrapper>
) : (
<StartWrapper>

View File

@@ -1,10 +1,21 @@
import { Modal } from '@mui/material';
import { twc } from 'react-twc';
import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
import { Cross } from '../../Icons/generated';
import { theme } from '../../Data/theme';
import styled from 'styled-components';
export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`;
export const ModalWrapper = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80vw;
height: 85vh;
background-color: ${theme.palette.background.default};
padding: 1rem;
overflow: scroll;
border-radius: 1rem;
color: ${theme.palette.text.primary};
border: none;
`;
type InfoModalProps = {
isOpen: boolean;
@@ -13,44 +24,29 @@ type InfoModalProps = {
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
return (
<Modal
open={isOpen}
onClose={closeModal}
style={{ display: 'flex', justifyContent: 'center' }}
>
<>
<div className="flex justify-center items-center relative w-full max-w-[532px]">
<button
onClick={closeModal}
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"
>
<Cross size="16px" className="text-text-primary " />
</button>
</div>
<Modal open={isOpen} onClose={closeModal}>
<ModalWrapper>
<div>
<h2 className="text-2xl text-center mb-4">📋 Usage Guide</h2>
<Separator height="1px" />
<Paragraph className="my-4">
<h2 style={{ textAlign: 'center' }}>📋 Usage Guide</h2>
<p>
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">
</p>
<h3>Life counter</h3>
<ul>
<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>.
<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">
<h3>Commander damage and other counters</h3>
<ul>
<li>
<strong>Tap</strong> on the counter to add{' '}
<strong>1 counter</strong>.
@@ -61,38 +57,40 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
</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>
<h3>Other</h3>
<p>
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.
</p>
<p>
Tap on the button to mark that player as lost, dimming their player
card.
</p>
</div>
<div className="text-center mt-4">
Visit my{' '}
<br />
<div
style={{
textAlign: 'center',
marginTop: '1rem',
}}
>
Visit my
<a
href="https://github.com/Vikeo/LifeTrinket"
target="_blank"
className="text-text-secondary underline"
style={{
textDecoration: 'none',
color: theme.palette.primary.light,
}}
>
GitHub
</a>{' '}
{' '}
GitHub{' '}
</a>
for more info about this web app.
</div>
</ModalWrapper>
</>
</Modal>
);
};

View File

@@ -1,29 +1,57 @@
import styled, { css } from 'styled-components';
import { theme } from '../../Data/theme';
import { Rotation } from '../../Types/Player';
import { twc } from 'react-twc';
//TODO Create provider for this
import tailwindConfig from './../../../tailwind.config';
import resolveConfig from 'tailwindcss/resolveConfig';
const fullConfig = resolveConfig(tailwindConfig);
const Container = twc.div`
flex
relative
w-full
h-full
items-center
justify-center
const Container = styled.div`
display: flex;
position: relative;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`;
const CenteredText = twc.div`absolute select-none text-common-black text-[6vmin] stroke-common-white
webkit-user-select-none tabular-nums`;
const CenteredText = styled.div<{
strokeWidth?: string;
strokeColor?: string;
fillColor?: string;
fontSize?: string;
fontWeight?: string;
$rotation?: Rotation;
}>`
position: absolute;
font-weight: ${(props) => props.fontWeight || ''};
font-variant-numeric: tabular-nums;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
const CenteredTextOutline = twc.span`
absolute
left-0
stroke-none
pointer-events-none
color: ${(props) => props.fillColor || theme.palette.common.black};
font-size: ${(props) => props.fontSize || '6vmin'};
-webkit-text-stroke: ${(props) => props.strokeWidth || '1vmin'}${(props) => props.strokeColor || theme.palette.common.white};
-webkit-text-fill-color: ${(props) =>
props.fillColor || theme.palette.common.black};
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: 270deg;
`;
}
}}
`;
const CenteredTextOutline = styled.span`
position: absolute;
left: 0;
-webkit-text-stroke: 0;
pointer-events: none;
`;
type OutlinedTextProps = {
@@ -45,33 +73,18 @@ export const OutlinedText: React.FC<OutlinedTextProps> = ({
fillColor,
rotation,
}) => {
const calcRotation =
rotation === Rotation.Side
? rotation - 180
: rotation === Rotation.SideFlipped
? rotation
: 0;
return (
<Container>
<CenteredText
style={{
fontSize,
fontWeight,
strokeWidth: strokeWidth || '1vmin',
color: fillColor || fullConfig.theme.colors.common.black,
WebkitTextStroke: `${strokeWidth || '1vmin'} ${
strokeColor || fullConfig.theme.colors.common.white
}`,
WebkitTextFillColor:
fillColor || fullConfig.theme.colors.common.black,
rotate: `${calcRotation}deg`,
}}
fontSize={fontSize}
fontWeight={fontWeight}
strokeWidth={strokeWidth}
strokeColor={strokeColor}
fillColor={fillColor}
$rotation={rotation}
>
{children}
<CenteredTextOutline aria-hidden style={{ WebkitTextStroke: 0 }}>
{children}
</CenteredTextOutline>
<CenteredTextOutline aria-hidden>{children}</CenteredTextOutline>
</CenteredText>
</Container>
);

View File

@@ -1,3 +1,13 @@
import styled from 'styled-components';
import { Spacer } from './Spacer';
const SeparatorContainer = styled.div<{ width?: string; height?: string }>`
width: ${(props) => props.width};
height: ${(props) => props.height};
background-color: #00000025;
border-radius: 50px;
`;
export const Separator = ({
width = '100%',
height = '100%',
@@ -6,9 +16,10 @@ export const Separator = ({
height?: string;
}) => {
return (
<div
className={`bg-common-white bg-opacity-30 rounded-full mt-2 mb-2`}
style={{ width, height }}
/>
<>
<Spacer height="0.5rem" />
<SeparatorContainer width={width} height={height} />
<Spacer height="0.5rem" />
</>
);
};

View File

@@ -1,19 +1,38 @@
import { Button, FormLabel, Modal, Switch } from '@mui/material';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { ModalWrapper } from './InfoModal';
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { theme } from '../../Data/theme';
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 mb-2`;
const SettingContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;
const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`;
const ToggleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Container = twc.div`flex flex-col items-center w-full`;
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
`;
const Description = twc.p`mr-16 text-xs text-left text-text-secondary`;
const Description = styled.p`
margin-top: -0.25rem;
margin-right: 3.5rem;
font-size: 0.8rem;
text-align: left;
color: ${theme.palette.text.secondary};
`;
type SettingsModalProps = {
isOpen: boolean;
@@ -22,69 +41,12 @@ type SettingsModalProps = {
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA } = useGlobalSettings();
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
useEffect(() => {
if (!isOpen) {
return;
}
async function checkIfLatestVersion() {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
/* @ts-expect-error is defined in vite.config.ts*/
Authorization: `Bearer ${REPO_READ_ACCESS_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setNewVersion(undefined);
setIsLatestVersion(false);
return;
}
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) {
setIsLatestVersion(true);
return;
}
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
checkIfLatestVersion();
}, [isOpen]);
return (
<Modal
open={isOpen}
onClose={closeModal}
className="w-full flex justify-center"
>
<>
<div className="flex justify-center items-center relative w-full max-w-[532px]">
<button
onClick={closeModal}
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"
>
<Cross size="16px" className="text-text-primary " />
</button>
</div>
<Modal open={isOpen} onClose={closeModal}>
<ModalWrapper>
<Container>
<h2 className="text-center text-2xl mb-2"> Settings </h2>
<Separator height="1px" />
<h2 style={{ textAlign: 'center' }}> Settings </h2>
<SettingContainer>
<ToggleContainer>
<FormLabel>Show Start Player</FormLabel>
@@ -103,60 +65,19 @@ 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>
<Switch
checked={settings.keepAwake}
onChange={() => {
setSettings({
...settings,
keepAwake: !settings.keepAwake,
});
setSettings({ ...settings, keepAwake: !settings.keepAwake });
}}
/>
</ToggleContainer>
<Description>
Will prevent device from going to sleep while this app is open
if this is enabled.
Will prevent device from going to sleep while this app is open if
this is enabled.
</Description>
</SettingContainer>
<SettingContainer>
@@ -179,7 +100,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer>
{!isPWA && (
<>
<Separator height="1px" />
<Separator height="2px" />
<SettingContainer>
<ToggleContainer>
<Paragraph>
@@ -189,50 +110,24 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
normal app!
</Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will
be automatically hidden.
<Description>
If you do, this app will work offline and the toolbar will be
automatically hidden.
</Description>
</SettingContainer>
</>
)}
<Separator height="1px" />
<Separator height="2px" />
<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>
)}
<Paragraph>Version: 0.4.0</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" />
<Separator height="2px" />
<Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
<Button variant="contained" onClick={closeModal}>
Save and Close
</Button>
</Container>
</ModalWrapper>
</>
</Modal>
);
};

View File

@@ -0,0 +1,6 @@
import styled from 'styled-components';
export const Spacer = styled.div<{ width?: string; height?: string }>`
width: ${(props) => props.width};
height: ${(props) => props.height};
`;

View File

@@ -1,29 +1,42 @@
import { Button, Drawer } from '@mui/material';
import { useState } from 'react';
import styled from 'styled-components';
import { theme } from '../../Data/theme';
import { BuyMeCoffee, KoFi } from '../../Icons/generated/Support';
import { Paragraph } from './TextComponents';
import LittleGuy from '../../Icons/generated/LittleGuy';
import { useAnalytics } from '../../Hooks/useAnalytics';
import { twc } from 'react-twc';
const SupportContainer = twc.div`flex flex-col items-center justify-center gap-4 mt-4 mb-4`;
// import { ButtonBase } from '@mui/material';
const SupportButton = twc.button`
flex
flex-row
items-center
justify-left
border-none
cursor-pointer
bg-primary-main
rounded-md
w-10/12
mx-4
px-4
py-2
transition-colors duration-200 ease-in-out
shadow-[1px_2px_4px_0px_rgba(0,0,0,0.3)]
hover:bg-primary-dark
const SupportContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
margin: 16px 0;
`;
const SupportButton = styled.button`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border: none;
background-color: transparent;
cursor: pointer;
padding: 0;
margin: 0;
background-color: ${theme.palette.primary.main};
border-radius: 4px;
margin: 0 1rem;
padding: 0 1rem;
transition: background-color 0.2s ease-in-out;
box-shadow: 1px 2px 4px 0px rgba(0, 0, 0, 0.3);
&:hover {
background-color: ${theme.palette.primary.dark};
}
`;
export const SupportMe = () => {
@@ -74,7 +87,13 @@ export const SupportMe = () => {
<LittleGuy
height={'4rem'}
width={'2.5rem'}
className="pointer-events-none absolute top-10 right-0 text-text-primary"
style={{
pointerEvents: 'none',
position: 'absolute',
top: '2.5rem',
right: '0',
color: theme.palette.text.primary,
}}
/>
<Drawer
@@ -85,12 +104,22 @@ export const SupportMe = () => {
>
<SupportContainer>
<SupportButton onClick={handleOpenBuyMeCoffee}>
<BuyMeCoffee height="1.5rem" width="1.5rem" className="mr-2" />
<Paragraph className="text-xs">Buy him a tea</Paragraph>
<BuyMeCoffee
height={'1.5rem'}
width={'1.5rem'}
style={{ marginRight: '0.5rem' }}
/>
<Paragraph style={{ fontSize: '0.7rem' }}>Buy him a tea</Paragraph>
</SupportButton>
<SupportButton onClick={handleOpenKoFi}>
<KoFi height="1.5rem" width="1.5rem" className="mr-2" />
<Paragraph className="text-xs">Buy him a ko-fi</Paragraph>
<KoFi
height={'1.5rem'}
width={'1.5rem'}
style={{ marginRight: '0.5rem' }}
/>
<Paragraph style={{ fontSize: '0.7rem' }}>
Buy him a ko-fi
</Paragraph>
</SupportButton>
</SupportContainer>
</Drawer>

View File

@@ -1,6 +1,11 @@
import { twc } from 'react-twc';
import styled from 'styled-components';
import { theme } from '../../Data/theme';
export const Paragraph = twc.p`text-text-primary;`;
export const Paragraph = styled.p`
color: ${theme.palette.text.primary};
`;
// eslint-disable-next-line react-refresh/only-export-components
export const H1 = twc.h1`text-text-primary;`;
export const H1 = styled.h1`
color: ${theme.palette.text.primary};
`;

View File

@@ -0,0 +1,455 @@
import { Button, Checkbox } from '@mui/material';
import styled, { css } from 'styled-components';
import { Player, Rotation } from '../../Types/Player';
import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import {
PartnerTax,
Poison,
Energy,
Experience,
Exit,
FullscreenOff,
FullscreenOn,
Cross,
ResetGame,
} from '../../Icons/generated';
import { useRef } from 'react';
import { Spacer } from '../Misc/Spacer';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
const SettingsContainer = styled.div<{
$rotation: Rotation;
}>`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: 100%;
width: 100%;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
height: 100%;
width: 100%;
`;
}
}}
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const BetterRowContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
height: 100%;
justify-content: end;
align-items: stretch;
`;
const TogglesSection = styled.div`
display: flex;
position: relative;
flex-direction: row;
gap: 0.5rem;
justify-content: space-evenly;
`;
const ButtonsSections = styled.div`
display: flex;
max-width: 100%;
gap: 1rem;
justify-content: space-between;
padding: 3% 3%;
align-items: center;
`;
const ColorPicker = styled.input`
position: absolute;
top: 5%;
left: 5%;
height: 8vmax;
width: 8vmax;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
user-select: none;
color: #ffffff;
`;
const CheckboxContainer = styled.div<{ $rotation: Rotation }>`
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
/* rotate: ${props.$rotation - 180}deg; */
`;
}
}}
`;
const PlayerMenuWrapper = styled.div<{
$rotation: Rotation;
}>`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(20, 20, 20, 0.9);
align-items: center;
justify-content: center;
z-index: 2;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return;
}
return css`
rotate: ${props.$rotation}deg;
`;
}};
`;
const CloseButton = styled.div<{
$rotation: Rotation;
}>`
position: absolute;
top: 15%;
right: 5%;
z-index: 9999;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
top: 5%;
right: auto;
left: 5%;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
top: auto;
left: auto;
bottom: 5%;
right: 5%;
`;
}
}}
`;
type PlayerMenuProps = {
player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
};
const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const dialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({
rotation: player.settings.rotation,
containerRef: settingsContainerRef,
});
const handleOnClick = () => {
setShowPlayerMenu(false);
};
const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const updatedPlayer = { ...player, color: event.target.value };
updatePlayer(updatedPlayer);
};
const handleSettingsChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = event.target;
const updatedSettings = { ...player.settings, [name]: checked };
const updatedPlayer = { ...player, settings: updatedSettings };
updatePlayer(updatedPlayer);
};
const handleResetGame = () => {
resetCurrentGame();
setShowPlayerMenu(false);
};
const toggleFullscreen = () => {
if (fullscreen.isFullscreen) {
fullscreen.disableFullscreen();
} else {
fullscreen.enableFullscreen();
}
};
const buttonFontSize = isSide ? '1.5vmax' : '3vmin';
const iconSize = isSide ? '6vmin' : '3vmax';
const extraCountersSize = isSide ? '8vmin' : '4vmax';
const closeButtonSize = isSide ? '6vmin' : '3vmax';
return (
<PlayerMenuWrapper $rotation={player.settings.rotation}>
<CloseButton $rotation={player.settings.rotation}>
<Button
variant="text"
onClick={handleOnClick}
style={{
margin: 0,
padding: 0,
height: closeButtonSize,
width: closeButtonSize,
}}
>
<Cross size={closeButtonSize} />
</Button>
</CloseButton>
<SettingsContainer
$rotation={player.settings.rotation}
ref={settingsContainerRef}
>
<ColorPicker
type="color"
value={player.color}
onChange={handleColorChange}
role="button"
aria-label="Color picker"
/>
<BetterRowContainer>
<TogglesSection>
{player.settings.useCommanderDamage && (
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="usePartner"
checked={player.settings.usePartner}
icon={
<PartnerTax
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="30"
/>
}
checkedIcon={
<PartnerTax
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="30"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.usePartner}
aria-label="Partner"
/>
</CheckboxContainer>
)}
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="usePoison"
checked={player.settings.usePoison}
icon={
<Poison
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="30"
/>
}
checkedIcon={
<Poison
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="30"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.usePoison}
aria-label="Poison"
/>
</CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="useEnergy"
checked={player.settings.useEnergy}
icon={
<Energy
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="15"
/>
}
checkedIcon={
<Energy
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="15"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.useEnergy}
aria-label="Energy"
/>
</CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="useExperience"
checked={player.settings.useExperience}
icon={
<Experience
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="15"
/>
}
checkedIcon={
<Experience
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="15"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.useExperience}
aria-label="Experience"
/>
</CheckboxContainer>
</TogglesSection>
<Spacer height="1rem" />
<ButtonsSections>
<Button
variant="text"
style={{
cursor: 'pointer',
userSelect: 'none',
}}
onClick={goToStart}
aria-label="Back to start"
>
<Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="fullscreen"
checked={document.fullscreenElement ? true : false}
icon={
<FullscreenOff
size={iconSize}
color={theme.palette.primary.main}
/>
}
checkedIcon={<FullscreenOn size={iconSize} />}
onChange={toggleFullscreen}
role="checkbox"
aria-checked={document.fullscreenElement ? true : false}
aria-label="Fullscreen"
/>
</CheckboxContainer>
<Button
variant={wakeLock.active ? 'contained' : 'outlined'}
style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize,
padding: '0 4px 0 4px',
}}
onClick={wakeLock.toggleWakeLock}
role="checkbox"
aria-checked={wakeLock.active}
aria-label="Keep awake"
>
Keep Awake
</Button>
<Button
style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize,
padding: '4px',
}}
onClick={() => dialogRef.current?.show()}
role="checkbox"
aria-checked={wakeLock.active}
aria-label="Reset Game"
>
<ResetGame size={iconSize} />
</Button>
</ButtonsSections>
</BetterRowContainer>
<dialog
ref={dialogRef}
style={{
zIndex: 9999,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRadius: '1rem',
border: 'none',
position: 'absolute',
top: '10%',
}}
>
<h1>Reset Game?</h1>
<div style={{ display: 'flex', justifyContent: 'space-evenly' }}>
<Button
variant="contained"
onClick={() => dialogRef.current?.close()}
>
No
</Button>
<Button
variant="contained"
onClick={() => {
handleResetGame();
dialogRef.current?.close();
}}
>
Yes
</Button>
</div>
</dialog>
</SettingsContainer>
</PlayerMenuWrapper>
);
};
export default PlayerMenu;

View File

@@ -1,443 +0,0 @@
import { Checkbox } from '@mui/material';
import { useRef } from 'react';
import { twc } from 'react-twc';
import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import {
Cross,
Energy,
Exit,
Experience,
FullscreenOff,
FullscreenOn,
PartnerTax,
Poison,
ResetGame,
} from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
const PlayerMenuWrapper = twc.div`
flex
flex-col
absolute
w-full
h-full
bg-background-settings
backdrop-blur-[3px]
items-center
justify-center
z-[2]
webkit-user-select-none
transition-all
`;
const BetterRowContainer = twc.div`
flex
flex-col
flex-grow
w-full
h-full
justify-between
items-stretch
`;
const TogglesSection = twc.div`
flex
flex-row
flex-wrap
relative
h-full
justify-evenly
items-center
`;
const ButtonsSections = twc.div`
flex
max-w-full
justify-evenly
items-center
flex-wrap
mt-0
px-2
`;
const ColorPickerButton = twc.div`
h-[8vmax]
w-[8vmax]
relative
max-h-12
max-w-12
rounded-full
cursor-pointer
overflow-hidden
`;
const SettingsContainer = twc.div<RotationDivProps>((props) => [
'flex flex-wrap h-full w-full overflow-y-scroll',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col'
: 'flex-row',
]);
type PlayerMenuProps = {
player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean;
};
const PlayerMenu = ({
player,
setShowPlayerMenu,
isShown,
}: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
const endGameDialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({
rotation: player.settings.rotation,
containerRef: settingsContainerRef,
});
const {
fullscreen,
wakeLock,
goToStart,
settings,
setPlaying,
setStopPlayerRandomization,
} = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const updatedPlayer = { ...player, color: event.target.value };
updatePlayer(updatedPlayer);
};
const handleSettingsChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = event.target;
const updatedSettings = { ...player.settings, [name]: checked };
const updatedPlayer = { ...player, settings: updatedSettings };
updatePlayer(updatedPlayer);
};
const handleResetGame = () => {
resetCurrentGame();
setShowPlayerMenu(false);
setPlaying(false);
setStopPlayerRandomization(false);
};
const handleGoToStart = () => {
goToStart();
setStopPlayerRandomization(false);
};
const toggleFullscreen = () => {
if (fullscreen.isFullscreen) {
fullscreen.disableFullscreen();
} else {
fullscreen.enableFullscreen();
}
};
const buttonFontSize = isSide ? '1.5vmax' : '3vmin';
const iconSize = isSide ? '6vmin' : '3vmax';
const extraCountersSize = isSide ? '8vmin' : '4vmax';
const calcRotation =
player.settings.rotation === Rotation.Side
? `${player.settings.rotation - 180}deg`
: player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '';
return (
<PlayerMenuWrapper
//TODO: Fix hacky solution to rotation for SideFlipped
style={{
rotate:
player.settings.rotation === Rotation.SideFlipped ? `180deg` : '',
translate: isShown ? '' : player.isSide ? `-100%` : `0 -100%`,
}}
>
<SettingsContainer
$rotation={player.settings.rotation}
style={{
rotate: calcRotation,
}}
ref={settingsContainerRef}
>
<button
onClick={() => setShowPlayerMenu(false)}
className="flex absolute top-0 right-2 z-10 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2 p-[0.2rem]"
>
<Cross size={buttonFontSize} className="text-primary-main " />
</button>
<BetterRowContainer>
<TogglesSection>
<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 && (
<div>
<Checkbox
name="usePartner"
checked={player.settings.usePartner}
icon={
<PartnerTax
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="30"
/>
}
checkedIcon={
<PartnerTax
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="30"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.usePartner}
aria-label="Partner"
/>
</div>
)}
<div>
<Checkbox
name="usePoison"
checked={player.settings.usePoison}
icon={
<Poison
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="30"
/>
}
checkedIcon={
<Poison
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="30"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.usePoison}
aria-label="Poison"
/>
</div>
<div>
<Checkbox
name="useEnergy"
checked={player.settings.useEnergy}
icon={
<Energy
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="15"
/>
}
checkedIcon={
<Energy
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="15"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.useEnergy}
aria-label="Energy"
/>
</div>
<div>
<Checkbox
name="useExperience"
checked={player.settings.useExperience}
icon={
<Experience
size={extraCountersSize}
color="black"
stroke="white"
strokeWidth="15"
/>
}
checkedIcon={
<Experience
size={extraCountersSize}
color={player.color}
stroke="white"
strokeWidth="15"
/>
}
onChange={handleSettingsChange}
role="checkbox"
aria-checked={player.settings.useExperience}
aria-label="Experience"
/>
</div>
</TogglesSection>
<ButtonsSections>
<button
className="text-primary-main cursor-pointer webkit-user-select-none"
onClick={() => endGameDialogRef.current?.show()}
aria-label="Back to start"
>
<Exit size={iconSize} style={{ rotate: '180deg' }} />
</button>
<div
data-fullscreen={document.fullscreenElement ? true : false}
className="flex
data-[fullscreen=true]:bg-secondary-dark rounded-lg border border-transparent
data-[fullscreen=true]:border-primary-main"
>
<Checkbox
name="fullscreen"
checked={document.fullscreenElement ? true : false}
icon={
<FullscreenOff
size={iconSize}
color={theme.palette.primary.main}
/>
}
checkedIcon={<FullscreenOn size={iconSize} />}
onChange={toggleFullscreen}
role="checkbox"
aria-checked={document.fullscreenElement ? true : false}
aria-label="Fullscreen"
style={{ padding: '4px' }}
/>
</div>
<button
data-wake-lock-active={settings.keepAwake}
style={{
fontSize: buttonFontSize,
}}
className="text-primary-main px-1 webkit-user-select-none cursor-pointer
data-[wake-lock-active=true]:bg-secondary-dark rounded-lg border border-transparent
data-[wake-lock-active=true]:border-primary-main
"
onClick={() => {
wakeLock.toggleWakeLock();
}}
role="checkbox"
aria-checked={settings.keepAwake}
aria-label="Keep awake"
>
Keep Awake
</button>
<button
style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize,
padding: '2px',
}}
className="text-primary-main"
onClick={() => resetGameDialogRef.current?.show()}
role="checkbox"
aria-label="Reset Game"
>
<ResetGame size={iconSize} />
</button>
</ButtonsSections>
</BetterRowContainer>
<dialog
ref={resetGameDialogRef}
className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => resetGameDialogRef.current?.close()}
>
<div className="flex size-full items-center justify-center">
<div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-xl border-none">
<h1
className="text-center text-text-primary"
style={{ fontSize: extraCountersSize }}
>
Reset Game?
</h1>
<div className="flex justify-evenly gap-2">
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
style={{ fontSize: iconSize }}
onClick={() => resetGameDialogRef.current?.close()}
>
No
</button>
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => {
handleResetGame();
resetGameDialogRef.current?.close();
}}
style={{ fontSize: iconSize }}
>
Yes
</button>
</div>
</div>
</div>
</dialog>
<dialog
ref={endGameDialogRef}
className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => endGameDialogRef.current?.close()}
>
<div className="flex size-full items-center justify-center">
<div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-xl border-none">
<h1
className="text-center text-text-primary"
style={{ fontSize: extraCountersSize }}
>
End Game?
</h1>
<div className="flex justify-evenly gap-2">
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
style={{ fontSize: iconSize }}
onClick={() => endGameDialogRef.current?.close()}
>
No
</button>
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => {
handleGoToStart();
endGameDialogRef.current?.close();
}}
style={{ fontSize: iconSize }}
>
Yes
</button>
</div>
</div>
</div>
</dialog>
</SettingsContainer>
</PlayerMenuWrapper>
);
};
export default PlayerMenu;

View File

@@ -1,158 +0,0 @@
import { useEffect, useRef } from 'react';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player as PlayerType } from '../../Types/Player';
import LifeCounter from '../LifeCounter/LifeCounter';
const getGridArea = (player: PlayerType) => {
switch (player.index) {
case 0:
return 'grid-in-player0';
case 1:
return 'grid-in-player1';
case 2:
return 'grid-in-player2';
case 3:
return 'grid-in-player3';
case 4:
return 'grid-in-player4';
case 5:
return 'grid-in-player5';
default:
throw new Error('Invalid player index');
}
};
const PlayersWrapper = twc.div`w-full h-full bg-black`;
export const Players = (players: PlayerType[], gridClasses: string) => {
const randomIntervalRef = useRef<NodeJS.Timeout | null>(null);
const prevRandomIndexRef = useRef<number>(-1);
const {
settings,
stopPlayerRandomization,
setStopPlayerRandomization,
playing,
} = useGlobalSettings();
const { setPlayers } = usePlayers();
useEffect(() => {
if (
players.length > 1 &&
settings.showStartingPlayer &&
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing
) {
randomIntervalRef.current = setInterval(() => {
let randomIndex: number;
do {
randomIndex = Math.floor(Math.random() * players.length);
} while (randomIndex === prevRandomIndexRef.current);
prevRandomIndexRef.current = randomIndex;
setPlayers(
players.map((p) =>
p.index === prevRandomIndexRef.current
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
}, 100);
}
if (!settings.useRandomStartingPlayerInterval) {
const randomPlayerIndex = Math.floor(Math.random() * players.length);
setPlayers(
players.map((p) =>
p.index === randomPlayerIndex
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
}
return () => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
players.length,
playing,
setPlayers,
settings.useRandomStartingPlayerInterval,
stopPlayerRandomization,
]);
const gradientColors = players.map((player) => player.color).join(', ');
return (
<PlayersWrapper>
{players.length > 1 &&
settings.showStartingPlayer &&
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing && (
<div
className="absolute flex justify-center items-center h-screen w-screen portrait:h-[100vw] portrait:w-[100vh] z-50 cursor-pointer text-5xl"
onClick={() => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
randomIntervalRef.current = null;
}
setStopPlayerRandomization(true);
}}
>
<div className="absolute flex top-[30%] justify-center items-center px-8 py-4">
<div
className="absolute size-full blur-[3px] rounded-2xl opacity-90 saturate-150"
style={{
backgroundImage: `linear-gradient(60deg, ${gradientColors})`,
}}
/>
<p className="relative z-10 text-[5vmax]">
PRESS TO SELECT PLAYER
</p>
</div>
</div>
)}
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => {
const gridArea = getGridArea(player);
return (
<div
key={player.index}
className={`flex justify-center items-center align-middle ${gridArea}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</div>
);
})}
</div>
</PlayersWrapper>
);
};

View File

@@ -1,67 +1,24 @@
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Orientation } from '../../Types/Settings';
import { Players } from '../Players/Players';
import { twc } from 'react-twc';
import styled from 'styled-components';
import Counters from '../Counters/Counters';
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`;
const MainWrapper = styled.div`
width: 100vmax;
height: 100vmin;
width: 100dvmax;
height: 100dvmin;
overflow: hidden;
`;
export const Play = () => {
const { players } = usePlayers();
const { initialGameSettings } = useGlobalSettings();
let Layout: JSX.Element;
switch (players.length) {
case 1:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Players(players, 'grid-areas-onePlayerPortrait');
}
Layout = Players(players, 'grid-areas-onePlayerLandscape');
break;
case 2:
switch (initialGameSettings?.orientation) {
case Orientation.Portrait:
Layout = Players(players, 'grid-areas-twoPlayersOppositePortrait');
break;
default:
case Orientation.Landscape:
Layout = Players(players, 'grid-areas-twoPlayersSameSideLandscape');
break;
case Orientation.OppositeLandscape:
Layout = Players(players, 'grid-areas-twoPlayersOppositeLandscape');
break;
}
break;
case 3:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Players(players, 'grid-areas-threePlayersSide');
break;
}
Layout = Players(players, 'grid-areas-threePlayers');
break;
default:
case 4:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Players(players, 'grid-areas-fourPlayerPortrait');
break;
}
Layout = Players(players, 'grid-areas-fourPlayer');
break;
case 5:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Players(players, 'grid-areas-fivePlayersSide');
break;
}
Layout = Players(players, 'grid-areas-fivePlayers');
break;
case 6:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Players(players, 'grid-areas-sixPlayersSide');
break;
}
Layout = Players(players, 'grid-areas-sixPlayers');
break;
}
return <MainWrapper>{Layout}</MainWrapper>;
type PlayProps = {
gridAreas: string;
};
const Play = ({ gridAreas }: PlayProps) => {
return (
<MainWrapper>
<Counters gridAreas={gridAreas} />
</MainWrapper>
);
};
export default Play;

View File

@@ -1,42 +1,41 @@
import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import React from 'react';
import styled from 'styled-components';
import { GridTemplateAreas } from '../../../Data/GridTemplateAreas';
import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import { theme } from '../../../Data/theme';
import {
FivePlayers,
FivePlayersSide,
FourPlayers,
FourPlayersSide,
OnePlayerPortrait,
SixPlayers,
SixPlayersSide,
ThreePlayers,
ThreePlayersSide,
TwoPlayersOppositeLandscape,
TwoPlayersOppositePortrait,
ThreePlayers,
ThreePlayersSide,
FourPlayers,
FourPlayersSide,
FivePlayers,
SixPlayers,
TwoPlayersSameSide,
} from '../../../Icons/generated/Layouts';
import { twc } from 'react-twc';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
import { Orientation } from '../../../Types/Settings';
const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
const LayoutWrapper = styled.div`
flex-direction: row;
display: flex;
justify-content: space-evenly;
`;
type LayoutOptionsProps = {
numberOfPlayers: number;
selectedOrientation: Orientation;
onChange: (orientation: Orientation) => void;
gridAreas: GridTemplateAreas;
onChange: (gridAreas: GridTemplateAreas) => void;
};
export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
const LayoutOptions: React.FC<LayoutOptionsProps> = ({
numberOfPlayers,
selectedOrientation,
gridAreas,
onChange,
}) => {
const iconWidth = '21vmin';
const iconHeight = '40vmin';
const iconMaxWidth = '124px';
const iconMaxHeight = '196px';
const iconHeight = '30vmin';
const iconWidth = '20vmin';
const renderLayoutOptions = () => {
switch (numberOfPlayers) {
@@ -44,7 +43,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.OnePlayerLandscape}
control={
<Radio
icon={
@@ -62,13 +61,12 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/>
}
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.OnePlayerPortrait}
control={
<Radio
icon={
@@ -86,7 +84,6 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/>
}
label=""
@@ -97,10 +94,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.TwoPlayersSameSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<TwoPlayersSameSide
height={iconHeight}
@@ -121,10 +117,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.TwoPlayersOppositePortrait}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<TwoPlayersOppositePortrait
height={iconHeight}
@@ -145,10 +140,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.OppositeLandscape}
value={GridTemplateAreas.TwoPlayersOppositeLandscape}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<TwoPlayersOppositeLandscape
height={iconHeight}
@@ -174,10 +168,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.ThreePlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<ThreePlayers
height={iconHeight}
@@ -198,10 +191,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.ThreePlayersSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<ThreePlayersSide
height={iconHeight}
@@ -228,10 +220,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.FourPlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FourPlayers
height={iconHeight}
@@ -252,10 +243,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.FourPlayersSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FourPlayersSide
height={iconHeight}
@@ -282,10 +272,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.FivePlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FivePlayers
height={iconHeight}
@@ -305,11 +294,10 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
}
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
{/* <FormControlLabel
value={GridTemplateAreas.FivePlayersSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FivePlayersSide
height={iconHeight}
@@ -328,7 +316,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
label=""
/>
/> */}
</>
);
@@ -336,10 +324,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.SixPlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<SixPlayers
height={iconHeight}
@@ -359,11 +346,10 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
}
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
{/* <FormControlLabel
value={GridTemplateAreas.SixPlayersSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<SixPlayersSide
height={iconHeight}
@@ -382,7 +368,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
label=""
/>
/> */}
</>
);
@@ -396,9 +382,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
<RadioGroup
row
onChange={(_e, value) => {
onChange(value as Orientation);
onChange(value as GridTemplateAreas);
}}
value={selectedOrientation}
value={gridAreas}
style={{ justifyContent: 'center' }}
>
{renderLayoutOptions()}
@@ -406,3 +392,5 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
</LayoutWrapper>
);
};
export default LayoutOptions;

View File

@@ -1,32 +1,58 @@
import { Button, FormControl, FormLabel, Switch } from '@mui/material';
import {
Button,
FormControl,
FormLabel,
MenuItem,
Select,
Switch,
} from '@mui/material';
import Slider from '@mui/material/Slider';
import { useEffect, useState } from 'react';
import { twc } from 'react-twc';
import styled from 'styled-components';
import { GridTemplateAreas } from '../../../Data/GridTemplateAreas';
import { createInitialPlayers } from '../../../Data/getInitialPlayers';
import { theme } from '../../../Data/theme';
import { useAnalytics } from '../../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../Hooks/usePlayers';
import { Cog, Info } from '../../../Icons/generated';
import {
GameFormat,
InitialGameSettings,
Orientation,
} from '../../../Types/Settings';
import { InfoModal } from '../../Misc/InfoModal';
import { SettingsModal } from '../../Misc/SettingsModal';
import { SupportMe } from '../../Misc/SupportMe';
import { LayoutOptions } from './LayoutOptions';
import { H1, Paragraph } from '../../Misc/TextComponents';
import LayoutOptions from './LayoutOptions';
import { Spacer } from '../../Misc/Spacer';
import { usePlayers } from '../../../Hooks/usePlayers';
import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { GameFormat, InitialGameSettings } from '../../../Types/Settings';
import { SettingsModal } from '../../Misc/SettingsModal';
const MainWrapper = twc.div`w-[100dvw] h-fit pb-14 overflow-hidden items-center flex flex-col`;
const MainWrapper = styled.div`
width: 100dvw;
height: fit-content;
padding-bottom: 58px;
overflow: hidden;
align-items: center;
display: flex;
flex-direction: column;
`;
const StartButtonFooter = twc.div`w-full max-w-[548px] fixed bottom-4 z-1 items-center flex flex-col px-4 z-10`;
const StartButtonFooter = styled.div`
position: fixed;
bottom: 1rem;
translate: -50%, -50%;
z-index: 1;
`;
const SliderWrapper = twc.div`mx-8`;
const ToggleButtonsWrapper = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`;
const ToggleButtonsWrapper = twc.div`flex flex-row justify-between items-center`;
const ToggleContainer = twc.div`flex flex-col items-center`;
const ToggleContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const playerMarks = [
{
@@ -99,8 +125,8 @@ const Start = () => {
numberOfPlayers: 4,
startingLifeTotal: 40,
useCommanderDamage: true,
orientation: Orientation.Portrait,
gameFormat: GameFormat.Commander,
gridAreas: GridTemplateAreas.FourPlayers,
}
);
@@ -138,9 +164,31 @@ const Start = () => {
return `${value}`;
};
const getDefaultLayout = (numberOfPlayers: number) => {
switch (numberOfPlayers) {
case 1:
return GridTemplateAreas.OnePlayerLandscape;
case 2:
return GridTemplateAreas.TwoPlayersSameSide;
case 3:
return GridTemplateAreas.ThreePlayers;
case 4:
return GridTemplateAreas.FourPlayers;
case 5:
return GridTemplateAreas.FivePlayers;
case 6:
return GridTemplateAreas.SixPlayers;
default:
return GridTemplateAreas.FourPlayers;
}
};
useEffect(() => {
const defaultLayout = getDefaultLayout(playerOptions.numberOfPlayers);
setPlayerOptions({
...playerOptions,
gridAreas: defaultLayout,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerOptions.numberOfPlayers]);
@@ -172,14 +220,13 @@ const Start = () => {
<SupportMe />
<h1 className="text-3xl block font-bold mt-6 mb-5 text-text-primary">
Life Trinket
</h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
<FormControl focused={false} style={{ width: '100%' }}>
<FormLabel>Number of Players</FormLabel>
<SliderWrapper>
<H1>Life Trinket</H1>
<FormControl focused={false} style={{ width: '80vw' }}>
<FormLabel>
{playerOptions.gameFormat === GameFormat.TwoHeadedGiant
? 'Number of Teams'
: 'Number of Players'}
</FormLabel>
<Slider
title="Number of Players"
max={6}
@@ -193,14 +240,11 @@ const Start = () => {
setPlayerOptions({
...playerOptions,
numberOfPlayers: value as number,
orientation: Orientation.Landscape,
});
}}
/>
</SliderWrapper>
<FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
<SliderWrapper>
<Spacer height="0.7rem" />
<FormLabel>Starting Health</FormLabel>
<Slider
title="Starting Health"
max={60}
@@ -214,39 +258,44 @@ const Start = () => {
setPlayerOptions({
...playerOptions,
startingLifeTotal: value as number,
orientation: Orientation.Landscape,
})
}
/>
</SliderWrapper>
<Spacer height="1rem" />
<ToggleButtonsWrapper className="mt-4">
<ToggleButtonsWrapper>
<ToggleContainer>
<FormLabel>Commander</FormLabel>
<FormLabel>Use Commander Damage</FormLabel>
<Switch
checked={
playerOptions.useCommanderDamage ??
initialGameSettings?.useCommanderDamage ??
true
}
disabled={playerOptions.gameFormat === GameFormat.Commander}
checked={playerOptions.useCommanderDamage}
onChange={(_e, value) => {
switch (playerOptions.gameFormat) {
case GameFormat.TwoHeadedGiant:
if (value) {
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
numberOfPlayers: 4,
startingLifeTotal: 40,
orientation: Orientation.Landscape,
startingLifeTotal: 60,
});
return;
}
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
numberOfPlayers: 2,
startingLifeTotal: 20,
orientation: Orientation.Landscape,
startingLifeTotal: 30,
});
return;
case GameFormat.Standard:
case GameFormat.Commander:
default:
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
});
return;
}
}}
/>
</ToggleContainer>
@@ -261,33 +310,77 @@ const Start = () => {
</Button>
</ToggleButtonsWrapper>
<FormLabel>Format</FormLabel>
<Select
value={playerOptions.gameFormat}
onChange={(e) => {
switch (e.target.value) {
case GameFormat.Standard:
setPlayerOptions({
...playerOptions,
useCommanderDamage: false,
numberOfPlayers: 2,
startingLifeTotal: 20,
gameFormat: GameFormat.Standard,
});
return;
case GameFormat.TwoHeadedGiant:
setPlayerOptions({
...playerOptions,
useCommanderDamage: false,
numberOfPlayers: 2,
startingLifeTotal: 30,
gameFormat: GameFormat.TwoHeadedGiant,
});
return;
case GameFormat.Commander:
default:
setPlayerOptions({
...playerOptions,
useCommanderDamage: true,
numberOfPlayers: 4,
startingLifeTotal: 40,
gameFormat: GameFormat.Commander,
});
return;
}
}}
>
<MenuItem value={GameFormat.Commander}>Commander</MenuItem>
<MenuItem value={GameFormat.Standard}>Standard</MenuItem>
<MenuItem value={GameFormat.TwoHeadedGiant}>
Two Headed Giant
</MenuItem>
</Select>
<FormLabel>Layout</FormLabel>
<LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation}
onChange={(orientation) => {
setPlayerOptions({
...playerOptions,
orientation,
});
}}
gridAreas={playerOptions.gridAreas}
onChange={(gridAreas) =>
setPlayerOptions({ ...playerOptions, gridAreas })
}
/>
</FormControl>
{!isPWA && (
<p className="text-center text-xs text-text-primary w-11/12 mt-4">
<Paragraph
style={{ textAlign: 'center', maxWidth: '75%', fontSize: '0.7rem' }}
>
If you're on iOS, this page works better if you{' '}
<strong>hide the toolbar</strong> or{' '}
<strong>add the app to your home screen</strong>.
</p>
</Paragraph>
)}
</div>
<StartButtonFooter>
<Button
size="large"
variant="contained"
onClick={doStartGame}
fullWidth
style={{ width: '90dvw' }}
>
START GAME
</Button>

View File

@@ -22,10 +22,6 @@ 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,8 +7,6 @@ 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

@@ -0,0 +1,15 @@
export enum GridTemplateAreas {
OnePlayerLandscape = '"player0 player0"',
OnePlayerPortrait = '"player0" "player0"',
TwoPlayersOppositeLandscape = '"player0" "player1"',
TwoPlayersOppositePortrait = '"player0 player1" "player0 player1"',
TwoPlayersSameSide = '"player0 player1"',
ThreePlayers = '"player0 player0" "player1 player2"',
ThreePlayersSide = '"player0 player0 player0 player2" "player1 player1 player1 player2"',
FourPlayers = '"player0 player1" "player2 player3"',
FourPlayersSide = '"player0 player1 player1 player1 player3" "player0 player2 player2 player2 player3"',
FivePlayers = '"player0 player0 player0 player1 player1 player1" "player2 player2 player3 player3 player4 player4"',
FivePlayersSide = '"player0 player0 player0 player0 player0 player1 player1 player1 player1 player1 player2" "player3 player3 player3 player3 player3 player4 player4 player4 player4 player4 player2"',
SixPlayers = '"player0 player1 player2" "player3 player4 player5"',
SixPlayersSide = '"player0 player1 player1 player1 player1 player2 player2 player2 player2 player3" "player0 player4 player4 player4 player4 player5 player5 player5 player5 player3"',
}

View File

@@ -1,5 +1,6 @@
import { Player, Rotation } from '../Types/Player';
import { InitialGameSettings, Orientation } from '../Types/Settings';
import { InitialGameSettings } from '../Types/Settings';
import { GridTemplateAreas } from './GridTemplateAreas';
const presetColors = [
'#F06292', // Light Pink
@@ -12,26 +13,16 @@ const presetColors = [
'#FF8A80', // Coral
];
const getOrientationRotations = (
index: number,
numberOfPlayers: number,
orientation: Orientation
): Rotation => {
switch (numberOfPlayers) {
case 1:
switch (orientation) {
default:
case Orientation.Landscape:
const getRotation = (index: number, gridAreas: GridTemplateAreas): Rotation => {
if (gridAreas === GridTemplateAreas.OnePlayerLandscape && index === 0) {
return Rotation.Normal;
case Orientation.Portrait:
}
if (gridAreas === GridTemplateAreas.OnePlayerPortrait && index === 0) {
return Rotation.Side;
}
case 2:
switch (orientation) {
default:
case Orientation.Landscape:
return Rotation.Normal;
case Orientation.Portrait:
if (gridAreas === GridTemplateAreas.TwoPlayersOppositePortrait) {
switch (index) {
case 0:
return Rotation.SideFlipped;
@@ -40,7 +31,9 @@ const getOrientationRotations = (
default:
return Rotation.Normal;
}
case Orientation.OppositeLandscape:
}
if (gridAreas === GridTemplateAreas.TwoPlayersOppositeLandscape) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -50,10 +43,19 @@ const getOrientationRotations = (
return Rotation.Normal;
}
}
case 3:
switch (orientation) {
if (gridAreas === GridTemplateAreas.TwoPlayersSameSide) {
switch (index) {
case 0:
return Rotation.Normal;
case 1:
return Rotation.Normal;
default:
case Orientation.Landscape:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.ThreePlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -64,7 +66,9 @@ const getOrientationRotations = (
default:
return Rotation.Normal;
}
case Orientation.Portrait:
}
if (gridAreas === GridTemplateAreas.ThreePlayersSide) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -76,10 +80,8 @@ const getOrientationRotations = (
return Rotation.Normal;
}
}
case 4:
switch (orientation) {
default:
case Orientation.Landscape:
if (gridAreas === GridTemplateAreas.FourPlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -92,7 +94,9 @@ const getOrientationRotations = (
default:
return Rotation.Normal;
}
case Orientation.Portrait:
}
if (gridAreas === GridTemplateAreas.FourPlayersSide) {
switch (index) {
case 0:
return Rotation.SideFlipped;
@@ -106,10 +110,8 @@ const getOrientationRotations = (
return Rotation.Normal;
}
}
case 5:
switch (orientation) {
default:
case Orientation.Landscape:
if (gridAreas === GridTemplateAreas.FivePlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -124,7 +126,9 @@ const getOrientationRotations = (
default:
return Rotation.Normal;
}
case Orientation.Portrait:
}
if (gridAreas === GridTemplateAreas.FivePlayersSide) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -140,10 +144,8 @@ const getOrientationRotations = (
return Rotation.Normal;
}
}
case 6:
switch (orientation) {
default:
case Orientation.Landscape:
if (gridAreas === GridTemplateAreas.SixPlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
@@ -160,7 +162,9 @@ const getOrientationRotations = (
default:
return Rotation.Normal;
}
case Orientation.Portrait:
}
if (gridAreas === GridTemplateAreas.SixPlayersSide) {
switch (index) {
case 0:
return Rotation.SideFlipped;
@@ -178,21 +182,22 @@ const getOrientationRotations = (
return Rotation.Normal;
}
}
default:
return Rotation.Normal;
}
};
export const createInitialPlayers = ({
numberOfPlayers,
startingLifeTotal,
useCommanderDamage,
orientation,
gridAreas,
}: 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];
@@ -208,7 +213,7 @@ export const createInitialPlayers = ({
});
}
const rotation = getOrientationRotations(i, numberOfPlayers, orientation);
const rotation = getRotation(i, gridAreas);
const player: Player = {
lifeTotal: startingLifeTotal,
@@ -222,11 +227,11 @@ export const createInitialPlayers = ({
usePoison: false,
rotation,
},
isStartingPlayer,
showStartingPlayer: isStartingPlayer,
extraCounters: [],
commanderDamage,
hasLost: false,
isStartingPlayer: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
};
players.push(player);

View File

@@ -1,28 +1,34 @@
import { createTheme } from '@mui/material';
import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from '../../tailwind.config';
//TODO Create provider for this
const fullConfig = resolveConfig(tailwindConfig);
const { primary, secondary, background, text, action, common } =
fullConfig.theme.colors;
export const theme = createTheme({
palette: {
primary,
secondary,
background,
text,
action,
common,
primary: {
main: '#7F9172',
},
secondary: {
main: '#5E714C',
},
background: {
default: '#495E35',
},
text: {
primary: '#F5F5F5',
secondary: '#b3b39b',
},
action: {
disabled: '#5E714C',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
},
components: {
MuiFormLabel: {
styleOverrides: {
root: {
fontSize: '1rem',
color: text.primary,
color: '#F5F5F5',
},
},
},
@@ -30,12 +36,12 @@ export const theme = createTheme({
styleOverrides: {
markLabel: {
fontSize: '1rem',
color: text.primary,
color: '#F5F5F5',
},
valueLabel: {
display: 'none',
color: text.primary,
background: secondary.main,
color: '#F5F5F5',
background: '#5E714C',
},
track: {
height: '0.7rem',
@@ -71,7 +77,7 @@ export const theme = createTheme({
styleOverrides: {
paper: {
top: '1rem',
background: background.default,
background: '#495E35',
height: 'auto',
borderRadius: '8px',
},
@@ -80,7 +86,7 @@ export const theme = createTheme({
MuiBackdrop: {
styleOverrides: {
root: {
backgroundColor: background.backdrop,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
},
},
@@ -94,7 +100,28 @@ export const theme = createTheme({
MuiSwitch: {
styleOverrides: {
colorPrimary: {
color: action.disabled,
color: '#5E714C',
},
},
},
MuiPaper: {
styleOverrides: {
root: {
backgroundColor: '#495E35',
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
backgroundColor: '#495E35',
},
},
},
MuiList: {
styleOverrides: {
root: {
backgroundColor: '#495E35',
},
},
},

View File

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

View File

@@ -1,52 +0,0 @@
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;
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

@@ -1,15 +1,11 @@
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { useWakeLock } from 'react-screen-wake-lock';
import {
GlobalSettingsContext,
GlobalSettingsContextType,
} from '../Contexts/GlobalSettingsContext';
import { useWakeLock } from 'react-screen-wake-lock';
import { useAnalytics } from '../Hooks/useAnalytics';
import {
InitialGameSettings,
InitialGameSettingsSchema,
Settings,
} from '../Types/Settings';
import { InitialGameSettings, Settings } from '../Types/Settings';
export const GlobalSettingsProvider = ({
children,
@@ -21,23 +17,11 @@ 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
@@ -46,47 +30,15 @@ export const GlobalSettingsProvider = ({
const [settings, setSettings] = useState<Settings>(
savedSettings
? JSON.parse(savedSettings)
: {
goFullscreenOnStart: true,
keepAwake: true,
showStartingPlayer: true,
showPlayerMenuCog: true,
useRandomStartingPlayerInterval: false,
}
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true }
);
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
setPlaying(false);
setShowPlay(false);
};
useEffect(() => {
if (savedGameSettings && JSON.parse(savedGameSettings).gridAreas) {
removeLocalStorage();
window.location.reload();
return;
}
//parse existing game settings with zod schema
const parsedInitialGameSettings =
InitialGameSettingsSchema.safeParse(initialGameSettings);
if (!parsedInitialGameSettings.success) {
removeLocalStorage();
window.location.reload();
return;
}
localStorage.setItem(
'initialGameSettings',
JSON.stringify(initialGameSettings)
);
}, [initialGameSettings, savedGameSettings]);
}, [initialGameSettings]);
useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings));
@@ -115,6 +67,14 @@ export const GlobalSettingsProvider = ({
request();
}
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
setShowPlay(localStorage.getItem('showPlay') === 'true' ?? false);
};
const ctxValue = useMemo((): GlobalSettingsContextType => {
const goToStart = async () => {
const currentPlayers = localStorage.getItem('players');
@@ -129,6 +89,7 @@ export const GlobalSettingsProvider = ({
};
const toggleWakeLock = async () => {
console.log('on press', active);
if (active) {
setSettings({ ...settings, keepAwake: false });
release();
@@ -168,14 +129,10 @@ export const GlobalSettingsProvider = ({
goToStart,
showPlay,
setShowPlay,
playing,
setPlaying: setPlayingAndLocalStorage,
initialGameSettings,
setInitialGameSettings,
settings,
setSettings,
stopPlayerRandomization,
setStopPlayerRandomization,
isPWA: window?.matchMedia('(display-mode: standalone)').matches,
};
}, [
@@ -184,12 +141,10 @@ export const GlobalSettingsProvider = ({
initialGameSettings,
isFullscreen,
isSupported,
playing,
release,
request,
settings,
showPlay,
stopPlayerRandomization,
type,
]);

View File

@@ -7,17 +7,6 @@ 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) : []
);
@@ -61,7 +50,9 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
return;
}
const newStartingPlayerIndex = Math.floor(Math.random() * players.length);
const startingPlayerIndex = Math.floor(
Math.random() * initialGameSettings.numberOfPlayers
);
players.forEach((player: Player) => {
player.commanderDamage.map((damage) => {
@@ -74,9 +65,16 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
});
player.lifeTotal = initialGameSettings.startingLifeTotal;
player.hasLost = false;
player.isStartingPlayer = newStartingPlayerIndex === player.index;
const isStartingPlayer = player.index === startingPlayerIndex;
player.isStartingPlayer = isStartingPlayer;
if (player.isStartingPlayer) {
player.showStartingPlayer = true;
}
updatePlayer(player);
});
@@ -89,10 +87,8 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
updatePlayer,
updateLifeTotal,
resetCurrentGame,
startingPlayerIndex,
setStartingPlayerIndex: setStartingPlayerIndexAndLocalStorage,
};
}, [players, startingPlayerIndex]);
}, [players]);
return (
<PlayersContext.Provider value={ctxValue}>

View File

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

View File

@@ -1,37 +1,21 @@
import { z } from 'zod';
import { GridTemplateAreas } from '../Data/GridTemplateAreas';
export enum Orientation {
OppositeLandscape = 'opposite-landscape',
Landscape = 'landscape',
Portrait = 'portrait',
}
export type Settings = {
keepAwake: boolean;
showStartingPlayer: boolean;
goFullscreenOnStart: boolean;
};
export type InitialGameSettings = {
startingLifeTotal: number;
useCommanderDamage: boolean;
gameFormat: GameFormat;
numberOfPlayers: number;
gridAreas: GridTemplateAreas;
};
export enum GameFormat {
Commander = 'commander',
Standard = 'standard',
TwoHeadedGiant = 'two-headed-giant',
}
export type Settings = {
keepAwake: boolean;
showStartingPlayer: boolean;
showPlayerMenuCog: boolean;
goFullscreenOnStart: boolean;
useRandomStartingPlayerInterval: boolean;
};
export type InitialGameSettings = {
startingLifeTotal: number;
useCommanderDamage: boolean;
gameFormat?: GameFormat;
numberOfPlayers: number;
orientation: Orientation;
};
export const InitialGameSettingsSchema = z.object({
startingLifeTotal: z.number().min(1).max(200).default(20),
useCommanderDamage: z.boolean().default(false),
gameFormat: z.nativeEnum(GameFormat).optional(),
numberOfPlayers: z.number().min(1).max(6).default(2),
orientation: z.nativeEnum(Orientation).default(Orientation.Landscape),
});

View File

@@ -1,87 +0,0 @@
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

@@ -1,20 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
overflow: hidden;
}
body {
overflow: auto;
}
html,
body {
height: 100%;
position: relative;
background-color: theme('colors.background.default');
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -22,25 +6,8 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
touch-action: manipulation;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@layer utilities {
.pointer-events-all {
pointer-events: all;
}
.webkit-user-select-none {
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
}
}

View File

@@ -1,108 +0,0 @@
//@ts-expect-error - tailwindcss-grid-areas does not have typescript support
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}'],
theme: {
screens: {
modalSm: '548px',
},
extend: {
gridTemplateAreas: {
onePlayerLandscape: ['player0 player0'],
onePlayerPortrait: ['player0', 'player0'],
twoPlayersOppositeLandscape: ['player0', 'player1'],
twoPlayersOppositePortrait: ['player0 player1', 'player0 player1'],
twoPlayersSameSideLandscape: ['player0 player1'],
threePlayers: ['player0 player0', 'player1 player2'],
threePlayersSide: [
'player0 player0 player0 player2',
'player1 player1 player1 player2',
],
fourPlayerPortrait: [
'player0 player1 player1 player1 player1 player3',
'player0 player2 player2 player2 player2 player3',
],
fourPlayer: ['player0 player1', 'player2 player3'],
fivePlayers: [
'player0 player0 player0 player1 player1 player1',
'player2 player2 player3 player3 player4 player4',
],
fivePlayersSide: [
'player0 player0 player0 player0 player0 player1 player1 player1 player1 player1 player2',
'player3 player3 player3 player3 player3 player4 player4 player4 player4 player4 player2',
],
sixPlayers: ['player0 player1 player2', 'player3 player4 player5'],
sixPlayersSide: [
'player0 player1 player1 player1 player1 player1 player1 player2 player2 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 player4 player4 player5 player5 player5 player5 player5 player5 player3',
],
},
colors: baseColors,
keyframes: {
fadeOut: {
'0%': {
opacity: '1',
},
'33%': {
opacity: '0.6',
},
'100%': {
opacity: '0',
},
},
},
animation: {
fadeOut: 'fadeOut 3s 1s ease-out forwards',
},
},
},
plugins: [tailwindcssGridAreas],
} satisfies Config;
// #98FF98

View File

@@ -1,27 +1,13 @@
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import react from '@vitejs/plugin-react-swc';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
clientsClaim: true,
skipWaiting: true,
},
}),
],
plugins: [react()],
build: {
minify: 'esbuild',
rollupOptions: {
external: ['babel-plugin-macros'],
},
},
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version),
REPO_READ_ACCESS_TOKEN: JSON.stringify(process.env.REPO_READ_ACCESS_TOKEN),
},
});

8867
yarn.lock

File diff suppressed because it is too large Load Diff