Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Rådberg
23ab7c4e46 wip 2023-10-21 15:36:53 +02:00
43 changed files with 1795 additions and 10464 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.1", "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,49 +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;
`;
}
}}
`;
const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [ const CommanderDamageTextContainer = styled.div<{
'flex flex-grow', $rotation: number;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side }>`
? 'flex-col' position: relative;
: 'flex-row', 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 CommanderDamageButton = twc.button<RotationButtonProps>((props) => [ ${(props) => {
'flex flex-grow border-none outline-none cursor-pointer m-0 p-0 webkit-user-select-none', if (
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped ||
? 'w-[6vmax] h-auto' props.$rotation === Rotation.Side
: 'h-[10vmin] w-1/2', ) {
]); return css`
rotate: 270deg;
`;
}
}}
`;
const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [ const PartnerDamageSeperator = styled.div<{
'relative top-1/2 left-1/2 tabular-nums pointer-events-none select-none webkit-user-select-none', $rotation: number;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side }>`
? 'rotate-[270deg]' width: 1px;
: '', background-color: rgba(0, 0, 0, 1);
]);
const PartnerDamageSeperator = twc.div<RotationDivProps>((props) => [ ${(props) => {
'bg-black', if (
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped ||
? 'w-full h-px' props.$rotation === Rotation.Side
: 'w-px', ) {
]); return css`
width: auto;
height: 1px;
`;
}
}}
`;
type CommanderDamageButtonComponentProps = { type CommanderDamageButtonComponentProps = {
player: Player; player: Player;
@@ -66,6 +127,10 @@ export const CommanderDamage = ({
const [timeoutFinished, setTimeoutFinished] = useState(false); const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); 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,
increment: number, increment: number,
@@ -128,9 +193,9 @@ export const CommanderDamage = ({
}; };
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
@@ -149,8 +214,8 @@ export const CommanderDamage = ({
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
@@ -183,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 { 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,15 @@ 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 isSide =
rotation === Rotation.Side || rotation === Rotation.SideFlipped;
const handleCountChange = (increment: number) => { const handleCountChange = (increment: number) => {
if (!counterTotal) { if (!counterTotal) {
setCounterTotal(increment, type); setCounterTotal(increment, type);
@@ -102,7 +115,7 @@ const ExtraCounter = ({
return ( return (
<ExtraCounterContainer> <ExtraCounterContainer>
<ExtraCounterButton <StyledExtraCounterButton
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -123,7 +136,7 @@ const ExtraCounter = ({
</OutlinedText> </OutlinedText>
</TextContainer> </TextContainer>
</IconContainer> </IconContainer>
</ExtraCounterButton> </StyledExtraCounterButton>
</ExtraCounterContainer> </ExtraCounterContainer>
); );
}; };

View File

@@ -1,42 +1,64 @@
import { 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 { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
type RotationButtonProps = TwcComponentProps<'div'> & { 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
text-lifeCounter-text ) {
font-semibold if (props.$align === 'right') {
bg-transparent return css`
border-none rotate: -90deg;
outline-none bottom: 25%;
cursor-pointer top: auto;
justify-center
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 = {
lifeTotal: number; lifeTotal: number;
@@ -91,7 +113,7 @@ const LifeCounterButton = ({
: '12vmin'; : '12vmin';
return ( return (
<LifeCounterButtonTwc <StyledLifeCounterButton
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -107,7 +129,7 @@ const LifeCounterButton = ({
> >
{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-[19%]` 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

@@ -1,14 +1,37 @@
import { twc } from 'react-twc'; import styled from 'styled-components';
import { Cog } from '../../Icons/generated'; import { css } from 'styled-components';
import { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
import { RotationButtonProps } from './CommanderDamage'; import { Cog } from '../../Icons/generated';
const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [ export const StyledSettingsButton = styled.button<{ $rotation: Rotation }>`
'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none', position: absolute;
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped flex-grow: 1;
? `right-auto top-[1vmax] left-[27%]` border: none;
: 'top-1/4 right-[1vmax]', 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 = { type SettingsButtonProps = {
onClick: () => void; onClick: () => void;
@@ -17,13 +40,13 @@ type SettingsButtonProps = {
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => { const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => {
return ( return (
<SettingsButtonTwc <StyledSettingsButton
onClick={onClick} onClick={onClick}
$rotation={rotation} $rotation={rotation}
aria-label={`Settings`} aria-label={`Settings`}
> >
<Cog size="5vmin" color="black" opacity="0.3" /> <Cog size="5vmin" color="black" opacity="0.3" />
</SettingsButtonTwc> </StyledSettingsButton>
); );
}; };

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,23 +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';
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;
@@ -31,6 +63,7 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers(); const { updatePlayer } = usePlayers();
const { initialGameSettings } = useGlobalSettings();
const handleCounterChange = ( const handleCounterChange = (
updatedCounterTotal: number, updatedCounterTotal: number,
@@ -87,6 +120,9 @@ 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}>
@@ -100,7 +136,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'commanderTax' (counter) => counter.type === 'commanderTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -115,7 +150,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'partnerTax' (counter) => counter.type === 'partnerTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -129,7 +163,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
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}
/> />
@@ -143,7 +176,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
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}
/> />
@@ -158,7 +190,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(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,6 +107,7 @@ type HealthProps = {
const Health = ({ const Health = ({
player, player,
rotation,
handleLifeChange, handleLifeChange,
differenceKey, differenceKey,
recentDifference, recentDifference,
@@ -98,25 +157,24 @@ 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
lifeTotal={player.lifeTotal} lifeTotal={player.lifeTotal}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
@@ -137,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>
@@ -154,7 +209,7 @@ const Health = ({
operation="add" operation="add"
increment={1} increment={1}
/> />
</LifeContainer> </LifeCountainer>
); );
}; };

View File

@@ -1,36 +1,137 @@
import { useEffect, 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 { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { 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 PlayerMenu from '../Player/PlayerMenu'; import PlayerMenu from '../PlayerMenu/PlayerMenu';
import Health from './Health'; import Health from './Health';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
const LifeCounterContentWrapper = twc.div` const LifeCounterContentWrapper = styled.div<{
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`; $backgroundColor: string;
}>`
position: relative;
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
@media (orientation: landscape) {
max-width: 100vmax;
max-height: 100vmin;
}
const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [ overflow: hidden;
'relative flex items-center w-full h-full z-[1]', `;
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `flex-row`
: `flex-col`,
]);
const StartingPlayerNoticeWrapper = twc.div`z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-primary-main`; const LifeCounterWrapper = styled.div<{
$rotation: Rotation;
}>`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [ z-index: 1;
'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper opacity-75',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `rotate-[${props.$rotation - 90}deg]`
: '',
]);
const DynamicText = twc.div`text-[8vmin] whitespace-nowrap`; ${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: row;
rotate: ${props.$rotation - 90}deg;
`;
}
return css`
flex-direction: column;
rotate: ${props.$rotation}deg;
`;
}}
`;
const PlayerNoticeWrapper = styled.div<{
$rotation: Rotation;
$backgroundColor: string;
}>`
z-index: 1;
display: flex;
position: absolute;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: ${(props) => props.$backgroundColor};
pointer-events: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 90}deg;
`;
}
}}
`;
const DynamicText = styled.div<{ $rotation: Rotation }>`
font-size: 8vmin;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
33% {
opacity: 0.6;
}
100% {
opacity: 0;
}
`;
export const RecentDifference = styled.span`
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
text-shadow: none;
background-color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
border-radius: 50%;
padding: 5px 10px;
font-size: 8vmin;
color: #333333;
animation: ${fadeOut} 3s 1s ease-out forwards;
`;
const hasCommanderDamageReached21 = (player: Player) => { const hasCommanderDamageReached21 = (player: Player) => {
const commanderDamageTotals = player.commanderDamage.map( const commanderDamageTotals = player.commanderDamage.map(
@@ -71,51 +172,14 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
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: () => {
console.log(`User DOWN Swiped on player ${player.index}`);
setShowPlayerMenu(true);
},
onSwipedUp: () => {
console.log(`User UP Swiped on player ${player.index}`);
setShowPlayerMenu(false);
},
swipeDuration: 500,
onSwiping: (eventData) => console.log(eventData),
rotationAngle,
});
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setRecentDifference(0); setRecentDifference(0);
}, 3_000); }, 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 (player.showStartingPlayer) { if (player.showStartingPlayer) {
@@ -144,43 +208,27 @@ 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}>
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
{...handlers}
>
{settings.showStartingPlayer && {settings.showStartingPlayer &&
player.isStartingPlayer && player.isStartingPlayer &&
player.showStartingPlayer && ( player.showStartingPlayer && (
<StartingPlayerNoticeWrapper <PlayerNoticeWrapper
style={{ rotate: `${calcRotation}deg` }} $rotation={player.settings.rotation}
> $backgroundColor={theme.palette.primary.main}
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
> >
<DynamicText $rotation={player.settings.rotation}>
You start! You start!
</DynamicText> </DynamicText>
</StartingPlayerNoticeWrapper> </PlayerNoticeWrapper>
)} )}
{player.hasLost && ( {player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} /> <PlayerNoticeWrapper
$rotation={player.settings.rotation}
$backgroundColor={'#00000070'}
/>
)} )}
<CommanderDamageBar <CommanderDamageBar
opponents={opponents} opponents={opponents}
@@ -188,6 +236,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
key={player.index} key={player.index}
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<SettingsButton
onClick={() => {
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}
/>
{playerCanLose(player) && ( {playerCanLose(player) && (
<LoseGameButton <LoseGameButton
rotation={player.settings.rotation} rotation={player.settings.rotation}
@@ -202,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,9 +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';
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;
@@ -12,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 relative w-full max-w-[548px]">
<button
onClick={closeModal}
className="flex absolute top-10 right-0 z-10 w-10 h-10 text-common-white bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
>
X
</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>.
@@ -60,21 +57,33 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
</li> </li>
</ul> </ul>
<h3 className="text-lg font-bold mb-2">Other</h3> <h3>Other</h3>
<Paragraph className="mb-4"> <p>
When a player is <strong>at or below 0 life</strong>, has taken{' '} When a player is <strong>at or below 0 life</strong>, has taken{' '}
<strong>21 or more Commander Damage</strong> or has{' '} <strong>21 or more Commander Damage</strong> or has{' '}
<strong>10 or more poison counters</strong>, a button with a skull <strong>10 or more poison counters</strong>, a button with a skull
will appear on that player's card. Tapping it will dim the will appear on that player's card.
player's card. </p>
</Paragraph> <p>
Tap on the button to mark that player as lost, dimming their player
card.
</p>
</div> </div>
<div className="text-center mt-4"> <br />
<div
style={{
textAlign: 'center',
marginTop: '1rem',
}}
>
Visit my 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{' '} GitHub{' '}
@@ -82,7 +91,6 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
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,18 +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';
const SettingContainer = twc.div`w-full flex flex-col`; const SettingContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;
const ToggleContainer = twc.div`flex flex-row justify-between items-center`; 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;
@@ -21,65 +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 open={isOpen} onClose={closeModal}> <Modal open={isOpen} onClose={closeModal}>
<>
<div className="flex relative w-full max-w-[548px]">
<button
onClick={closeModal}
className="flex absolute top-10 right-0 z-10 w-10 h-10 text-common-white bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
>
X
</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>
@@ -104,16 +71,13 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
<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>
@@ -136,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>
@@ -146,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,50 +0,0 @@
import LifeCounter from '../LifeCounter/LifeCounter';
import { Player as PlayerType } from '../../Types/Player';
import { twc } from 'react-twc';
const getGridArea = (player: PlayerType) => {
switch (player.index) {
case 0:
return 'grid-in-player0';
case 1:
return 'grid-in-player1';
case 2:
return 'grid-in-player2';
case 3:
return 'grid-in-player3';
case 4:
return 'grid-in-player4';
case 5:
return 'grid-in-player5';
default:
throw new Error('Invalid player index');
}
};
const PlayerWrapper = twc.div`w-full h-full bg-black`;
export const Player = (players: PlayerType[], gridClasses: string) => {
return (
<PlayerWrapper>
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => {
const gridArea = getGridArea(player);
return (
<div
key={player.index}
className={`flex justify-center items-center align-middle ${gridArea}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</div>
);
})}
</div>
</PlayerWrapper>
);
};

View File

@@ -1,98 +1,175 @@
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 {
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;
items-center height: 100%;
justify-center width: 100%;
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-end justify-content: end;
items-stretch align-items: stretch;
`; `;
const TogglesSection = twc.div` const TogglesSection = styled.div`
flex display: flex;
relative position: relative;
flex-row flex-direction: row;
gap-2 gap: 0.5rem;
justify-evenly justify-content: space-evenly;
`; `;
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;
`; `;
const ColorPicker = twc.input` const ColorPicker = styled.input`
absolute position: absolute;
top-[5%] top: 5%;
left-[5%] left: 5%;
h-[8vmax] height: 8vmax;
w-[8vmax] width: 8vmax;
border-none border: none;
outline-none outline: none;
cursor-pointer cursor: pointer;
bg-transparent background-color: transparent;
user-select-none user-select: none;
text-common-white 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 dialogRef = useRef<HTMLDialogElement | null>(null); const dialogRef = useRef<HTMLDialogElement | null>(null);
@@ -101,6 +178,9 @@ const PlayerMenu = ({
containerRef: settingsContainerRef, containerRef: settingsContainerRef,
}); });
const handleOnClick = () => {
setShowPlayerMenu(false);
};
const { fullscreen, wakeLock, goToStart } = useGlobalSettings(); const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers(); const { updatePlayer, resetCurrentGame } = usePlayers();
@@ -132,28 +212,26 @@ 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}
> >
<ColorPicker <ColorPicker
@@ -166,7 +244,7 @@ const PlayerMenu = ({
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
{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}
@@ -194,7 +272,7 @@ const PlayerMenu = ({
</CheckboxContainer> </CheckboxContainer>
)} )}
<CheckboxContainer> <CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
checked={player.settings.usePoison} checked={player.settings.usePoison}
@@ -221,7 +299,7 @@ const PlayerMenu = ({
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer> <CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
checked={player.settings.useEnergy} checked={player.settings.useEnergy}
@@ -248,7 +326,7 @@ const PlayerMenu = ({
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer> <CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
checked={player.settings.useExperience} checked={player.settings.useExperience}
@@ -275,7 +353,8 @@ const PlayerMenu = ({
/> />
</CheckboxContainer> </CheckboxContainer>
</TogglesSection> </TogglesSection>
<ButtonsSections className="mt-4"> <Spacer height="1rem" />
<ButtonsSections>
<Button <Button
variant="text" variant="text"
style={{ style={{
@@ -287,7 +366,7 @@ const PlayerMenu = ({
> >
<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}
@@ -339,11 +418,18 @@ const PlayerMenu = ({
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={dialogRef} ref={dialogRef}
className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute bottom-[20%]" style={{
zIndex: 9999,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRadius: '1rem',
border: 'none',
position: 'absolute',
top: '10%',
}}
> >
<div className="h-full flex flex-col p-4 gap-2"> <h1>Reset Game?</h1>
<h1 className="text-center">Reset Game?</h1> <div style={{ display: 'flex', justifyContent: 'space-evenly' }}>
<div className="flex justify-evenly gap-4">
<Button <Button
variant="contained" variant="contained"
onClick={() => dialogRef.current?.close()} onClick={() => dialogRef.current?.close()}
@@ -360,7 +446,6 @@ const PlayerMenu = ({
Yes Yes
</Button> </Button>
</div> </div>
</div>
</dialog> </dialog>
</SettingsContainer> </SettingsContainer>
</PlayerMenuWrapper> </PlayerMenuWrapper>

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 { Player } from '../Player/Player';
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 = Player(players, 'grid-areas-onePlayerPortrait');
}
Layout = Player(players, 'grid-areas-onePlayerLandscape');
break;
case 2:
switch (initialGameSettings?.orientation) {
case Orientation.Portrait:
Layout = Player(players, 'grid-areas-twoPlayersOppositePortrait');
break;
default:
case Orientation.Landscape:
Layout = Player(players, 'grid-areas-twoPlayersSameSideLandscape');
break;
case Orientation.OppositeLandscape:
Layout = Player(players, 'grid-areas-twoPlayersOppositeLandscape');
break;
}
break;
case 3:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-threePlayersSide');
break;
}
Layout = Player(players, 'grid-areas-threePlayers');
break;
default:
case 4:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-fourPlayerPortrait');
break;
}
Layout = Player(players, 'grid-areas-fourPlayer');
break;
case 5:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-fivePlayersSide');
break;
}
Layout = Player(players, 'grid-areas-fivePlayers');
break;
case 6:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-sixPlayersSide');
break;
}
Layout = Player(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`; 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

@@ -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,34 +162,35 @@ 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
@@ -210,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,
@@ -229,7 +232,6 @@ export const createInitialPlayers = ({
extraCounters: [], extraCounters: [],
commanderDamage, commanderDamage,
hasLost: false, hasLost: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
}; };
players.push(player); players.push(player);

View File

@@ -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,
@@ -37,36 +33,12 @@ export const GlobalSettingsProvider = ({
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true } : { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true }
); );
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
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));
@@ -95,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');
@@ -109,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();

View File

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

View File

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

View File

@@ -1,10 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body { body {
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',
@@ -12,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,102 +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';
/** @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: {
primary: {
main: '#3E7D78',
dark: '#2D5F5B',
},
secondary: {
main: '#284F4C',
dark: '#1B3B38',
},
background: {
default: '#08253B',
backdrop: 'rgba(0, 0, 0, 0.3)',
settings: 'rgba(20, 20, 0, 0.9)',
},
text: {
primary: '#F5F5F5',
secondary: '#76A6A5',
},
action: {
disabled: '#234A47',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#000000',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
},
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