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,1705225255906,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.json,1705225255906,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
assets/index-08359bdb.css,1705225256081,d2766260d28230d960d75362810713efaddf40687205e697432b52869f162af7
logo192.png,1705225255905,3b0fcf91fe2128f493de0bce2f6e2d35520a4260a04e05b8d855181359b3d3fe
favicon.ico,1705225255905,75661e6187b524767554b4f28ec09a64bc72b0bb102a0b453aaead88519d9ed3
logo512.png,1705225255906,cf49739c9e6890bbfcd4157f299dde425df60759b7320ae9188d7ab9dc51e8ca
assets/index-20658f4b.js,1705225256081,742f2c10740beea3a23f269aa6266b3c288d1fd9c7e20b6829034e8a898bf1e1
robots.txt,1693082171694,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.json,1693082171694,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
assets/index-5265c558.css,1693082171837,08c4451946bbdf520fe337edb365417a8bbf91914c018b83866723ef52d57b43
index.html,1693082171837,09e1919fbaaa3a0bf08f43eb46c29136d62a7747b41f8b5d0f4a7ed23337c344
logo192.png,1693082171693,4309255bccbdbb341b5ab88708677e3d43b9e171d2666528ff932295a8257e4e
favicon.ico,1693082171692,48d8c1b9714dbc9bcb012d9c9f04112d229f20e6c889bda588ac159f973e6a8d
logo512.png,1693082171694,92c7c05dc98170596d04f48e5e60eaae9535f409bcaeff129fd98fef8aba9f4e
assets/index-5023e89e.js,1693082171838,8a6177168e95e1ca90e5ad8774252a8a02a9a78765bd329b7deae729c01aedf3

View File

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

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

1
env.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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 { TwcComponentProps, twc } from 'react-twc';
import { OutlinedText } from '../Misc/OutlinedText';
import { decrementTimeoutMs } from '../../Data/constants';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText';
export type RotationDivProps = TwcComponentProps<'div'> & {
$rotation?: number;
};
const CommanderDamageContainer = styled.div<{
$rotation: number;
}>`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
export type RotationSpanProps = TwcComponentProps<'span'> & {
$rotation?: number;
};
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
`;
}
}}
`;
export type RotationButtonProps = TwcComponentProps<'button'> & {
$rotation?: number;
};
const CommanderDamageButton = styled.button<{
$backgroundColor?: string;
$rotation: number;
}>`
display: flex;
flex-grow: 1;
border: none;
height: 10vmin;
width: 50%;
outline: none;
cursor: pointer;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
margin: 0;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 0;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: 6vmax;
height: auto;
`;
}
}}
`;
const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col'
: 'flex-row',
]);
const CommanderDamageTextContainer = styled.div<{
$rotation: number;
}>`
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-variant-numeric: tabular-nums;
pointer-events: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
const CommanderDamageButton = twc.button<RotationButtonProps>((props) => [
'flex flex-grow border-none outline-none cursor-pointer m-0 p-0 webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-[6vmax] h-auto'
: 'h-[10vmin] w-1/2',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: 270deg;
`;
}
}}
`;
const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
'relative top-1/2 left-1/2 tabular-nums pointer-events-none select-none webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'rotate-[270deg]'
: '',
]);
const PartnerDamageSeperator = styled.div<{
$rotation: number;
}>`
width: 1px;
background-color: rgba(0, 0, 0, 1);
const PartnerDamageSeperator = twc.div<RotationDivProps>((props) => [
'bg-black',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-full h-px'
: 'w-px',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: auto;
height: 1px;
`;
}
}}
`;
type CommanderDamageButtonComponentProps = {
player: Player;
@@ -66,6 +127,10 @@ export const CommanderDamage = ({
const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false);
const isSide =
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped;
const handleCommanderDamageChange = (
index: number,
increment: number,
@@ -128,9 +193,9 @@ export const CommanderDamage = ({
};
const opponentIndex = opponent.index;
const fontSize = player.isSide ? '4vmax' : '7vmin';
const fontSize = isSide ? '4vmax' : '7vmin';
const fontWeight = 'bold';
const strokeWidth = player.isSide ? '0.4vmax' : '0.7vmin';
const strokeWidth = isSide ? '0.4vmax' : '0.7vmin';
return (
<CommanderDamageContainer
@@ -149,8 +214,8 @@ export const CommanderDamage = ({
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}}
$backgroundColor={opponent.color}
aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
>
<CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText
@@ -183,8 +248,8 @@ export const CommanderDamage = ({
) => {
e.preventDefault();
}}
$backgroundColor={opponent.color}
aria-label={`Partner Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
>
<CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText

View File

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

View File

@@ -1,42 +1,64 @@
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 { Rotation } from '../../Types/Player';
type RotationButtonProps = TwcComponentProps<'div'> & {
$align?: string;
$rotation?: number;
};
const LifeCounterButtonTwc = twc.button`
h-full
w-full
flex
text-lifeCounter-text
font-semibold
bg-transparent
border-none
outline-none
cursor-pointer
justify-center
items-center
select-none
webkit-user-select-none
export const StyledLifeCounterButton = styled.button`
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
`;
const TextContainer = twc.div<RotationButtonProps>((props) => [
'relative',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? props.$align === 'right'
? '-rotate-90 bottom-1/4 top-auto'
: '-rotate-90 top-1/4'
: 'top-auto',
props.$rotation === Rotation.Flipped || props.$rotation === Rotation.Normal
? props.$align === 'right'
? 'left-1/4'
: 'right-1/4'
: '',
]);
const TextContainer = styled.div<{
$align?: string;
$rotation: number;
}>`
position: relative;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
if (props.$align === 'right') {
return css`
rotate: -90deg;
bottom: 25%;
top: auto;
`;
}
return css`
rotate: -90deg;
top: 25%;
`;
}
if (props.$align === 'right') {
return css`
left: 25%;
`;
}
return css`
right: 25%;
`;
}}
`;
type LifeCounterButtonProps = {
lifeTotal: number;
@@ -91,7 +113,7 @@ const LifeCounterButton = ({
: '12vmin';
return (
<LifeCounterButtonTwc
<StyledLifeCounterButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput}
@@ -107,7 +129,7 @@ const LifeCounterButton = ({
>
{operation === 'add' ? '\u002B' : '\u2212'}
</TextContainer>
</LifeCounterButtonTwc>
</StyledLifeCounterButton>
);
};

View File

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

View File

@@ -1,14 +1,37 @@
import { twc } from 'react-twc';
import { Cog } from '../../Icons/generated';
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import { RotationButtonProps } from './CommanderDamage';
import { Cog } from '../../Icons/generated';
const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none',
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped
? `right-auto top-[1vmax] left-[27%]`
: 'top-1/4 right-[1vmax]',
]);
export const StyledSettingsButton = styled.button<{ $rotation: Rotation }>`
position: absolute;
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
top: 25%;
right: 1vmax;
background-color: transparent;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
z-index: 1;
${(props) => {
if (
props.$rotation === Rotation.Side ||
props.$rotation === Rotation.SideFlipped
) {
return css`
right: auto;
top: 1vmax;
left: 27%;
`;
}
}}
`;
type SettingsButtonProps = {
onClick: () => void;
@@ -17,13 +40,13 @@ type SettingsButtonProps = {
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => {
return (
<SettingsButtonTwc
<StyledSettingsButton
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<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 { CommanderDamage, RotationDivProps } from '../Buttons/CommanderDamage';
import styled from 'styled-components';
import { css } from 'styled-components';
import { CommanderDamage } from '../Buttons/CommanderDamage';
const CommanderDamageGrid = twc.div<RotationDivProps>((props) => [
'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col h-full w-auto'
: 'flex-row w-full',
]);
const CommanderDamageGrid = styled.div<{ $rotation: number }>`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
height: 100%;
width: auto;
`;
}
}}
`;
type CommanderDamageBarProps = {
opponents: Player[];

View File

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

View File

@@ -1,5 +1,8 @@
import { twc } from 'react-twc';
import { usePlayers } from '../../Hooks/usePlayers';
import { CounterType, Player } from '../../Types/Player';
import ExtraCounter from '../Buttons/ExtraCounter';
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import {
CommanderTax,
Energy,
@@ -7,23 +10,52 @@ import {
PartnerTax,
Poison,
} from '../../Icons/generated';
import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { GameFormat } from '../../Types/Settings';
const Container = twc.div<RotationDivProps>((props) => [
'flex',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'h-full w-[8vmax]'
: 'h-[20vmin] w-full',
]);
const Container = styled.div<{ $rotation: Rotation }>`
width: 100%;
height: 20vmin;
display: flex;
export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [
'flex absolute flex-row flex-grow pointer-events-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse h-full w-auto bottom-auto'
: 'w-full bottom-0',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
height: 100%;
width: 9.3vmax;
`;
}
}}
`;
export const ExtraCountersGrid = styled.div<{ $rotation: Rotation }>`
display: flex;
position: absolute;
width: 100%;
flex-direction: row;
flex-grow: 1;
bottom: 0;
pointer-events: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
height: 100%;
width: auto;
bottom: auto;
right: -6px;
`;
}
}}
`;
type ExtraCountersBarProps = {
player: Player;
@@ -31,6 +63,7 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers();
const { initialGameSettings } = useGlobalSettings();
const handleCounterChange = (
updatedCounterTotal: number,
@@ -87,6 +120,9 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
return null;
}
const isTwoHeadedGiant =
initialGameSettings?.gameFormat === GameFormat.TwoHeadedGiant;
return (
<Container $rotation={player.settings.rotation}>
<ExtraCountersGrid $rotation={player.settings.rotation}>
@@ -100,7 +136,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'commanderTax'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -115,7 +150,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'partnerTax'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -129,7 +163,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
player.extraCounters?.find((counter) => counter.type === 'poison')
?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -143,7 +176,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
player.extraCounters?.find((counter) => counter.type === 'energy')
?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -158,7 +190,6 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'experience'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>

View File

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

View File

@@ -1,36 +1,137 @@
import { useEffect, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import styled, { css, keyframes } from 'styled-components';
import { theme } from '../../Data/theme';
import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar';
import PlayerMenu from '../Player/PlayerMenu';
import PlayerMenu from '../PlayerMenu/PlayerMenu';
import Health from './Health';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
const LifeCounterContentWrapper = twc.div`
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
const LifeCounterContentWrapper = styled.div<{
$backgroundColor: string;
}>`
position: relative;
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
@media (orientation: landscape) {
max-width: 100vmax;
max-height: 100vmin;
}
const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [
'relative flex items-center w-full h-full z-[1]',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `flex-row`
: `flex-col`,
]);
overflow: hidden;
`;
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-[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]`
: '',
]);
z-index: 1;
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 commanderDamageTotals = player.commanderDamage.map(
@@ -71,51 +172,14 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0);
const [differenceKey, setDifferenceKey] = useState(Date.now());
const [isLandscape, setIsLandscape] = useState(false);
const calcRot = player.isSide
? player.settings.rotation - 180
: player.settings.rotation;
const rotationAngle = isLandscape ? calcRot : calcRot + 90;
const handlers = useSwipeable({
trackMouse: true,
onSwipedDown: () => {
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(() => {
const timer = setTimeout(() => {
setRecentDifference(0);
}, 3_000);
const resizeObserver = new ResizeObserver(() => {
if (document.body.clientWidth > document.body.clientHeight)
setIsLandscape(true);
else setIsLandscape(false);
return;
});
resizeObserver.observe(document.body);
return () => {
clearTimeout(timer);
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference, document.body.clientHeight, document.body.clientWidth]);
return () => clearTimeout(timer);
}, [recentDifference]);
useEffect(() => {
if (player.showStartingPlayer) {
@@ -144,43 +208,27 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
updatePlayer(updatedPlayer);
};
const calcRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 90
: player.settings.rotation;
const calcTextRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
return (
<LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
{...handlers}
>
<LifeCounterContentWrapper $backgroundColor={player.color}>
<LifeCounterWrapper $rotation={player.settings.rotation}>
{settings.showStartingPlayer &&
player.isStartingPlayer &&
player.showStartingPlayer && (
<StartingPlayerNoticeWrapper
style={{ rotate: `${calcRotation}deg` }}
>
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
<PlayerNoticeWrapper
$rotation={player.settings.rotation}
$backgroundColor={theme.palette.primary.main}
>
<DynamicText $rotation={player.settings.rotation}>
You start!
</DynamicText>
</StartingPlayerNoticeWrapper>
</PlayerNoticeWrapper>
)}
{player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} />
<PlayerNoticeWrapper
$rotation={player.settings.rotation}
$backgroundColor={'#00000070'}
/>
)}
<CommanderDamageBar
opponents={opponents}
@@ -188,6 +236,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
key={player.index}
handleLifeChange={handleLifeChange}
/>
<SettingsButton
onClick={() => {
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}
/>
{playerCanLose(player) && (
<LoseGameButton
rotation={player.settings.rotation}
@@ -202,13 +256,11 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange}
/>
<ExtraCountersBar player={player} />
<PlayerMenu
isShown={showPlayerMenu}
player={player}
setShowPlayerMenu={setShowPlayerMenu}
/>
</LifeCounterWrapper>
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)}
</LifeCounterContentWrapper>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,38 @@
import { Button, FormLabel, Modal, Switch } from '@mui/material';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { ModalWrapper } from './InfoModal';
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { theme } from '../../Data/theme';
import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
import { useEffect, useState } from 'react';
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 = {
isOpen: boolean;
@@ -21,65 +41,12 @@ type SettingsModalProps = {
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA } = useGlobalSettings();
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
useEffect(() => {
if (!isOpen) {
return;
}
async function checkIfLatestVersion() {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
/* @ts-expect-error is defined in vite.config.ts*/
Authorization: `Bearer ${REPO_READ_ACCESS_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setNewVersion(undefined);
setIsLatestVersion(false);
return;
}
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) {
setIsLatestVersion(true);
return;
}
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
checkIfLatestVersion();
}, [isOpen]);
return (
<Modal open={isOpen} onClose={closeModal}>
<>
<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>
<Container>
<h2 className="text-center text-2xl mb-2"> Settings </h2>
<Separator height="1px" />
<h2 style={{ textAlign: 'center' }}> Settings </h2>
<SettingContainer>
<ToggleContainer>
<FormLabel>Show Start Player</FormLabel>
@@ -104,16 +71,13 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
<Switch
checked={settings.keepAwake}
onChange={() => {
setSettings({
...settings,
keepAwake: !settings.keepAwake,
});
setSettings({ ...settings, keepAwake: !settings.keepAwake });
}}
/>
</ToggleContainer>
<Description>
Will prevent device from going to sleep while this app is open
if this is enabled.
Will prevent device from going to sleep while this app is open if
this is enabled.
</Description>
</SettingContainer>
<SettingContainer>
@@ -136,7 +100,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer>
{!isPWA && (
<>
<Separator height="1px" />
<Separator height="2px" />
<SettingContainer>
<ToggleContainer>
<Paragraph>
@@ -146,50 +110,24 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
normal app!
</Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will
be automatically hidden.
<Description>
If you do, this app will work offline and the toolbar will be
automatically hidden.
</Description>
</SettingContainer>
</>
)}
<Separator height="1px" />
<Separator height="2px" />
<SettingContainer>
<Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
)}
<Paragraph>Version: 0.4.0</Paragraph>
</SettingContainer>
{!isLatestVersion && newVersion && (
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)}
<Separator height="1px" />
<Separator height="2px" />
<Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
<Button variant="contained" onClick={closeModal}>
Save and Close
</Button>
</Container>
</ModalWrapper>
</>
</Modal>
);
};

View File

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

View File

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

View File

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

View File

@@ -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 { useRef } from 'react';
import { twc } from 'react-twc';
import styled, { css } from 'styled-components';
import { Player, Rotation } from '../../Types/Player';
import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import {
Energy,
Exit,
Experience,
FullscreenOff,
FullscreenOn,
PartnerTax,
Poison,
Energy,
Experience,
Exit,
FullscreenOff,
FullscreenOn,
Cross,
ResetGame,
} from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import { useRef } from 'react';
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`
flex
flex-col
absolute
w-full
h-full
bg-background-settings
items-center
justify-center
z-[2]
webkit-user-select-none
transition-all
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
height: 100%;
width: 100%;
`;
}
}}
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const BetterRowContainer = twc.div`
flex
flex-col
flex-grow
w-full
h-full
justify-end
items-stretch
const BetterRowContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
height: 100%;
justify-content: end;
align-items: stretch;
`;
const TogglesSection = twc.div`
flex
relative
flex-row
gap-2
justify-evenly
const TogglesSection = styled.div`
display: flex;
position: relative;
flex-direction: row;
gap: 0.5rem;
justify-content: space-evenly;
`;
const ButtonsSections = twc.div`
flex
max-w-full
gap-4
justify-between
p-[3%]
items-center
const ButtonsSections = styled.div`
display: flex;
max-width: 100%;
gap: 1rem;
justify-content: space-between;
padding: 3% 3%;
align-items: center;
`;
const ColorPicker = twc.input`
absolute
top-[5%]
left-[5%]
h-[8vmax]
w-[8vmax]
border-none
outline-none
cursor-pointer
bg-transparent
user-select-none
text-common-white
const ColorPicker = styled.input`
position: absolute;
top: 5%;
left: 5%;
height: 8vmax;
width: 8vmax;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
user-select: none;
color: #ffffff;
`;
const SettingsContainer = twc.div<RotationDivProps>((props) => [
'flex flex-wrap h-full w-full',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col'
: 'flex-row',
]);
const CheckboxContainer = styled.div<{ $rotation: Rotation }>`
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
/* rotate: ${props.$rotation - 180}deg; */
`;
}
}}
`;
const PlayerMenuWrapper = styled.div<{
$rotation: Rotation;
}>`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(20, 20, 20, 0.9);
align-items: center;
justify-content: center;
z-index: 2;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return;
}
return css`
rotate: ${props.$rotation}deg;
`;
}};
`;
const CloseButton = styled.div<{
$rotation: Rotation;
}>`
position: absolute;
top: 15%;
right: 5%;
z-index: 9999;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
top: 5%;
right: auto;
left: 5%;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
top: auto;
left: auto;
bottom: 5%;
right: 5%;
`;
}
}}
`;
type PlayerMenuProps = {
player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean;
};
const PlayerMenu = ({
player,
setShowPlayerMenu,
isShown,
}: PlayerMenuProps) => {
const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const dialogRef = useRef<HTMLDialogElement | null>(null);
@@ -101,6 +178,9 @@ const PlayerMenu = ({
containerRef: settingsContainerRef,
});
const handleOnClick = () => {
setShowPlayerMenu(false);
};
const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers();
@@ -132,28 +212,26 @@ const PlayerMenu = ({
const buttonFontSize = isSide ? '1.5vmax' : '3vmin';
const iconSize = isSide ? '6vmin' : '3vmax';
const extraCountersSize = isSide ? '8vmin' : '4vmax';
const calcRotation =
player.settings.rotation === Rotation.Side
? `${player.settings.rotation - 180}deg`
: player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '';
const closeButtonSize = isSide ? '6vmin' : '3vmax';
return (
<PlayerMenuWrapper
//TODO: Fix hacky solution to rotation for SideFlipped
<PlayerMenuWrapper $rotation={player.settings.rotation}>
<CloseButton $rotation={player.settings.rotation}>
<Button
variant="text"
onClick={handleOnClick}
style={{
rotate:
player.settings.rotation === Rotation.SideFlipped ? `180deg` : '',
translate: isShown ? '' : player.isSide ? `-100%` : `0 -100%`,
margin: 0,
padding: 0,
height: closeButtonSize,
width: closeButtonSize,
}}
>
<Cross size={closeButtonSize} />
</Button>
</CloseButton>
<SettingsContainer
$rotation={player.settings.rotation}
style={{
rotate: calcRotation,
}}
ref={settingsContainerRef}
>
<ColorPicker
@@ -166,7 +244,7 @@ const PlayerMenu = ({
<BetterRowContainer>
<TogglesSection>
{player.settings.useCommanderDamage && (
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="usePartner"
checked={player.settings.usePartner}
@@ -194,7 +272,7 @@ const PlayerMenu = ({
</CheckboxContainer>
)}
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="usePoison"
checked={player.settings.usePoison}
@@ -221,7 +299,7 @@ const PlayerMenu = ({
/>
</CheckboxContainer>
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="useEnergy"
checked={player.settings.useEnergy}
@@ -248,7 +326,7 @@ const PlayerMenu = ({
/>
</CheckboxContainer>
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="useExperience"
checked={player.settings.useExperience}
@@ -275,7 +353,8 @@ const PlayerMenu = ({
/>
</CheckboxContainer>
</TogglesSection>
<ButtonsSections className="mt-4">
<Spacer height="1rem" />
<ButtonsSections>
<Button
variant="text"
style={{
@@ -287,7 +366,7 @@ const PlayerMenu = ({
>
<Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button>
<CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}>
<Checkbox
name="fullscreen"
checked={document.fullscreenElement ? true : false}
@@ -339,11 +418,18 @@ const PlayerMenu = ({
</BetterRowContainer>
<dialog
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 className="text-center">Reset Game?</h1>
<div className="flex justify-evenly gap-4">
<h1>Reset Game?</h1>
<div style={{ display: 'flex', justifyContent: 'space-evenly' }}>
<Button
variant="contained"
onClick={() => dialogRef.current?.close()}
@@ -360,7 +446,6 @@ const PlayerMenu = ({
Yes
</Button>
</div>
</div>
</dialog>
</SettingsContainer>
</PlayerMenuWrapper>

View File

@@ -1,67 +1,24 @@
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Orientation } from '../../Types/Settings';
import { Player } from '../Player/Player';
import { twc } from 'react-twc';
import styled from 'styled-components';
import Counters from '../Counters/Counters';
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`;
const MainWrapper = styled.div`
width: 100vmax;
height: 100vmin;
width: 100dvmax;
height: 100dvmin;
overflow: hidden;
`;
export const Play = () => {
const { players } = usePlayers();
const { initialGameSettings } = useGlobalSettings();
let Layout: JSX.Element;
switch (players.length) {
case 1:
if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = 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>;
type PlayProps = {
gridAreas: string;
};
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 styled from 'styled-components';
import { GridTemplateAreas } from '../../../Data/GridTemplateAreas';
import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import { theme } from '../../../Data/theme';
import {
FivePlayers,
FourPlayers,
FourPlayersSide,
OnePlayerPortrait,
SixPlayers,
ThreePlayers,
ThreePlayersSide,
TwoPlayersOppositeLandscape,
TwoPlayersOppositePortrait,
ThreePlayers,
ThreePlayersSide,
FourPlayers,
FourPlayersSide,
FivePlayers,
SixPlayers,
TwoPlayersSameSide,
} from '../../../Icons/generated/Layouts';
import { twc } from 'react-twc';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
import { Orientation } from '../../../Types/Settings';
const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
const LayoutWrapper = styled.div`
flex-direction: row;
display: flex;
justify-content: space-evenly;
`;
type LayoutOptionsProps = {
numberOfPlayers: number;
selectedOrientation: Orientation;
onChange: (orientation: Orientation) => void;
gridAreas: GridTemplateAreas;
onChange: (gridAreas: GridTemplateAreas) => void;
};
export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
const LayoutOptions: React.FC<LayoutOptionsProps> = ({
numberOfPlayers,
selectedOrientation,
gridAreas,
onChange,
}) => {
const iconWidth = '21vmin';
const iconHeight = '40vmin';
const iconMaxWidth = '124px';
const iconMaxHeight = '196px';
const iconHeight = '30vmin';
const iconWidth = '20vmin';
const renderLayoutOptions = () => {
switch (numberOfPlayers) {
case 1:
return (
<div>
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.OnePlayerLandscape}
control={
<Radio
icon={
@@ -60,13 +61,12 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/>
}
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.OnePlayerPortrait}
control={
<Radio
icon={
@@ -84,21 +84,19 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/>
}
TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/>
}
label=""
/>
</div>
</>
);
case 2:
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.TwoPlayersSameSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<TwoPlayersSameSide
height={iconHeight}
@@ -119,10 +117,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.TwoPlayersOppositePortrait}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<TwoPlayersOppositePortrait
height={iconHeight}
@@ -143,10 +140,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.OppositeLandscape}
value={GridTemplateAreas.TwoPlayersOppositeLandscape}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<TwoPlayersOppositeLandscape
height={iconHeight}
@@ -172,10 +168,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.ThreePlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<ThreePlayers
height={iconHeight}
@@ -196,10 +191,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.ThreePlayersSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<ThreePlayersSide
height={iconHeight}
@@ -226,10 +220,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.FourPlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FourPlayers
height={iconHeight}
@@ -250,10 +243,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label=""
/>
<FormControlLabel
value={Orientation.Portrait}
value={GridTemplateAreas.FourPlayersSide}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FourPlayersSide
height={iconHeight}
@@ -280,10 +272,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.FivePlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<FivePlayers
height={iconHeight}
@@ -333,10 +324,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return (
<>
<FormControlLabel
value={Orientation.Landscape}
value={GridTemplateAreas.SixPlayers}
control={
<Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={
<SixPlayers
height={iconHeight}
@@ -392,9 +382,9 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
<RadioGroup
row
onChange={(_e, value) => {
onChange(value as Orientation);
onChange(value as GridTemplateAreas);
}}
value={selectedOrientation}
value={gridAreas}
style={{ justifyContent: 'center' }}
>
{renderLayoutOptions()}
@@ -402,3 +392,5 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
</LayoutWrapper>
);
};
export default LayoutOptions;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 { useWakeLock } from 'react-screen-wake-lock';
import {
GlobalSettingsContext,
GlobalSettingsContextType,
} from '../Contexts/GlobalSettingsContext';
import { useWakeLock } from 'react-screen-wake-lock';
import { useAnalytics } from '../Hooks/useAnalytics';
import {
InitialGameSettings,
InitialGameSettingsSchema,
Settings,
} from '../Types/Settings';
import { InitialGameSettings, Settings } from '../Types/Settings';
export const GlobalSettingsProvider = ({
children,
@@ -37,36 +33,12 @@ export const GlobalSettingsProvider = ({
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true }
);
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
setShowPlay(false);
};
useEffect(() => {
if (savedGameSettings && JSON.parse(savedGameSettings).gridAreas) {
removeLocalStorage();
window.location.reload();
return;
}
//parse existing game settings with zod schema
const parsedInitialGameSettings =
InitialGameSettingsSchema.safeParse(initialGameSettings);
if (!parsedInitialGameSettings.success) {
removeLocalStorage();
window.location.reload();
return;
}
localStorage.setItem(
'initialGameSettings',
JSON.stringify(initialGameSettings)
);
}, [initialGameSettings, savedGameSettings]);
}, [initialGameSettings]);
useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings));
@@ -95,6 +67,14 @@ export const GlobalSettingsProvider = ({
request();
}
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
setShowPlay(localStorage.getItem('showPlay') === 'true' ?? false);
};
const ctxValue = useMemo((): GlobalSettingsContextType => {
const goToStart = async () => {
const currentPlayers = localStorage.getItem('players');
@@ -109,6 +89,7 @@ export const GlobalSettingsProvider = ({
};
const toggleWakeLock = async () => {
console.log('on press', active);
if (active) {
setSettings({ ...settings, keepAwake: false });
release();

View File

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

View File

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

View File

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

View File

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

8867
yarn.lock

File diff suppressed because it is too large Load Diff