Compare commits

..

1 Commits

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

View File

@@ -1,8 +1,8 @@
index.html,1705225256081,6ef0d7e2de82bf64addbb9294fb28845fd06daaa544b010a47422c12ae3ad97f robots.txt,1693082171694,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
robots.txt,1705225255906,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 manifest.json,1693082171694,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
manifest.json,1705225255906,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a assets/index-5265c558.css,1693082171837,08c4451946bbdf520fe337edb365417a8bbf91914c018b83866723ef52d57b43
assets/index-08359bdb.css,1705225256081,d2766260d28230d960d75362810713efaddf40687205e697432b52869f162af7 index.html,1693082171837,09e1919fbaaa3a0bf08f43eb46c29136d62a7747b41f8b5d0f4a7ed23337c344
logo192.png,1705225255905,3b0fcf91fe2128f493de0bce2f6e2d35520a4260a04e05b8d855181359b3d3fe logo192.png,1693082171693,4309255bccbdbb341b5ab88708677e3d43b9e171d2666528ff932295a8257e4e
favicon.ico,1705225255905,75661e6187b524767554b4f28ec09a64bc72b0bb102a0b453aaead88519d9ed3 favicon.ico,1693082171692,48d8c1b9714dbc9bcb012d9c9f04112d229f20e6c889bda588ac159f973e6a8d
logo512.png,1705225255906,cf49739c9e6890bbfcd4157f299dde425df60759b7320ae9188d7ab9dc51e8ca logo512.png,1693082171694,92c7c05dc98170596d04f48e5e60eaae9535f409bcaeff129fd98fef8aba9f4e
assets/index-20658f4b.js,1705225256081,742f2c10740beea3a23f269aa6266b3c288d1fd9c7e20b6829034e8a898bf1e1 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", "name": "life-trinket",
"private": true, "private": true,
"version": "0.6.7", "version": "0.4.0",
"type": "commonjs", "type": "commonjs",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
@@ -13,7 +13,7 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"generate-icons": "npx @svgr/cli src/Icons/svgs", "generate-icons": "npx @svgr/cli src/Icons/svgs",
"deploy": "bun run build && firebase deploy --only hosting" "deploy": "bun build && firebase deploy --only hosting"
}, },
"dependencies": { "dependencies": {
"@mui/material": "^5.13.6", "@mui/material": "^5.13.6",
@@ -22,31 +22,23 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-screen-wake-lock": "^3.0.2", "react-screen-wake-lock": "^3.0.2",
"react-swipeable": "^7.0.1", "styled-components": "^6.0.7"
"react-twc": "^1.3.0",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@savvywombat/tailwindcss-grid-areas": "^3.1.0",
"@svgr/cli": "^8.1.0", "@svgr/cli": "^8.1.0",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.16",
"eslint": "^8.45.0", "eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-react-refresh": "^0.4.3",
"firebase-tools": "^12.5.2", "firebase-tools": "^12.5.2",
"install": "^0.13.0",
"postcss": "^8.4.32",
"prettier": "2.8.8", "prettier": "2.8.8",
"tailwindcss": "^3.4.1", "typescript": "^5.0.2",
"typescript": "^5.3.3", "vite": "^4.4.5"
"vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.4"
} }
} }

View File

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

View File

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

View File

@@ -1,80 +1,89 @@
import { useEffect, useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { TwcComponentProps, twc } from 'react-twc'; import styled from 'styled-components';
import { css } from 'styled-components';
import { lifeLongPressMultiplier } from '../../Data/constants'; 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'> & { import { Rotation } from '../../Types/Player';
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 = styled.div<{
$align?: string; $align?: string;
$rotation?: number; $rotation: number;
}; }>`
position: relative;
const LifeCounterButtonTwc = twc.button` ${(props) => {
h-full if (
w-full props.$rotation === Rotation.SideFlipped ||
flex props.$rotation === Rotation.Side
font-semibold ) {
bg-transparent if (props.$align === 'right') {
border-none return css`
outline-none rotate: -90deg;
cursor-pointer bottom: 25%;
justify-center top: auto;
items-center
select-none
webkit-user-select-none
`; `;
}
return css`
rotate: -90deg;
top: 25%;
`;
}
const TextContainer = twc.div<RotationButtonProps>((props) => [ if (props.$align === 'right') {
'relative', return css`
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side left: 25%;
? props.$align === 'right' `;
? '-rotate-90 bottom-1/4 top-auto' }
: '-rotate-90 top-1/4' return css`
: 'top-auto', right: 25%;
props.$rotation === Rotation.Flipped || props.$rotation === Rotation.Normal `;
? props.$align === 'right' }}
? 'left-1/4' `;
: 'right-1/4'
: '',
]);
type LifeCounterButtonProps = { type LifeCounterButtonProps = {
player: Player; lifeTotal: number;
setLifeTotal: (lifeTotal: number) => void; setLifeTotal: (lifeTotal: number) => void;
rotation: number;
operation: 'add' | 'subtract'; operation: 'add' | 'subtract';
increment: number; increment: number;
}; };
const LifeCounterButton = ({ const LifeCounterButton = ({
player, lifeTotal,
setLifeTotal, setLifeTotal,
rotation,
operation, operation,
increment, increment,
}: LifeCounterButtonProps) => { }: LifeCounterButtonProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false); const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); const [hasPressedDown, setHasPressedDown] = useState(false);
const downPositionRef = useRef({ x: 0, y: 0 });
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(player.color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [player.color]);
const handleLifeChange = (increment: number) => { const handleLifeChange = (increment: number) => {
setLifeTotal(player.lifeTotal + increment); setLifeTotal(lifeTotal + increment);
}; };
const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => { const handleDownInput = () => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
setTimeoutFinished(false); setTimeoutFinished(false);
setHasPressedDown(true); setHasPressedDown(true);
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
@@ -83,23 +92,10 @@ const LifeCounterButton = ({
}, 500); }, 500);
}; };
const handleUpInput = (event: React.PointerEvent<HTMLButtonElement>) => { const handleUpInput = () => {
if (!(hasPressedDown && !timeoutFinished)) { if (!(hasPressedDown && !timeoutFinished)) {
return; return;
} }
const upPosition = { x: event.clientX, y: event.clientY };
const hasMoved =
Math.abs(upPosition.x - downPositionRef.current.x) >
MAX_TAP_MOVE_DISTANCE ||
Math.abs(upPosition.y - downPositionRef.current.y) >
MAX_TAP_MOVE_DISTANCE;
if (hasMoved) {
return;
}
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
handleLifeChange(operation === 'add' ? 1 : -1); handleLifeChange(operation === 'add' ? 1 : -1);
setHasPressedDown(false); setHasPressedDown(false);
@@ -112,13 +108,12 @@ const LifeCounterButton = ({
}; };
const fontSize = const fontSize =
player.settings.rotation === Rotation.SideFlipped || rotation === Rotation.SideFlipped || rotation === Rotation.Side
player.settings.rotation === Rotation.Side
? '8vmax' ? '8vmax'
: '12vmin'; : '12vmin';
return ( return (
<LifeCounterButtonTwc <StyledLifeCounterButton
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -129,15 +124,12 @@ const LifeCounterButton = ({
aria-label={`${operation === 'add' ? 'Add' : 'Subtract'} life`} aria-label={`${operation === 'add' ? 'Add' : 'Subtract'} life`}
> >
<TextContainer <TextContainer
$rotation={player.settings.rotation} $rotation={rotation}
$align={operation === 'add' ? 'right' : 'left'} $align={operation === 'add' ? 'right' : 'left'}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark
data-[contrast=light]:text-icons-light"
> >
{operation === 'add' ? '\u002B' : '\u2212'} {operation === 'add' ? '\u002B' : '\u2212'}
</TextContainer> </TextContainer>
</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 { Skull } from '../../Icons/generated';
import { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
import { RotationDivProps } from './CommanderDamage';
const LoseButton = twc.div<RotationDivProps>((props) => [ export const LoseButton = styled.button<{ $rotation: Rotation }>`
'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 ', 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 ${(props) => {
? `left-[21%]` if (props.$rotation === Rotation.SideFlipped) {
: 'top-[21%]', 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 = { type LoseButtonProps = {
onClick: () => void; onClick: () => void;
@@ -17,23 +45,9 @@ type LoseButtonProps = {
}; };
export const LoseGameButton = ({ rotation, onClick }: 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 ( return (
<LoseButton <LoseButton $rotation={rotation} onClick={onClick} aria-label={`Lose Game`}>
$rotation={rotation} <Skull size="5vmin" color="black" opacity={0.5} />
onClick={onClick}
aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }}
>
<Skull size="8vmin" color="black" opacity={0.5} />
</LoseButton> </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 { 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) => [ const CommanderDamageGrid = styled.div<{ $rotation: number }>`
'flex flex-grow', display: flex;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side flex-direction: row;
? 'flex-col h-full w-auto' flex-grow: 1;
: 'flex-row w-full', width: 100%;
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
height: 100%;
width: auto;
`;
}
}}
`;
type CommanderDamageBarProps = { type CommanderDamageBarProps = {
opponents: Player[]; 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 { CounterType, Player } from '../../Types/Player';
import { usePlayers } from '../../Hooks/usePlayers'; import ExtraCounter from '../Buttons/ExtraCounter';
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import { import {
CommanderTax, CommanderTax,
Energy, Energy,
@@ -7,25 +10,52 @@ import {
PartnerTax, PartnerTax,
Poison, Poison,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { CounterType, Player, Rotation } from '../../Types/Player'; import { usePlayers } from '../../Hooks/usePlayers';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import ExtraCounter from '../Buttons/ExtraCounter'; import { GameFormat } from '../../Types/Settings';
import { useEffect, useState } from 'react';
import { checkContrast } from '../../Utils/checkContrast';
const Container = twc.div<RotationDivProps>((props) => [ const Container = styled.div<{ $rotation: Rotation }>`
'flex', width: 100%;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side height: 20vmin;
? 'h-full w-[8vmax]' display: flex;
: 'h-[20vmin] w-full',
]);
export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [ ${(props) => {
'flex absolute flex-row flex-grow pointer-events-none', if (
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped ||
? 'flex-col-reverse h-full w-auto bottom-auto' props.$rotation === Rotation.Side
: 'w-full bottom-0', ) {
]); 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 = { type ExtraCountersBarProps = {
player: Player; player: Player;
@@ -33,17 +63,7 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers(); const { updatePlayer } = usePlayers();
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark'); const { initialGameSettings } = useGlobalSettings();
useEffect(() => {
const contrast = checkContrast(player.color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [player.color]);
const handleCounterChange = ( const handleCounterChange = (
updatedCounterTotal: number, updatedCounterTotal: number,
@@ -100,26 +120,22 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
return null; return null;
} }
const isTwoHeadedGiant =
initialGameSettings?.gameFormat === GameFormat.TwoHeadedGiant;
return ( return (
<Container $rotation={player.settings.rotation}> <Container $rotation={player.settings.rotation}>
<ExtraCountersGrid $rotation={player.settings.rotation}> <ExtraCountersGrid $rotation={player.settings.rotation}>
{useCommanderDamage && ( {useCommanderDamage && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={ Icon={<CommanderTax size={iconSize} opacity="0.5" color="black" />}
<CommanderTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.CommanderTax} type={CounterType.CommanderTax}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'commanderTax' (counter) => counter.type === 'commanderTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -127,20 +143,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{Boolean(useCommanderDamage && usePartner) && ( {Boolean(useCommanderDamage && usePartner) && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={ Icon={<PartnerTax size={iconSize} opacity="0.5" color="black" />}
<PartnerTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.PartnerTax} type={CounterType.PartnerTax}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'partnerTax' (counter) => counter.type === 'partnerTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -148,19 +157,12 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{usePoison && ( {usePoison && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={ Icon={<Poison size={iconSize} opacity="0.5" color="black" />}
<Poison
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Poison} type={CounterType.Poison}
counterTotal={ counterTotal={
player.extraCounters?.find((counter) => counter.type === 'poison') player.extraCounters?.find((counter) => counter.type === 'poison')
?.value ?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -168,19 +170,12 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useEnergy && ( {useEnergy && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={ Icon={<Energy size={iconSize} opacity="0.5" color="black" />}
<Energy
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Energy} type={CounterType.Energy}
counterTotal={ counterTotal={
player.extraCounters?.find((counter) => counter.type === 'energy') player.extraCounters?.find((counter) => counter.type === 'energy')
?.value ?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -188,20 +183,13 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useExperience && ( {useExperience && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={ Icon={<Experience size={iconSize} opacity="0.5" color="black" />}
<Experience
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Experience} type={CounterType.Experience}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'experience' (counter) => counter.type === 'experience'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />

View File

@@ -1,43 +1,101 @@
import { useEffect, useRef, useState } from 'react'; 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 { Player, Rotation } from '../../Types/Player';
import {
RotationDivProps,
RotationSpanProps,
} from '../Buttons/CommanderDamage';
import LifeCounterButton from '../Buttons/LifeCounterButton'; import LifeCounterButton from '../Buttons/LifeCounterButton';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
const LifeContainer = twc.div<RotationDivProps>((props) => [ const LifeCountainer = styled.div<{
'flex flex-grow relative w-full h-full justify-between items-center', $rotation: Rotation;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side }>`
? 'flex-col-reverse' position: relative;
: 'flex-row', 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) => [ ${(props) => {
'absolute m-0 p-0 pointer-events-none select-none webkit-user-select-none', if (
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped ||
? 'w-full h-2/3' props.$rotation === Rotation.Side
: 'w-2/3 h-full', ) {
]); return css`
flex-direction: column-reverse;
const TextWrapper = twc.div` `;
flex }
absolute }}
justify-center
items-center
w-full
h-full
z-[-1]
`; `;
const RecentDifference = twc.div<RotationSpanProps>((props) => [ const LifeCounterTextContainer = styled.div<{
'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', $rotation: Rotation;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side }>`
? 'top-1/3 translate-x-1/4 translate-y-1/2 rotate-[270deg]' position: absolute;
: 'top-1/4 left-[50%] -translate-x-1/2', 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 = { type HealthProps = {
player: Player; player: Player;
@@ -49,13 +107,28 @@ type HealthProps = {
const Health = ({ const Health = ({
player, player,
rotation,
handleLifeChange, handleLifeChange,
differenceKey, differenceKey,
recentDifference, recentDifference,
}: HealthProps) => { }: HealthProps) => {
const [showStartingPlayer, setShowStartingPlayer] = useState(
localStorage.getItem('playing') === 'true'
);
const [fontSize, setFontSize] = useState(16); const [fontSize, setFontSize] = useState(16);
const textContainerRef = useRef<HTMLDivElement | null>(null); const textContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!showStartingPlayer) {
const playingTimer = setTimeout(() => {
localStorage.setItem('playing', 'true');
setShowStartingPlayer(localStorage.getItem('playing') === 'true');
}, 3_000);
return () => clearTimeout(playingTimer);
}
}, [showStartingPlayer]);
useEffect(() => { useEffect(() => {
if (!textContainerRef.current) { if (!textContainerRef.current) {
return; return;
@@ -84,28 +157,28 @@ const Health = ({
}, [textContainerRef]); }, [textContainerRef]);
const calculateFontSize = (container: HTMLDivElement) => { const calculateFontSize = (container: HTMLDivElement) => {
const widthRatio = player.isSide const isSide =
? container.clientHeight rotation === Rotation.SideFlipped || rotation === Rotation.Side;
: container.clientWidth;
const heightRatio = player.isSide const widthRatio = isSide ? container.clientHeight : container.clientWidth;
? container.clientWidth
: container.clientHeight; const heightRatio = isSide ? container.clientWidth : container.clientHeight;
const minRatio = Math.min(widthRatio, heightRatio); 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 minRatio * scaleFactor * 1;
}; };
return ( return (
<LifeContainer $rotation={player.settings.rotation}> <LifeCountainer $rotation={player.settings.rotation}>
<LifeCounterButton <LifeCounterButton
player={player} lifeTotal={player.lifeTotal}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="subtract" operation="subtract"
increment={-1} increment={-1}
/> />
@@ -122,10 +195,7 @@ const Health = ({
{player.lifeTotal} {player.lifeTotal}
</OutlinedText> </OutlinedText>
{recentDifference !== 0 && ( {recentDifference !== 0 && (
<RecentDifference <RecentDifference key={differenceKey}>
key={differenceKey}
$rotation={player.settings.rotation}
>
{recentDifference > 0 ? '+' : ''} {recentDifference > 0 ? '+' : ''}
{recentDifference} {recentDifference}
</RecentDifference> </RecentDifference>
@@ -133,12 +203,13 @@ const Health = ({
</LifeCounterTextContainer> </LifeCounterTextContainer>
</TextWrapper> </TextWrapper>
<LifeCounterButton <LifeCounterButton
player={player} lifeTotal={player.lifeTotal}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="add" operation="add"
increment={1} increment={1}
/> />
</LifeContainer> </LifeCountainer>
); );
}; };

View File

@@ -1,64 +1,137 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSwipeable } from 'react-swipeable'; import styled, { css, keyframes } from 'styled-components';
import { twc } from 'react-twc'; import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Cog } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import {
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton'; import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar'; import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar'; import ExtraCountersBar from '../Counters/ExtraCountersBar';
import { Paragraph } from '../Misc/TextComponents'; import PlayerMenu from '../PlayerMenu/PlayerMenu';
import PlayerMenu from '../Players/PlayerMenu';
import Health from './Health'; import Health from './Health';
import { baseColors } from '../../../tailwind.config'; import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [ const LifeCounterContentWrapper = styled.div<{
'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none', $backgroundColor: string;
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped }>`
? `right-auto top-[1vmax] left-[27%]` position: relative;
: 'top-1/4 right-[1vmax]', 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;
}
type SettingsButtonProps = { overflow: hidden;
onClick: () => void; `;
rotation: Rotation;
};
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => { const LifeCounterWrapper = styled.div<{
return ( $rotation: Rotation;
<SettingsButtonTwc }>`
onClick={onClick} position: relative;
$rotation={rotation} display: flex;
aria-label={`Settings`} flex-direction: column;
> align-items: center;
<Cog size="5vmin" color="black" opacity="0.3" /> width: 100%;
</SettingsButtonTwc> height: 100%;
);
};
const LifeCounterContentWrapper = twc.div` z-index: 1;
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [ ${(props) => {
'relative flex items-center w-full h-full z-[1]', if (
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped ||
? `flex-row` props.$rotation === Rotation.Side
: `flex-col`, ) {
]); return css`
flex-direction: row;
rotate: ${props.$rotation - 90}deg;
`;
}
const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [ return css`
'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', flex-direction: column;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side rotate: ${props.$rotation}deg;
? `rotate-[${props.$rotation - 90}deg]` `;
: '', }}
]); `;
const DynamicText = twc.div`text-[8vmin] whitespace-nowrap`; 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 hasCommanderDamageReached21 = (player: Player) => {
const commanderDamageTotals = player.commanderDamage.map( const commanderDamageTotals = player.commanderDamage.map(
@@ -90,87 +163,36 @@ const playerCanLose = (player: Player) => {
type LifeCounterProps = { type LifeCounterProps = {
player: Player; player: Player;
opponents: Player[]; opponents: Player[];
isStartingPlayer?: boolean;
}; };
const RECENT_DIFFERENCE_TTL = 3_000;
const LifeCounter = ({ player, opponents }: LifeCounterProps) => { const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
const { updatePlayer, updateLifeTotal } = usePlayers(); const { updatePlayer, updateLifeTotal } = usePlayers();
const { settings, playing, setPlaying, stopPlayerRandomization } = const { settings } = useGlobalSettings();
useGlobalSettings();
const playingTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [showPlayerMenu, setShowPlayerMenu] = useState(false); const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0); const [recentDifference, setRecentDifference] = useState(0);
const [differenceKey, setDifferenceKey] = useState(Date.now()); const [differenceKey, setDifferenceKey] = useState(Date.now());
const [isLandscape, setIsLandscape] = useState(false);
const calcRot = player.isSide
? player.settings.rotation - 180
: player.settings.rotation;
const rotationAngle = isLandscape ? calcRot : calcRot + 90;
const handlers = useSwipeable({
trackMouse: true,
onSwipedDown: (e) => {
e.event.stopPropagation();
setShowPlayerMenu(true);
},
onSwipedUp: (e) => {
e.event.stopPropagation();
setShowPlayerMenu(false);
},
swipeDuration: 500,
onSwiping: (e) => e.event.stopPropagation(),
rotationAngle,
});
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setRecentDifference(0); setRecentDifference(0);
}, RECENT_DIFFERENCE_TTL); }, 3_000);
const resizeObserver = new ResizeObserver(() => { return () => clearTimeout(timer);
if (document.body.clientWidth > document.body.clientHeight) }, [recentDifference]);
setIsLandscape(true);
else setIsLandscape(false);
return;
});
resizeObserver.observe(document.body);
return () => {
clearTimeout(timer);
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference, document.body.clientHeight, document.body.clientWidth]);
useEffect(() => { useEffect(() => {
if ( if (player.showStartingPlayer) {
player.isStartingPlayer && const playingTimer = setTimeout(() => {
((!playing && localStorage.setItem('playing', 'true');
settings.useRandomStartingPlayerInterval && player.showStartingPlayer = false;
stopPlayerRandomization) || updatePlayer(player);
(!settings.useRandomStartingPlayerInterval && !playing)) }, 3_000);
) {
playingTimerRef.current = setTimeout(() => {
setPlaying(true);
}, 10_000);
}
return () => clearTimeout(playingTimerRef.current); return () => clearTimeout(playingTimer);
}, [ }
player.isStartingPlayer, // eslint-disable-next-line react-hooks/exhaustive-deps
playing, }, [player.showStartingPlayer]);
setPlaying,
settings.useRandomStartingPlayerInterval,
stopPlayerRandomization,
]);
player.settings.rotation === Rotation.SideFlipped || player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side; player.settings.rotation === Rotation.Side;
@@ -186,69 +208,26 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
updatePlayer(updatedPlayer); 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;
return ( return (
<LifeCounterContentWrapper style={{ background: player.color }}> <LifeCounterContentWrapper $backgroundColor={player.color}>
<LifeCounterWrapper <LifeCounterWrapper $rotation={player.settings.rotation}>
{settings.showStartingPlayer &&
player.isStartingPlayer &&
player.showStartingPlayer && (
<PlayerNoticeWrapper
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }} $backgroundColor={theme.palette.primary.main}
{...handlers}
> >
{!playing && settings.showStartingPlayer && player.isStartingPlayer && ( <DynamicText $rotation={player.settings.rotation}>
<div You start!
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> </DynamicText>
</div> </PlayerNoticeWrapper>
)} )}
{player.hasLost && ( {player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} /> <PlayerNoticeWrapper
)} $rotation={player.settings.rotation}
{settings.useRandomStartingPlayerInterval && $backgroundColor={'#00000070'}
!stopPlayerRandomization &&
!playing && (
<div
className="flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none z-10"
style={{ backgroundColor: player.color }}
/> />
)} )}
<CommanderDamageBar <CommanderDamageBar
@@ -257,14 +236,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
key={player.index} key={player.index}
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
{settings.showPlayerMenuCog && (
<SettingsButton <SettingsButton
onClick={() => { onClick={() => {
setShowPlayerMenu(!showPlayerMenu); setShowPlayerMenu(!showPlayerMenu);
}} }}
rotation={player.settings.rotation} rotation={player.settings.rotation}
/> />
)}
{playerCanLose(player) && ( {playerCanLose(player) && (
<LoseGameButton <LoseGameButton
rotation={player.settings.rotation} rotation={player.settings.rotation}
@@ -279,13 +256,11 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<ExtraCountersBar player={player} /> <ExtraCountersBar player={player} />
<PlayerMenu
isShown={showPlayerMenu}
player={player}
setShowPlayerMenu={setShowPlayerMenu}
/>
</LifeCounterWrapper> </LifeCounterWrapper>
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)}
</LifeCounterContentWrapper> </LifeCounterContentWrapper>
); );
}; };

View File

@@ -1,37 +1,47 @@
import { twc } from 'react-twc'; import styled from 'styled-components';
import { useGlobalSettings } from '../Hooks/useGlobalSettings'; import Play from './Views/Play';
import { Play } from './Views/Play';
import StartMenu from './Views/StartMenu/StartMenu'; 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 EmergencyResetButton = styled.button`
const { goToStart } = useGlobalSettings(); width: 100vmax;
height: 100vmin;
const EmergencyResetButton = twc.button`w-[100dvmax] h-[100dvmin] absolute top-0 z-[-1] bg-background-default`; font-size: 4vmax;
const Paragraph = twc.p`text-[4vmax] text-text-secondary`; position: absolute;
top: 0;
return ( z-index: -1;
<EmergencyResetButton onClick={goToStart}> background-color: #4e6815;
<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>
);
};
export const LifeTrinket = () => { export const LifeTrinket = () => {
const { showPlay, initialGameSettings } = useGlobalSettings(); const { showPlay, goToStart, initialGameSettings } = useGlobalSettings();
return ( return (
<> <>
{showPlay && initialGameSettings ? ( {showPlay && initialGameSettings ? (
<PlayWrapper> <PlayWrapper>
<Play /> <Play gridAreas={initialGameSettings?.gridAreas} />
<EmergencyResetButton /> <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> </PlayWrapper>
) : ( ) : (
<StartWrapper> <StartWrapper>

View File

@@ -1,10 +1,21 @@
import { Modal } from '@mui/material'; import { Modal } from '@mui/material';
import { twc } from 'react-twc'; import { theme } from '../../Data/theme';
import { Separator } from './Separator'; import styled from 'styled-components';
import { Paragraph } from './TextComponents';
import { Cross } from '../../Icons/generated';
export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`; export const ModalWrapper = 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 = { type InfoModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -13,44 +24,29 @@ type InfoModalProps = {
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => { export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
return ( return (
<Modal <Modal open={isOpen} onClose={closeModal}>
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>
<ModalWrapper> <ModalWrapper>
<div> <div>
<h2 className="text-2xl text-center mb-4">📋 Usage Guide</h2> <h2 style={{ textAlign: 'center' }}>📋 Usage Guide</h2>
<Separator height="1px" /> <p>
<Paragraph className="my-4">
There are some controls that you might not know about, so here's a There are some controls that you might not know about, so here's a
short list of them. short list of them.
</Paragraph> </p>
<h3 className="text-lg font-bold mb-2">Life counter</h3>
<ul className="list-disc ml-6 mb-4"> <h3>Life counter</h3>
<ul>
<li> <li>
<strong>Tap</strong> on a player's + or - button to add or <strong>Tap</strong> on a player's + or - button to add or
subtract <strong>1 life</strong>. subtract <strong>1 life</strong>.
</li> </li>
<li> <li>
<strong>Long press</strong> on a player's + or - button to add <strong>Long press</strong> on a player's + or - button to add or
or subtract <strong>10 life</strong>. subtract <strong>10 life</strong>.
</li> </li>
</ul> </ul>
<h3 className="text-lg font-bold mb-2"> <h3>Commander damage and other counters</h3>
Commander damage and other counters <ul>
</h3>
<ul className="list-disc ml-6 mb-4">
<li> <li>
<strong>Tap</strong> on the counter to add{' '} <strong>Tap</strong> on the counter to add{' '}
<strong>1 counter</strong>. <strong>1 counter</strong>.
@@ -61,38 +57,40 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
</li> </li>
</ul> </ul>
<h3 className="text-lg font-bold mb-2">Other functionality</h3> <h3>Other</h3>
<ul className="list-disc ml-6"> <p>
<li> When a player is <strong>at or below 0 life</strong>, has taken{' '}
<Paragraph className="mb-1"> <strong>21 or more Commander Damage</strong> or has{' '}
When a player is <strong>at or below 0 life</strong>, has <strong>10 or more poison counters</strong>, a button with a skull
taken <strong>21 or more Commander Damage</strong> or has{' '} will appear on that player's card.
<strong>10 or more poison counters</strong>, a button with a </p>
skull will appear on that player's card. Tapping it will dim <p>
the player's card. Tap on the button to mark that player as lost, dimming their player
</Paragraph> card.
</li> </p>
<li>
<Paragraph className="mb-4">
Swiping <strong>down</strong> on a player's card will show
that player's settings menu.
</Paragraph>
</li>
</ul>
</div> </div>
<div className="text-center mt-4"> <br />
Visit my{' '} <div
style={{
textAlign: 'center',
marginTop: '1rem',
}}
>
Visit my
<a <a
href="https://github.com/Vikeo/LifeTrinket" href="https://github.com/Vikeo/LifeTrinket"
target="_blank" target="_blank"
className="text-text-secondary underline" style={{
textDecoration: 'none',
color: theme.palette.primary.light,
}}
> >
GitHub {' '}
</a>{' '} GitHub{' '}
</a>
for more info about this web app. for more info about this web app.
</div> </div>
</ModalWrapper> </ModalWrapper>
</>
</Modal> </Modal>
); );
}; };

View File

@@ -1,30 +1,58 @@
import styled, { css } from 'styled-components';
import { theme } from '../../Data/theme';
import { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
import { twc } from 'react-twc'; const Container = styled.div`
//TODO Create provider for this display: flex;
import tailwindConfig from './../../../tailwind.config'; position: relative;
import resolveConfig from 'tailwindcss/resolveConfig'; width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`;
const fullConfig = resolveConfig(tailwindConfig); 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 Container = twc.div` color: ${(props) => props.fillColor || theme.palette.common.black};
flex font-size: ${(props) => props.fontSize || '6vmin'};
relative -webkit-text-stroke: ${(props) => props.strokeWidth || '1vmin'}${(props) => props.strokeColor || theme.palette.common.white};
w-full -webkit-text-fill-color: ${(props) =>
h-full props.fillColor || theme.palette.common.black};
items-center
justify-center ${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: 270deg;
`; `;
}
}}
`;
const CenteredText = twc.div`absolute select-none text-common-black text-[6vmin] stroke-common-white const CenteredTextOutline = styled.span`
webkit-user-select-none tabular-nums`; position: absolute;
left: 0;
const CenteredTextOutline = twc.span` -webkit-text-stroke: 0;
absolute pointer-events: none;
left-0 `;
stroke-none
pointer-events-none
`;
type OutlinedTextProps = { type OutlinedTextProps = {
children?: React.ReactNode; children?: React.ReactNode;
@@ -45,33 +73,18 @@ export const OutlinedText: React.FC<OutlinedTextProps> = ({
fillColor, fillColor,
rotation, rotation,
}) => { }) => {
const calcRotation =
rotation === Rotation.Side
? rotation - 180
: rotation === Rotation.SideFlipped
? rotation
: 0;
return ( return (
<Container> <Container>
<CenteredText <CenteredText
style={{ fontSize={fontSize}
fontSize, fontWeight={fontWeight}
fontWeight, strokeWidth={strokeWidth}
strokeWidth: strokeWidth || '1vmin', strokeColor={strokeColor}
color: fillColor || fullConfig.theme.colors.common.black, fillColor={fillColor}
WebkitTextStroke: `${strokeWidth || '1vmin'} ${ $rotation={rotation}
strokeColor || fullConfig.theme.colors.common.white
}`,
WebkitTextFillColor:
fillColor || fullConfig.theme.colors.common.black,
rotate: `${calcRotation}deg`,
}}
> >
{children} {children}
<CenteredTextOutline aria-hidden style={{ WebkitTextStroke: 0 }}> <CenteredTextOutline aria-hidden>{children}</CenteredTextOutline>
{children}
</CenteredTextOutline>
</CenteredText> </CenteredText>
</Container> </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 = ({ export const Separator = ({
width = '100%', width = '100%',
height = '100%', height = '100%',
@@ -6,9 +16,10 @@ export const Separator = ({
height?: string; height?: string;
}) => { }) => {
return ( return (
<div <>
className={`bg-common-white bg-opacity-30 rounded-full mt-2 mb-2`} <Spacer height="0.5rem" />
style={{ width, height }} <SeparatorContainer width={width} height={height} />
/> <Spacer height="0.5rem" />
</>
); );
}; };

View File

@@ -1,19 +1,38 @@
import { Button, FormLabel, Modal, Switch } from '@mui/material'; import { Button, FormLabel, Modal, Switch } from '@mui/material';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { ModalWrapper } from './InfoModal'; import { ModalWrapper } from './InfoModal';
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { theme } from '../../Data/theme';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; 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 = { type SettingsModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -22,69 +41,12 @@ type SettingsModalProps = {
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => { export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA } = useGlobalSettings(); 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 ( return (
<Modal <Modal open={isOpen} onClose={closeModal}>
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>
<ModalWrapper> <ModalWrapper>
<Container> <Container>
<h2 className="text-center text-2xl mb-2"> Settings </h2> <h2 style={{ textAlign: 'center' }}> Settings </h2>
<Separator height="1px" />
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Show Start Player</FormLabel> <FormLabel>Show Start Player</FormLabel>
@@ -103,60 +65,19 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
start first if this is enabled. start first if this is enabled.
</Description> </Description>
</SettingContainer> </SettingContainer>
<SettingContainer>
<ToggleContainer>
<FormLabel>Show Player Menu Cog</FormLabel>
<Switch
checked={settings.showPlayerMenuCog}
onChange={() => {
setSettings({
...settings,
showPlayerMenuCog: !settings.showPlayerMenuCog,
});
}}
/>
</ToggleContainer>
<Description>
A cog on the top right of each player's card will be shown if
this is enabled.
</Description>
</SettingContainer>
<SettingContainer>
<ToggleContainer>
<FormLabel>Randomize starting player with interval</FormLabel>
<Switch
checked={settings.useRandomStartingPlayerInterval}
onChange={() => {
setSettings({
...settings,
useRandomStartingPlayerInterval:
!settings.useRandomStartingPlayerInterval,
});
}}
/>
</ToggleContainer>
<Description>
Will randomize between all players at when starting a game,
pressing the screen aborts the interval and chooses the player
that has the crown.
</Description>
</SettingContainer>
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Keep Awake</FormLabel> <FormLabel>Keep Awake</FormLabel>
<Switch <Switch
checked={settings.keepAwake} checked={settings.keepAwake}
onChange={() => { onChange={() => {
setSettings({ setSettings({ ...settings, keepAwake: !settings.keepAwake });
...settings,
keepAwake: !settings.keepAwake,
});
}} }}
/> />
</ToggleContainer> </ToggleContainer>
<Description> <Description>
Will prevent device from going to sleep while this app is open Will prevent device from going to sleep while this app is open if
if this is enabled. this is enabled.
</Description> </Description>
</SettingContainer> </SettingContainer>
<SettingContainer> <SettingContainer>
@@ -179,7 +100,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer> </SettingContainer>
{!isPWA && ( {!isPWA && (
<> <>
<Separator height="1px" /> <Separator height="2px" />
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<Paragraph> <Paragraph>
@@ -189,50 +110,24 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
normal app! normal app!
</Paragraph> </Paragraph>
</ToggleContainer> </ToggleContainer>
<Description className="mt-1"> <Description>
If you do, this app will work offline and the toolbar will If you do, this app will work offline and the toolbar will be
be automatically hidden. automatically hidden.
</Description> </Description>
</SettingContainer> </SettingContainer>
</> </>
)} )}
<Separator height="1px" /> <Separator height="2px" />
<SettingContainer> <SettingContainer>
<Paragraph> <Paragraph>Version: 0.4.0</Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
)}
</SettingContainer> </SettingContainer>
{!isLatestVersion && newVersion && ( <Separator height="2px" />
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)}
<Separator height="1px" />
<Button <Button variant="contained" onClick={closeModal}>
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close Save and Close
</Button> </Button>
</Container> </Container>
</ModalWrapper> </ModalWrapper>
</>
</Modal> </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,30 +1,43 @@
import { Button, Drawer } from '@mui/material'; import { Button, Drawer } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
import styled from 'styled-components';
import { theme } from '../../Data/theme';
import { BuyMeCoffee, KoFi } from '../../Icons/generated/Support'; import { BuyMeCoffee, KoFi } from '../../Icons/generated/Support';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import LittleGuy from '../../Icons/generated/LittleGuy'; import LittleGuy from '../../Icons/generated/LittleGuy';
import { useAnalytics } from '../../Hooks/useAnalytics'; 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` const SupportContainer = styled.div`
flex display: flex;
flex-row flex-direction: column;
items-center align-items: center;
justify-left justify-content: center;
border-none gap: 1rem;
cursor-pointer margin: 16px 0;
bg-primary-main `;
rounded-md
w-10/12 const SupportButton = styled.button`
mx-4 display: flex;
px-4 flex-direction: row;
py-2 align-items: center;
transition-colors duration-200 ease-in-out justify-content: center;
shadow-[1px_2px_4px_0px_rgba(0,0,0,0.3)] border: none;
hover:bg-primary-dark 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 = () => { export const SupportMe = () => {
const analytics = useAnalytics(); const analytics = useAnalytics();
@@ -74,7 +87,13 @@ export const SupportMe = () => {
<LittleGuy <LittleGuy
height={'4rem'} height={'4rem'}
width={'2.5rem'} 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 <Drawer
@@ -85,12 +104,22 @@ export const SupportMe = () => {
> >
<SupportContainer> <SupportContainer>
<SupportButton onClick={handleOpenBuyMeCoffee}> <SupportButton onClick={handleOpenBuyMeCoffee}>
<BuyMeCoffee height="1.5rem" width="1.5rem" className="mr-2" /> <BuyMeCoffee
<Paragraph className="text-xs">Buy him a tea</Paragraph> height={'1.5rem'}
width={'1.5rem'}
style={{ marginRight: '0.5rem' }}
/>
<Paragraph style={{ fontSize: '0.7rem' }}>Buy him a tea</Paragraph>
</SupportButton> </SupportButton>
<SupportButton onClick={handleOpenKoFi}> <SupportButton onClick={handleOpenKoFi}>
<KoFi height="1.5rem" width="1.5rem" className="mr-2" /> <KoFi
<Paragraph className="text-xs">Buy him a ko-fi</Paragraph> height={'1.5rem'}
width={'1.5rem'}
style={{ marginRight: '0.5rem' }}
/>
<Paragraph style={{ fontSize: '0.7rem' }}>
Buy him a ko-fi
</Paragraph>
</SupportButton> </SupportButton>
</SupportContainer> </SupportContainer>
</Drawer> </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 // 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

@@ -1,117 +1,187 @@
import { Button, Checkbox } from '@mui/material'; import { Button, Checkbox } from '@mui/material';
import { useRef } from 'react'; import styled, { css } from 'styled-components';
import { twc } from 'react-twc'; import { Player, Rotation } from '../../Types/Player';
import { theme } from '../../Data/theme'; import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import { import {
Cross,
Energy,
Exit,
Experience,
FullscreenOff,
FullscreenOn,
PartnerTax, PartnerTax,
Poison, Poison,
Energy,
Experience,
Exit,
FullscreenOff,
FullscreenOn,
Cross,
ResetGame, ResetGame,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { useRef } from 'react';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { Spacer } from '../Misc/Spacer';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
const CheckboxContainer = twc.div``; const SettingsContainer = styled.div<{
$rotation: Rotation;
}>`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: 100%;
width: 100%;
const PlayerMenuWrapper = twc.div` ${(props) => {
flex if (
flex-col props.$rotation === Rotation.SideFlipped ||
absolute props.$rotation === Rotation.Side
w-full ) {
h-full return css`
bg-background-settings flex-direction: column-reverse;
backdrop-blur-[3px] height: 100%;
items-center width: 100%;
justify-center `;
z-[2] }
webkit-user-select-none }}
transition-all ${(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 = twc.div` const BetterRowContainer = styled.div`
flex display: flex;
flex-col flex-direction: column;
flex-grow flex-grow: 1;
w-full width: 100%;
h-full height: 100%;
justify-between justify-content: end;
items-stretch align-items: stretch;
`; `;
const TogglesSection = twc.div` const TogglesSection = styled.div`
flex display: flex;
flex-row position: relative;
flex-wrap flex-direction: row;
relative gap: 0.5rem;
gap-2 justify-content: space-evenly;
h-full
justify-evenly
items-center
`; `;
const ButtonsSections = twc.div` const ButtonsSections = styled.div`
flex display: flex;
max-w-full max-width: 100%;
gap-4 gap: 1rem;
justify-between justify-content: space-between;
p-[3%] padding: 3% 3%;
items-center align-items: center;
flex-wrap
`; `;
const ColorPickerButton = twc.div` const ColorPicker = styled.input`
h-[8vmax] position: absolute;
w-[8vmax] top: 5%;
relative left: 5%;
max-h-12 height: 8vmax;
max-w-12 width: 8vmax;
rounded-full border: none;
cursor-pointer outline: none;
overflow-hidden cursor: pointer;
background-color: transparent;
user-select: none;
color: #ffffff;
`; `;
const SettingsContainer = twc.div<RotationDivProps>((props) => [ const CheckboxContainer = styled.div<{ $rotation: Rotation }>`
'flex flex-wrap h-full w-full', ${(props) => {
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side if (
? 'flex-col' props.$rotation === Rotation.SideFlipped ||
: 'flex-row', 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 = { type PlayerMenuProps = {
player: Player; player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void; setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean;
}; };
const PlayerMenu = ({ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
player,
setShowPlayerMenu,
isShown,
}: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null); const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null); const dialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({ const { isSide } = useSafeRotate({
rotation: player.settings.rotation, rotation: player.settings.rotation,
containerRef: settingsContainerRef, containerRef: settingsContainerRef,
}); });
const { const handleOnClick = () => {
fullscreen, setShowPlayerMenu(false);
wakeLock, };
goToStart, const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
settings,
setPlaying,
setStopPlayerRandomization,
} = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers(); const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -129,13 +199,6 @@ const PlayerMenu = ({
const handleResetGame = () => { const handleResetGame = () => {
resetCurrentGame(); resetCurrentGame();
setShowPlayerMenu(false); setShowPlayerMenu(false);
setPlaying(false);
setStopPlayerRandomization(false);
};
const handleGoToStart = () => {
goToStart();
setStopPlayerRandomization(false);
}; };
const toggleFullscreen = () => { const toggleFullscreen = () => {
@@ -149,50 +212,39 @@ const PlayerMenu = ({
const buttonFontSize = isSide ? '1.5vmax' : '3vmin'; const buttonFontSize = isSide ? '1.5vmax' : '3vmin';
const iconSize = isSide ? '6vmin' : '3vmax'; const iconSize = isSide ? '6vmin' : '3vmax';
const extraCountersSize = isSide ? '8vmin' : '4vmax'; const extraCountersSize = isSide ? '8vmin' : '4vmax';
const closeButtonSize = isSide ? '6vmin' : '3vmax';
const calcRotation =
player.settings.rotation === Rotation.Side
? `${player.settings.rotation - 180}deg`
: player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '';
return ( return (
<PlayerMenuWrapper <PlayerMenuWrapper $rotation={player.settings.rotation}>
//TODO: Fix hacky solution to rotation for SideFlipped <CloseButton $rotation={player.settings.rotation}>
<Button
variant="text"
onClick={handleOnClick}
style={{ style={{
rotate: margin: 0,
player.settings.rotation === Rotation.SideFlipped ? `180deg` : '', padding: 0,
translate: isShown ? '' : player.isSide ? `-100%` : `0 -100%`, height: closeButtonSize,
width: closeButtonSize,
}} }}
> >
<Cross size={closeButtonSize} />
</Button>
</CloseButton>
<SettingsContainer <SettingsContainer
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{
rotate: calcRotation,
}}
ref={settingsContainerRef} ref={settingsContainerRef}
> >
{settings.showPlayerMenuCog && ( <ColorPicker
<button type="color"
onClick={() => setShowPlayerMenu(false)} value={player.color}
className="flex absolute top-0 right-2 z-10 w-8 h-8 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2" onChange={handleColorChange}
> role="button"
<Cross size="16px" className="text-primary-main " /> aria-label="Color picker"
</button> />
)}
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
<ColorPickerButton aria-label="Color picker">
<input
onChange={handleColorChange}
type="color"
className="size-[200%] absolute -left-2 -top-2"
value={player.color}
/>
</ColorPickerButton>
{player.settings.useCommanderDamage && ( {player.settings.useCommanderDamage && (
<CheckboxContainer> <CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="usePartner" name="usePartner"
checked={player.settings.usePartner} checked={player.settings.usePartner}
@@ -219,7 +271,8 @@ const PlayerMenu = ({
/> />
</CheckboxContainer> </CheckboxContainer>
)} )}
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
checked={player.settings.usePoison} checked={player.settings.usePoison}
@@ -245,7 +298,8 @@ const PlayerMenu = ({
aria-label="Poison" aria-label="Poison"
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
checked={player.settings.useEnergy} checked={player.settings.useEnergy}
@@ -271,7 +325,8 @@ const PlayerMenu = ({
aria-label="Energy" aria-label="Energy"
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
checked={player.settings.useExperience} checked={player.settings.useExperience}
@@ -298,19 +353,20 @@ const PlayerMenu = ({
/> />
</CheckboxContainer> </CheckboxContainer>
</TogglesSection> </TogglesSection>
<ButtonsSections className="mt-4"> <Spacer height="1rem" />
<ButtonsSections>
<Button <Button
variant="text" variant="text"
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
userSelect: 'none', userSelect: 'none',
}} }}
onClick={handleGoToStart} onClick={goToStart}
aria-label="Back to start" aria-label="Back to start"
> >
<Exit size={iconSize} style={{ rotate: '180deg' }} /> <Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button> </Button>
<CheckboxContainer> <CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="fullscreen" name="fullscreen"
checked={document.fullscreenElement ? true : false} checked={document.fullscreenElement ? true : false}
@@ -351,7 +407,7 @@ const PlayerMenu = ({
fontSize: buttonFontSize, fontSize: buttonFontSize,
padding: '4px', padding: '4px',
}} }}
onClick={() => resetGameDialogRef.current?.show()} onClick={() => dialogRef.current?.show()}
role="checkbox" role="checkbox"
aria-checked={wakeLock.active} aria-checked={wakeLock.active}
aria-label="Reset Game" aria-label="Reset Game"
@@ -361,17 +417,22 @@ const PlayerMenu = ({
</ButtonsSections> </ButtonsSections>
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={resetGameDialogRef} ref={dialogRef}
className="z-[999] size-full bg-background-settings" style={{
onClick={() => resetGameDialogRef.current?.close()} zIndex: 9999,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRadius: '1rem',
border: 'none',
position: 'absolute',
top: '10%',
}}
> >
<div className="flex size-full items-center justify-center"> <h1>Reset Game?</h1>
<div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-2xl border-none"> <div style={{ display: 'flex', justifyContent: 'space-evenly' }}>
<h1 className="text-center text-text-primary">Reset Game?</h1>
<div className="flex justify-evenly gap-4">
<Button <Button
variant="contained" variant="contained"
onClick={() => resetGameDialogRef.current?.close()} onClick={() => dialogRef.current?.close()}
> >
No No
</Button> </Button>
@@ -379,14 +440,12 @@ const PlayerMenu = ({
variant="contained" variant="contained"
onClick={() => { onClick={() => {
handleResetGame(); handleResetGame();
resetGameDialogRef.current?.close(); dialogRef.current?.close();
}} }}
> >
Yes Yes
</Button> </Button>
</div> </div>
</div>
</div>
</dialog> </dialog>
</SettingsContainer> </SettingsContainer>
</PlayerMenuWrapper> </PlayerMenuWrapper>

View File

@@ -1,144 +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 (
settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing
) {
randomIntervalRef.current = setInterval(() => {
let randomIndex: number;
do {
randomIndex = Math.floor(Math.random() * players.length);
} while (randomIndex === prevRandomIndexRef.current);
prevRandomIndexRef.current = randomIndex;
setPlayers(
players.map((p) =>
p.index === prevRandomIndexRef.current
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
}, 100);
}
if (!settings.useRandomStartingPlayerInterval) {
const randomPlayerIndex = Math.floor(Math.random() * players.length);
setPlayers(
players.map((p) =>
p.index === randomPlayerIndex
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
}
return () => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
players.length,
playing,
setPlayers,
settings.useRandomStartingPlayerInterval,
stopPlayerRandomization,
]);
return (
<PlayersWrapper>
{settings.useRandomStartingPlayerInterval &&
!stopPlayerRandomization &&
!playing && (
<div
className="absolute flex justify-center items-center bg-black bg-opacity-40 h-screen w-screen portrait:h-[100vw] portrait:w-[100vh] z-50 cursor-pointer text-5xl"
onClick={() => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
randomIntervalRef.current = null;
}
setStopPlayerRandomization(true);
}}
>
<div className="bg-primary-main px-8 py-2 rounded-2xl opacity-70 text-[5vmax]">
PRESS TO SELECT PLAYER
</div>
</div>
)}
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => {
const gridArea = getGridArea(player);
return (
<div
key={player.index}
className={`flex justify-center items-center align-middle ${gridArea}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</div>
);
})}
</div>
</PlayersWrapper>
);
};

View File

@@ -1,67 +1,24 @@
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import styled from 'styled-components';
import { usePlayers } from '../../Hooks/usePlayers'; import Counters from '../Counters/Counters';
import { Orientation } from '../../Types/Settings';
import { Players } from '../Players/Players';
import { twc } from 'react-twc';
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 = () => { type PlayProps = {
const { players } = usePlayers(); gridAreas: string;
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>;
}; };
const Play = ({ gridAreas }: PlayProps) => {
return (
<MainWrapper>
<Counters gridAreas={gridAreas} />
</MainWrapper>
);
};
export default Play;

View File

@@ -1,48 +1,49 @@
import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import React from 'react'; 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 { theme } from '../../../Data/theme';
import { import {
FivePlayers,
FourPlayers,
FourPlayersSide,
OnePlayerPortrait, OnePlayerPortrait,
SixPlayers,
ThreePlayers,
ThreePlayersSide,
TwoPlayersOppositeLandscape, TwoPlayersOppositeLandscape,
TwoPlayersOppositePortrait, TwoPlayersOppositePortrait,
ThreePlayers,
ThreePlayersSide,
FourPlayers,
FourPlayersSide,
FivePlayers,
SixPlayers,
TwoPlayersSameSide, TwoPlayersSameSide,
} from '../../../Icons/generated/Layouts'; } from '../../../Icons/generated/Layouts';
import { twc } from 'react-twc';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape'; 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 = { type LayoutOptionsProps = {
numberOfPlayers: number; numberOfPlayers: number;
selectedOrientation: Orientation; gridAreas: GridTemplateAreas;
onChange: (orientation: Orientation) => void; onChange: (gridAreas: GridTemplateAreas) => void;
}; };
export const LayoutOptions: React.FC<LayoutOptionsProps> = ({ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
numberOfPlayers, numberOfPlayers,
selectedOrientation, gridAreas,
onChange, onChange,
}) => { }) => {
const iconWidth = '21vmin'; const iconHeight = '30vmin';
const iconHeight = '40vmin'; const iconWidth = '20vmin';
const iconMaxWidth = '124px';
const iconMaxHeight = '196px';
const renderLayoutOptions = () => { const renderLayoutOptions = () => {
switch (numberOfPlayers) { switch (numberOfPlayers) {
case 1: case 1:
return ( return (
<div> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={GridTemplateAreas.OnePlayerLandscape}
control={ control={
<Radio <Radio
icon={ icon={
@@ -60,13 +61,12 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={Orientation.Portrait} value={GridTemplateAreas.OnePlayerPortrait}
control={ control={
<Radio <Radio
icon={ icon={
@@ -84,21 +84,19 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
</div> </>
); );
case 2: case 2:
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={GridTemplateAreas.TwoPlayersSameSide}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersSameSide <TwoPlayersSameSide
height={iconHeight} height={iconHeight}
@@ -119,10 +117,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={Orientation.Portrait} value={GridTemplateAreas.TwoPlayersOppositePortrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositePortrait <TwoPlayersOppositePortrait
height={iconHeight} height={iconHeight}
@@ -143,10 +140,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={Orientation.OppositeLandscape} value={GridTemplateAreas.TwoPlayersOppositeLandscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositeLandscape <TwoPlayersOppositeLandscape
height={iconHeight} height={iconHeight}
@@ -172,10 +168,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={GridTemplateAreas.ThreePlayers}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayers <ThreePlayers
height={iconHeight} height={iconHeight}
@@ -196,10 +191,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={Orientation.Portrait} value={GridTemplateAreas.ThreePlayersSide}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayersSide <ThreePlayersSide
height={iconHeight} height={iconHeight}
@@ -226,10 +220,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={GridTemplateAreas.FourPlayers}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayers <FourPlayers
height={iconHeight} height={iconHeight}
@@ -250,10 +243,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={Orientation.Portrait} value={GridTemplateAreas.FourPlayersSide}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayersSide <FourPlayersSide
height={iconHeight} height={iconHeight}
@@ -280,10 +272,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={GridTemplateAreas.FivePlayers}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayers <FivePlayers
height={iconHeight} height={iconHeight}
@@ -333,10 +324,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={GridTemplateAreas.SixPlayers}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayers <SixPlayers
height={iconHeight} height={iconHeight}
@@ -392,9 +382,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
<RadioGroup <RadioGroup
row row
onChange={(_e, value) => { onChange={(_e, value) => {
onChange(value as Orientation); onChange(value as GridTemplateAreas);
}} }}
value={selectedOrientation} value={gridAreas}
style={{ justifyContent: 'center' }} style={{ justifyContent: 'center' }}
> >
{renderLayoutOptions()} {renderLayoutOptions()}
@@ -402,3 +392,5 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
</LayoutWrapper> </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 Slider from '@mui/material/Slider';
import { useEffect, useState } from 'react'; 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 { createInitialPlayers } from '../../../Data/getInitialPlayers';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { useAnalytics } from '../../../Hooks/useAnalytics'; import { useAnalytics } from '../../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../Hooks/usePlayers';
import { Cog, Info } from '../../../Icons/generated'; import { Cog, Info } from '../../../Icons/generated';
import {
GameFormat,
InitialGameSettings,
Orientation,
} from '../../../Types/Settings';
import { InfoModal } from '../../Misc/InfoModal'; import { InfoModal } from '../../Misc/InfoModal';
import { SettingsModal } from '../../Misc/SettingsModal';
import { SupportMe } from '../../Misc/SupportMe'; 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 = styled.div`
display: flex;
const ToggleContainer = twc.div`flex flex-col items-center`; flex-direction: column;
align-items: center;
`;
const playerMarks = [ const playerMarks = [
{ {
@@ -99,8 +125,8 @@ const Start = () => {
numberOfPlayers: 4, numberOfPlayers: 4,
startingLifeTotal: 40, startingLifeTotal: 40,
useCommanderDamage: true, useCommanderDamage: true,
orientation: Orientation.Portrait,
gameFormat: GameFormat.Commander, gameFormat: GameFormat.Commander,
gridAreas: GridTemplateAreas.FourPlayers,
} }
); );
@@ -138,9 +164,31 @@ const Start = () => {
return `${value}`; 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(() => { useEffect(() => {
const defaultLayout = getDefaultLayout(playerOptions.numberOfPlayers);
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
gridAreas: defaultLayout,
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerOptions.numberOfPlayers]); }, [playerOptions.numberOfPlayers]);
@@ -172,14 +220,13 @@ const Start = () => {
<SupportMe /> <SupportMe />
<h1 className="text-3xl block font-bold mt-6 mb-5 text-text-primary"> <H1>Life Trinket</H1>
Life Trinket <FormControl focused={false} style={{ width: '80vw' }}>
</h1> <FormLabel>
{playerOptions.gameFormat === GameFormat.TwoHeadedGiant
<div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4"> ? 'Number of Teams'
<FormControl focused={false} style={{ width: '100%' }}> : 'Number of Players'}
<FormLabel>Number of Players</FormLabel> </FormLabel>
<SliderWrapper>
<Slider <Slider
title="Number of Players" title="Number of Players"
max={6} max={6}
@@ -193,14 +240,11 @@ const Start = () => {
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
numberOfPlayers: value as number, numberOfPlayers: value as number,
orientation: Orientation.Landscape,
}); });
}} }}
/> />
</SliderWrapper> <Spacer height="0.7rem" />
<FormLabel>Starting Health</FormLabel>
<FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
<SliderWrapper>
<Slider <Slider
title="Starting Health" title="Starting Health"
max={60} max={60}
@@ -214,39 +258,44 @@ const Start = () => {
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
startingLifeTotal: value as number, startingLifeTotal: value as number,
orientation: Orientation.Landscape,
}) })
} }
/> />
</SliderWrapper> <Spacer height="1rem" />
<ToggleButtonsWrapper className="mt-4"> <ToggleButtonsWrapper>
<ToggleContainer> <ToggleContainer>
<FormLabel>Commander</FormLabel> <FormLabel>Use Commander Damage</FormLabel>
<Switch <Switch
checked={ disabled={playerOptions.gameFormat === GameFormat.Commander}
playerOptions.useCommanderDamage ?? checked={playerOptions.useCommanderDamage}
initialGameSettings?.useCommanderDamage ??
true
}
onChange={(_e, value) => { onChange={(_e, value) => {
switch (playerOptions.gameFormat) {
case GameFormat.TwoHeadedGiant:
if (value) { if (value) {
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
useCommanderDamage: value, useCommanderDamage: value,
numberOfPlayers: 4, startingLifeTotal: 60,
startingLifeTotal: 40,
orientation: Orientation.Landscape,
}); });
return; return;
} }
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
useCommanderDamage: value, useCommanderDamage: value,
numberOfPlayers: 2, startingLifeTotal: 30,
startingLifeTotal: 20,
orientation: Orientation.Landscape,
}); });
return;
case GameFormat.Standard:
case GameFormat.Commander:
default:
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
});
return;
}
}} }}
/> />
</ToggleContainer> </ToggleContainer>
@@ -261,45 +310,77 @@ const Start = () => {
</Button> </Button>
</ToggleButtonsWrapper> </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> <FormLabel>Layout</FormLabel>
{/* <LayoutOptions <LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers} numberOfPlayers={playerOptions.numberOfPlayers}
gridAreas={playerOptions.gridAreas} gridAreas={playerOptions.gridAreas}
onChange={(gridAreas) => onChange={(gridAreas) =>
setPlayerOptions({ setPlayerOptions({ ...playerOptions, gridAreas })
...playerOptions,
gridAreas,
//TODO fix the layout selection
orientation: Orientation.Portrait,
})
} }
/> */}
<LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation}
onChange={(orientation) => {
setPlayerOptions({
...playerOptions,
orientation,
});
}}
/> />
</FormControl> </FormControl>
{!isPWA && ( {!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{' '} If you're on iOS, this page works better if you{' '}
<strong>hide the toolbar</strong> or{' '} <strong>hide the toolbar</strong> or{' '}
<strong>add the app to your home screen</strong>. <strong>add the app to your home screen</strong>.
</p> </Paragraph>
)} )}
</div>
<StartButtonFooter> <StartButtonFooter>
<Button <Button
size="large" size="large"
variant="contained" variant="contained"
onClick={doStartGame} onClick={doStartGame}
fullWidth style={{ width: '90dvw' }}
> >
START GAME START GAME
</Button> </Button>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,53 +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;
console.log(orientation);
if (orientation) {
const { angle, type } = orientation;
setState({ angle, type });
if (type.includes('landscape')) {
setIsLandscape(true);
} else if (type.includes('portrait')) {
setIsLandscape(false);
}
} else if (window.orientation !== undefined) {
setState({
angle:
typeof window.orientation === 'number' ? window.orientation : 0,
type: '',
});
}
}
};
onChange();
return () => {
mounted = false;
};
}, [isLandscape]);
return { state, isLandscape };
}

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,21 @@
import { z } from 'zod'; import { GridTemplateAreas } from '../Data/GridTemplateAreas';
export enum Orientation { export type Settings = {
OppositeLandscape = 'opposite-landscape', keepAwake: boolean;
Landscape = 'landscape', showStartingPlayer: boolean;
Portrait = 'portrait', goFullscreenOnStart: boolean;
} };
export type InitialGameSettings = {
startingLifeTotal: number;
useCommanderDamage: boolean;
gameFormat: GameFormat;
numberOfPlayers: number;
gridAreas: GridTemplateAreas;
};
export enum GameFormat { export enum GameFormat {
Commander = 'commander', Commander = 'commander',
Standard = 'standard', Standard = 'standard',
TwoHeadedGiant = 'two-headed-giant', 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 { body {
overflow: auto;
}
html,
body {
height: 100%;
position: relative;
background-color: theme('colors.background.default');
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -22,25 +6,8 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
#root {
touch-action: manipulation;
}
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; 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 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 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 { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa'; import react from '@vitejs/plugin-react-swc';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [react()],
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
clientsClaim: true,
skipWaiting: true,
},
}),
],
build: { build: {
minify: 'esbuild', minify: 'esbuild',
rollupOptions: { rollupOptions: {
external: ['babel-plugin-macros'], 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