Compare commits

...

95 Commits

Author SHA1 Message Date
Viktor Rådberg
354c0dbbb2 bump 2024-01-20 11:11:03 +01:00
Viktor Rådberg
3770d13beb fix some styling 2024-01-20 10:56:53 +01:00
Viktor Rådberg
13733242a2 bump 2024-01-14 14:39:20 +01:00
Viktor Rådberg
81f3891b20 add better pwa support 2024-01-14 14:38:56 +01:00
Viktor Rådberg
e153de9093 Release 0.5.51 2024-01-14 13:42:11 +01:00
Viktor Rådberg
07775f85d2 fix start menu style 2024-01-14 13:41:51 +01:00
Viktor Rådberg
10039175a1 bump 2024-01-14 13:14:53 +01:00
Viktor Rådberg
bcf2a0a840 new colors 2024-01-14 13:14:33 +01:00
Viktor Rådberg
d25da5d97b fix styling 2024-01-14 12:31:57 +01:00
Viktor Rådberg
f5a80e573e cache 2024-01-14 10:41:14 +01:00
Viktor Rådberg
1f36264e39 update package json 2024-01-14 10:40:48 +01:00
Viktor Rådberg
d615cfd3ba reset game styling 2024-01-14 10:38:28 +01:00
Viktor Rådberg
4453b12ce6 bump 2024-01-13 20:32:10 +01:00
Viktor Rådberg
d601a820f8 remove log 2024-01-13 20:32:00 +01:00
Viktor Rådberg
0455f43794 test 2024-01-13 20:26:07 +01:00
Viktor Rådberg
f94103fe51 fix 2024-01-13 20:20:50 +01:00
Viktor Rådberg
c36668b933 fix 2024-01-13 20:20:20 +01:00
Viktor Rådberg
f9d0346300 bump 2024-01-13 20:18:50 +01:00
Viktor Rådberg
2f3ee74c74 test 2024-01-13 20:15:43 +01:00
Viktor Rådberg
f8f0788b97 bump 2024-01-13 19:53:02 +01:00
Viktor Rådberg
bfe25eacb7 fix lint 2024-01-13 19:44:27 +01:00
Viktor Rådberg
7b0965c0dd test release 2024-01-13 19:40:36 +01:00
Viktor Rådberg
e55ea6a83a final test 2024-01-13 19:32:16 +01:00
Viktor Rådberg
481196de9b test 2024-01-13 19:30:42 +01:00
Viktor Rådberg
a136dbd3f9 test 2024-01-13 19:28:50 +01:00
Viktor Rådberg
8d23349dac test 2024-01-13 19:27:25 +01:00
Viktor Rådberg
a7caa46156 test 2024-01-13 19:22:31 +01:00
Viktor Rådberg
39cd3faae2 test 2024-01-13 19:19:30 +01:00
Viktor Rådberg
bdaa8e602f test 2024-01-13 19:18:45 +01:00
Viktor Rådberg
26490103a9 Merge pull request #30 from Vikeo/develop
test pr
2024-01-13 19:14:03 +01:00
Viktor Rådberg
56b07784d5 fix 2024-01-13 19:13:34 +01:00
Viktor Rådberg
4544c689a5 test 2024-01-13 19:12:11 +01:00
Viktor Rådberg
8a7a4b4127 test 2024-01-13 19:11:49 +01:00
Viktor Rådberg
391e654779 test 2024-01-13 18:58:07 +01:00
Viktor Rådberg
f79a0d3e7e test 2024-01-13 18:55:41 +01:00
Viktor Rådberg
0664e340a0 test 2024-01-13 18:49:06 +01:00
Viktor Rådberg
dcb98aeac6 test 2024-01-13 18:48:11 +01:00
Viktor Rådberg
89b62ddac4 release 2024-01-13 18:37:12 +01:00
Viktor Rådberg
c704e3c7f4 release workflow 2024-01-13 18:26:34 +01:00
Viktor Rådberg
69a71e2d6e version check 2024-01-13 18:17:24 +01:00
Viktor Rådberg
18945204bf test 2024-01-13 18:03:59 +01:00
Viktor Rådberg
495e731636 tet 2024-01-13 17:57:03 +01:00
Viktor Rådberg
67b231f0d4 more logs 2024-01-13 17:12:31 +01:00
Viktor Rådberg
9d42fb1635 check version 2024-01-13 17:07:03 +01:00
Viktor Rådberg
38ad046344 test 2024-01-13 15:39:56 +01:00
Viktor Rådberg
bc87f073af test 2024-01-13 15:35:42 +01:00
Viktor Rådberg
da46c25944 test version update 2024-01-13 15:34:24 +01:00
Viktor Rådberg
104f54f5b7 Merge pull request #29 from Vikeo/develop
Release 0.5.0
2024-01-13 15:13:52 +01:00
Viktor Rådberg
101a055694 bump version 2024-01-13 15:09:58 +01:00
Viktor Rådberg
38e4cb8e8c dvh 2024-01-13 15:02:33 +01:00
Viktor Rådberg
4ecb83060d fix webkit highlight 2024-01-13 14:55:39 +01:00
Viktor Rådberg
4f231ba6f4 finish up tailwind 2024-01-13 14:50:36 +01:00
Viktor Rådberg
3cd982c643 wip tailwind 2024-01-07 19:02:07 +01:00
Viktor Rådberg
1013914cdf commander damage tailwind 2024-01-07 00:15:37 +01:00
Viktor Rådberg
db85fc2102 fix some tailwind 2024-01-06 19:45:25 +01:00
Viktor Rådberg
2b0d8102d8 fix version 2024-01-06 01:14:30 +01:00
Viktor Rådberg
35e0224066 fix backdrop 2024-01-06 00:47:21 +01:00
Viktor Rådberg
1fa433a38f remove styled components global styles 2024-01-06 00:45:35 +01:00
Viktor Rådberg
26821273d7 Merge branch 'develop' 2023-12-27 21:39:59 +01:00
Viktor Rådberg
7f19214624 reload window 2023-12-27 21:39:50 +01:00
Viktor Rådberg
8b2cd43a96 Merge pull request #28 from Vikeo/develop
New release
2023-12-27 21:31:40 +01:00
Viktor Rådberg
23e18f8f41 fix styling 2023-12-27 21:29:28 +01:00
Viktor Rådberg
23b844c47e fix 2023-12-27 21:11:44 +01:00
Viktor Rådberg
6ade1998f6 fix 2023-12-27 21:08:43 +01:00
Viktor Rådberg
cc98a1b84a test 2023-12-27 21:05:27 +01:00
Viktor Rådberg
2ca6b91d09 test 2023-12-27 21:03:22 +01:00
Viktor Rådberg
00bda4fb68 test 2023-12-27 20:54:22 +01:00
Viktor Rådberg
d09d992535 test 2023-12-27 20:52:34 +01:00
Viktor Rådberg
e96e4f3aa9 test 2023-12-27 20:50:57 +01:00
Viktor Rådberg
cb132360a9 test 2023-12-27 20:48:29 +01:00
Viktor Rådberg
66b0892461 revert 2023-12-27 20:42:57 +01:00
Viktor Rådberg
fdab09d598 fix 2023-12-27 20:41:30 +01:00
Viktor Rådberg
ec030e7076 remove local storage when initial settings is not correct 2023-12-27 20:39:13 +01:00
Viktor Rådberg
9812c6737c add zod validation 2023-12-27 20:35:10 +01:00
Viktor Rådberg
e8528f46ae remove console logs 2023-12-27 19:57:16 +01:00
Viktor Rådberg
4ff7f67484 Merge pull request #27 from Vikeo/tailwind-wip
Tailwind wip
2023-12-27 19:56:38 +01:00
Viktor Rådberg
6e222995b6 finish 2023-12-27 19:53:11 +01:00
Viktor Rådberg
808c55109d wip done 2023-12-27 19:49:38 +01:00
Viktor Rådberg
183fd9c079 template 2023-12-27 17:50:05 +01:00
Viktor Rådberg
ea2114e048 wip 2023-12-24 15:15:32 +01:00
Viktor Rådberg
bc97e459cd Merge pull request #26 from Vikeo/develop
style fixes
2023-12-24 14:48:14 +01:00
Viktor Rådberg
47251b6f7b no right 2023-12-24 14:47:59 +01:00
Viktor Rådberg
77e8a79a35 add type 2023-12-24 14:44:13 +01:00
Viktor Rådberg
bdbea848d3 small style fix 2023-12-24 14:42:15 +01:00
Viktor Rådberg
3e2deca2f0 fix side recent difference rotation 2023-12-24 12:05:47 +01:00
Viktor Rådberg
866dca8e41 Merge pull request #25 from Vikeo/develop
Change start menu header from h2 to h1
2023-10-01 19:01:12 +02:00
Viktor Rådberg
5859bb5a49 Merge pull request #24 from Vikeo/develop
Release 0.4.0
2023-09-28 14:34:29 +02:00
Viktor Rådberg
20fb2153b3 Merge pull request #23 from Vikeo/develop
Just another small release
2023-09-26 17:31:25 +02:00
Viktor Rådberg
75038212c5 Merge pull request #22 from Vikeo/develop
Release 0.3.0
2023-09-22 14:39:31 +02:00
Viktor Rådberg
b712fb6e03 Merge pull request #21 from Vikeo/develop
add button to go back to start if render fails
2023-09-18 22:32:46 +02:00
Viktor Rådberg
3d27335fd0 Merge pull request #20 from Vikeo/develop
Add reset game button
2023-09-18 18:29:44 +02:00
Viktor Rådberg
18b53669d2 Merge pull request #19 from Vikeo/develop
New Release
2023-09-18 14:01:27 +02:00
Viktor Rådberg
28954eb948 Merge pull request #18 from Vikeo/develop
yarn instead of bun
2023-09-17 19:23:32 +02:00
Viktor Rådberg
3c59d5d05b Merge pull request #17 from Vikeo/develop
New release
2023-09-17 19:20:10 +02:00
Viktor Rådberg
22b58c74d6 Update start page text (#16)
* fix z index of settings and lose button

* update start page promt
2023-09-02 08:32:14 +02:00
41 changed files with 10355 additions and 1652 deletions

View File

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

View File

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

58
.github/workflows/firebase-release.yml vendored Normal file
View File

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

12
CHANGELOG.md Normal file
View File

@@ -0,0 +1,12 @@
# 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 Normal file
View File

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

View File

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

6
postcss.config.js Normal file
View File

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

View File

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

View File

@@ -1,110 +1,49 @@
import styled from 'styled-components';
import { css } from 'styled-components';
import { Player, Rotation } from '../../Types/Player';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { OutlinedText } from '../Misc/OutlinedText'; import { TwcComponentProps, twc } from 'react-twc';
import { decrementTimeoutMs } from '../../Data/constants'; import { decrementTimeoutMs } from '../../Data/constants';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText';
const CommanderDamageContainer = styled.div<{ export type RotationDivProps = TwcComponentProps<'div'> & {
$rotation: number; $rotation?: number;
}>` };
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
${(props) => { export type RotationSpanProps = TwcComponentProps<'span'> & {
if ( $rotation?: number;
props.$rotation === Rotation.SideFlipped || };
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
`;
}
}}
`;
const CommanderDamageButton = styled.button<{ export type RotationButtonProps = TwcComponentProps<'button'> & {
$backgroundColor?: string; $rotation?: number;
$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 CommanderDamageTextContainer = styled.div<{ const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
$rotation: number; 'flex flex-grow',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
position: relative; ? 'flex-col'
top: 50%; : 'flex-row',
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;
${(props) => { const CommanderDamageButton = twc.button<RotationButtonProps>((props) => [
if ( 'flex flex-grow border-none outline-none cursor-pointer m-0 p-0 webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'w-[6vmax] h-auto'
) { : 'h-[10vmin] w-1/2',
return css` ]);
rotate: 270deg;
`;
}
}}
`;
const PartnerDamageSeperator = styled.div<{ const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
$rotation: number; '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
width: 1px; ? 'rotate-[270deg]'
background-color: rgba(0, 0, 0, 1); : '',
]);
${(props) => { const PartnerDamageSeperator = twc.div<RotationDivProps>((props) => [
if ( 'bg-black',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'w-full h-px'
) { : 'w-px',
return css` ]);
width: auto;
height: 1px;
`;
}
}}
`;
type CommanderDamageButtonComponentProps = { type CommanderDamageButtonComponentProps = {
player: Player; player: Player;
@@ -214,8 +153,8 @@ export const CommanderDamage = ({
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
}} }}
$backgroundColor={opponent.color}
aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`} aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
> >
<CommanderDamageTextContainer $rotation={player.settings.rotation}> <CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText <OutlinedText
@@ -248,8 +187,8 @@ export const CommanderDamage = ({
) => { ) => {
e.preventDefault(); e.preventDefault();
}} }}
$backgroundColor={opponent.color}
aria-label={`Partner Commander damage. Player ${player.index}, opponent ${opponent.index}`} aria-label={`Partner Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
> >
<CommanderDamageTextContainer $rotation={player.settings.rotation}> <CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText <OutlinedText

View File

@@ -1,57 +1,45 @@
import { ReactNode, useRef, useState } from 'react'; import { ReactNode, useRef, useState } from 'react';
import styled from 'styled-components'; import { twc } from 'react-twc';
import { css } from 'styled-components';
import { decrementTimeoutMs } from '../../Data/constants'; import { decrementTimeoutMs } from '../../Data/constants';
import { CounterType, Rotation } from '../../Types/Player'; import { CounterType, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
import { RotationDivProps } from './CommanderDamage';
const ExtraCounterContainer = styled.div` const ExtraCounterContainer = twc.div`
display: flex; flex
justify-content: center; justify-center
align-items: center; items-center
pointer-events: all; pointer-events-all
flex-grow: 1; flex-grow
`; `;
export const StyledExtraCounterButton = styled.button` const ExtraCounterButton = twc.button`
position: relative; flex
flex-grow: 1; justify-center
border: none; items-center
outline: none; relative
cursor: pointer; flex-grow
background-color: transparent; border-none
user-select: none; outline-none
-webkit-touch-callout: none; cursor-pointer
-webkit-tap-highlight-color: transparent; bg-transparent
-moz-user-select: -moz-none; select-none
-webkit-user-select: none; h-full
-ms-user-select: none; webkit-user-select-none
height: 100%; `;
`;
const IconContainer = styled.div<{ const IconContainer = twc.div<RotationDivProps>((props) => [
$rotation: number; 'w-auto',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
width: auto; ? 'rotate-[-90deg]'
: '',
]);
${(props) => { const TextContainer = twc.div`
if ( absolute
props.$rotation === Rotation.SideFlipped || top-1/2
props.$rotation === Rotation.Side left-1/2
) { `;
return css`
rotate: -90deg;
`;
}
}}
`;
const TextContainer = styled.div`
position: absolute;
translate: -50%;
top: 50%;
left: 50%;
`;
type ExtraCounterProps = { type ExtraCounterProps = {
Icon: ReactNode; Icon: ReactNode;
@@ -115,7 +103,7 @@ const ExtraCounter = ({
return ( return (
<ExtraCounterContainer> <ExtraCounterContainer>
<StyledExtraCounterButton <ExtraCounterButton
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -136,7 +124,7 @@ const ExtraCounter = ({
</OutlinedText> </OutlinedText>
</TextContainer> </TextContainer>
</IconContainer> </IconContainer>
</StyledExtraCounterButton> </ExtraCounterButton>
</ExtraCounterContainer> </ExtraCounterContainer>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,59 +0,0 @@
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,8 +1,5 @@
import { CounterType, Player } from '../../Types/Player'; import { twc } from 'react-twc';
import ExtraCounter from '../Buttons/ExtraCounter'; import { usePlayers } from '../../Hooks/usePlayers';
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import { import {
CommanderTax, CommanderTax,
Energy, Energy,
@@ -10,50 +7,23 @@ import {
PartnerTax, PartnerTax,
Poison, Poison,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { usePlayers } from '../../Hooks/usePlayers'; import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter';
const Container = styled.div<{ $rotation: Rotation }>` const Container = twc.div<RotationDivProps>((props) => [
width: 100%; 'flex',
height: 20vmin; props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
display: flex; ? 'h-full w-[8vmax]'
: 'h-[20vmin] w-full',
]);
${(props) => { export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [
if ( 'flex absolute flex-row flex-grow pointer-events-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'flex-col-reverse h-full w-auto bottom-auto'
) { : 'w-full bottom-0',
return css` ]);
height: 100%;
width: 9.3vmax;
`;
}
}}
`;
export const ExtraCountersGrid = styled.div<{ $rotation: Rotation }>`
display: flex;
position: absolute;
width: 100%;
flex-direction: row;
flex-grow: 1;
bottom: 0;
pointer-events: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
height: 100%;
width: auto;
bottom: auto;
right: -6px;
`;
}
}}
`;
type ExtraCountersBarProps = { type ExtraCountersBarProps = {
player: Player; player: Player;

View File

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

View File

@@ -1,137 +1,36 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styled, { css, keyframes } from 'styled-components'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton'; import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton'; import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar'; import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar'; import ExtraCountersBar from '../Counters/ExtraCountersBar';
import PlayerMenu from '../PlayerMenu/PlayerMenu'; import PlayerMenu from '../Player/PlayerMenu';
import Health from './Health'; import Health from './Health';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
const LifeCounterContentWrapper = styled.div<{ const LifeCounterContentWrapper = twc.div`
$backgroundColor: string; relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
}>`
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;
}
overflow: hidden; 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`,
]);
const LifeCounterWrapper = styled.div<{ 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`;
$rotation: Rotation;
}>`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
z-index: 1; const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [
'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `rotate-[${props.$rotation - 90}deg]`
: '',
]);
${(props) => { const DynamicText = twc.div`text-[8vmin] whitespace-nowrap`;
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: row;
rotate: ${props.$rotation - 90}deg;
`;
}
return css`
flex-direction: column;
rotate: ${props.$rotation}deg;
`;
}}
`;
const PlayerNoticeWrapper = styled.div<{
$rotation: Rotation;
$backgroundColor: string;
}>`
z-index: 1;
display: flex;
position: absolute;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: ${(props) => props.$backgroundColor};
pointer-events: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 90}deg;
`;
}
}}
`;
const DynamicText = styled.div<{ $rotation: Rotation }>`
font-size: 8vmin;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
33% {
opacity: 0.6;
}
100% {
opacity: 0;
}
`;
export const RecentDifference = styled.span`
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
text-shadow: none;
background-color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
border-radius: 50%;
padding: 5px 10px;
font-size: 8vmin;
color: #333333;
animation: ${fadeOut} 3s 1s ease-out forwards;
`;
const hasCommanderDamageReached21 = (player: Player) => { const hasCommanderDamageReached21 = (player: Player) => {
const commanderDamageTotals = player.commanderDamage.map( const commanderDamageTotals = player.commanderDamage.map(
@@ -208,27 +107,42 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
updatePlayer(updatedPlayer); updatePlayer(updatedPlayer);
}; };
const calcRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 90
: player.settings.rotation;
const calcTextRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
return ( return (
<LifeCounterContentWrapper $backgroundColor={player.color}> <LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper $rotation={player.settings.rotation}> <LifeCounterWrapper
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
>
{settings.showStartingPlayer && {settings.showStartingPlayer &&
player.isStartingPlayer && player.isStartingPlayer &&
player.showStartingPlayer && ( player.showStartingPlayer && (
<PlayerNoticeWrapper <StartingPlayerNoticeWrapper
$rotation={player.settings.rotation} style={{ rotate: `${calcRotation}deg` }}
$backgroundColor={theme.palette.primary.main}
> >
<DynamicText $rotation={player.settings.rotation}> <DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
>
You start! You start!
</DynamicText> </DynamicText>
</PlayerNoticeWrapper> </StartingPlayerNoticeWrapper>
)} )}
{player.hasLost && ( {player.hasLost && (
<PlayerNoticeWrapper <PlayerLostWrapper $rotation={player.settings.rotation} />
$rotation={player.settings.rotation}
$backgroundColor={'#00000070'}
/>
)} )}
<CommanderDamageBar <CommanderDamageBar
opponents={opponents} opponents={opponents}
@@ -256,11 +170,10 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<ExtraCountersBar player={player} /> <ExtraCountersBar player={player} />
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)}
</LifeCounterWrapper> </LifeCounterWrapper>
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)}
</LifeCounterContentWrapper> </LifeCounterContentWrapper>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,38 +1,18 @@
import { Button, FormLabel, Modal, Switch } from '@mui/material'; import { Button, FormLabel, Modal, Switch } from '@mui/material';
import { ModalWrapper } from './InfoModal'; import { twc } from 'react-twc';
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { theme } from '../../Data/theme'; import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { useEffect, useState } from 'react';
const SettingContainer = styled.div` const SettingContainer = twc.div`w-full flex flex-col`;
width: 100%;
display: flex;
flex-direction: column;
`;
const ToggleContainer = styled.div` const ToggleContainer = twc.div`flex flex-row justify-between items-center`;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Container = styled.div` const Container = twc.div`flex flex-col items-center w-full`;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
`;
const Description = styled.p` const Description = twc.p`mr-16 text-xs text-left text-text-secondary`;
margin-top: -0.25rem;
margin-right: 3.5rem;
font-size: 0.8rem;
text-align: left;
color: ${theme.palette.text.secondary};
`;
type SettingsModalProps = { type SettingsModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -41,93 +21,175 @@ type SettingsModalProps = {
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => { export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA } = useGlobalSettings(); const { settings, setSettings, isPWA } = useGlobalSettings();
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
useEffect(() => {
if (!isOpen) {
return;
}
async function checkIfLatestVersion() {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
/* @ts-expect-error is defined in vite.config.ts*/
Authorization: `Bearer ${REPO_READ_ACCESS_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setNewVersion(undefined);
setIsLatestVersion(false);
return;
}
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) {
setIsLatestVersion(true);
return;
}
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
checkIfLatestVersion();
}, [isOpen]);
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal open={isOpen} onClose={closeModal}>
<ModalWrapper> <>
<Container> <div className="flex relative w-full max-w-[548px]">
<h2 style={{ textAlign: 'center' }}> Settings </h2> <button
<SettingContainer> onClick={closeModal}
<ToggleContainer> 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"
<FormLabel>Show Start Player</FormLabel> >
<Switch X
checked={settings.showStartingPlayer} </button>
onChange={() => { </div>
setSettings({ <ModalWrapper>
...settings, <Container>
showStartingPlayer: !settings.showStartingPlayer, <h2 className="text-center text-2xl mb-2"> Settings </h2>
}); <Separator height="1px" />
}} <SettingContainer>
/> <ToggleContainer>
</ToggleContainer> <FormLabel>Show Start Player</FormLabel>
<Description> <Switch
On start or reset of game, will pick a random player who will checked={settings.showStartingPlayer}
start first if this is enabled. onChange={() => {
</Description> setSettings({
</SettingContainer> ...settings,
<SettingContainer> showStartingPlayer: !settings.showStartingPlayer,
<ToggleContainer> });
<FormLabel>Keep Awake</FormLabel> }}
<Switch />
checked={settings.keepAwake} </ToggleContainer>
onChange={() => { <Description>
setSettings({ ...settings, keepAwake: !settings.keepAwake }); On start or reset of game, will pick a random player who will
}} start first if this is enabled.
/> </Description>
</ToggleContainer> </SettingContainer>
<Description> <SettingContainer>
Will prevent device from going to sleep while this app is open if <ToggleContainer>
this is enabled. <FormLabel>Keep Awake</FormLabel>
</Description> <Switch
</SettingContainer> checked={settings.keepAwake}
<SettingContainer> onChange={() => {
<ToggleContainer> setSettings({
<FormLabel>Go fullscreen on start (Android only)</FormLabel> ...settings,
<Switch keepAwake: !settings.keepAwake,
checked={settings.goFullscreenOnStart} });
onChange={() => { }}
setSettings({ />
...settings, </ToggleContainer>
goFullscreenOnStart: !settings.goFullscreenOnStart, <Description>
}); Will prevent device from going to sleep while this app is open
}} if this is enabled.
/> </Description>
</ToggleContainer> </SettingContainer>
<Description> <SettingContainer>
Will enter fullscreen mode when starting a game if this is <ToggleContainer>
enabled. <FormLabel>Go fullscreen on start (Android only)</FormLabel>
</Description> <Switch
</SettingContainer> checked={settings.goFullscreenOnStart}
{!isPWA && ( onChange={() => {
<> setSettings({
<Separator height="2px" /> ...settings,
<SettingContainer> goFullscreenOnStart: !settings.goFullscreenOnStart,
<ToggleContainer> });
<Paragraph> }}
<b>Tip:</b> You can{' '} />
<b>add this webapp to your home page on iOS</b> or{' '} </ToggleContainer>
<b>install it on Android</b> to have it act just like a <Description>
normal app! Will enter fullscreen mode when starting a game if this is
</Paragraph> enabled.
</ToggleContainer> </Description>
<Description> </SettingContainer>
If you do, this app will work offline and the toolbar will be {!isPWA && (
automatically hidden. <>
</Description> <Separator height="1px" />
</SettingContainer> <SettingContainer>
</> <ToggleContainer>
)} <Paragraph>
<Separator height="2px" /> <b>Tip:</b> You can{' '}
<SettingContainer> <b>add this webapp to your home page on iOS</b> or{' '}
<Paragraph>Version: 0.4.0</Paragraph> <b>install it on Android</b> to have it act just like a
</SettingContainer> normal app!
<Separator height="2px" /> </Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will
be automatically hidden.
</Description>
</SettingContainer>
</>
)}
<Separator height="1px" />
<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>
)}
</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" />
<Button variant="contained" onClick={closeModal}> <Button
Save and Close variant="contained"
</Button> onClick={closeModal}
</Container> style={{ marginTop: '0.25rem' }}
</ModalWrapper> >
Save and Close
</Button>
</Container>
</ModalWrapper>
</>
</Modal> </Modal>
); );
}; };

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
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,168 +1,98 @@
import { Button, Checkbox } from '@mui/material'; import { Button, Checkbox } from '@mui/material';
import styled, { css } from 'styled-components'; import { useRef } from 'react';
import { Player, Rotation } from '../../Types/Player'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme'; import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import { import {
PartnerTax, Cross,
Poison,
Energy, Energy,
Experience,
Exit, Exit,
Experience,
FullscreenOff, FullscreenOff,
FullscreenOn, FullscreenOn,
Cross, PartnerTax,
Poison,
ResetGame, ResetGame,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { useRef } from 'react'; import { Player, Rotation } from '../../Types/Player';
import { Spacer } from '../Misc/Spacer'; import {
import { useSafeRotate } from '../../Hooks/useSafeRotate'; RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
const SettingsContainer = styled.div<{ const CheckboxContainer = twc.div``;
$rotation: Rotation;
}>`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: 100%;
width: 100%;
${(props) => { const PlayerMenuWrapper = twc.div`
if ( flex
props.$rotation === Rotation.SideFlipped || flex-col
props.$rotation === Rotation.Side absolute
) { w-full
return css` h-full
flex-direction: column-reverse; bg-background-settings
height: 100%; items-center
width: 100%; justify-center
`; z-[2]
} webkit-user-select-none
}}
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`; `;
const BetterRowContainer = styled.div` const BetterRowContainer = twc.div`
display: flex; flex
flex-direction: column; flex-col
flex-grow: 1; flex-grow
width: 100%; w-full
height: 100%; h-full
justify-content: end; justify-end
align-items: stretch; items-stretch
`; `;
const TogglesSection = styled.div` const TogglesSection = twc.div`
display: flex; flex
position: relative; relative
flex-direction: row; flex-row
gap: 0.5rem; gap-2
justify-content: space-evenly; justify-evenly
`; `;
const ButtonsSections = styled.div` const ButtonsSections = twc.div`
display: flex; flex
max-width: 100%; max-w-full
gap: 1rem; gap-4
justify-content: space-between; justify-between
padding: 3% 3%; p-[3%]
align-items: center; items-center
`; `;
const ColorPicker = styled.input` const ColorPicker = twc.input`
position: absolute; absolute
top: 5%; top-[5%]
left: 5%; left-[5%]
height: 8vmax; h-[8vmax]
width: 8vmax; w-[8vmax]
border: none; border-none
outline: none; outline-none
cursor: pointer; cursor-pointer
background-color: transparent; bg-transparent
user-select: none; user-select-none
color: #ffffff; text-common-white
`; `;
const CheckboxContainer = styled.div<{ $rotation: Rotation }>` const SettingsContainer = twc.div<RotationDivProps>((props) => [
${(props) => { 'flex flex-wrap h-full w-full',
if ( props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.SideFlipped || ? 'flex-col'
props.$rotation === Rotation.Side : 'flex-row',
) { ]);
return css`
/* rotate: ${props.$rotation - 180}deg; */
`;
}
}}
`;
const PlayerMenuWrapper = styled.div<{ const CloseButton = twc.button<RotationButtonProps>((props) => [
$rotation: Rotation; 'absolute border-none outline-none cursor-pointer bg-transparent z-[99]',
}>` props.$rotation === Rotation.Side
display: flex; ? `top-[5%] right-auto left-[5%]`
flex-direction: column; : props.$rotation === Rotation.SideFlipped
position: absolute; ? 'top-auto left-auto bottom-[5%] right-[5%]'
width: 100%; : 'top-[15%] right-[5%]',
height: 100%; ]);
background-color: rgba(20, 20, 20, 0.9);
align-items: center;
justify-content: center;
z-index: 2;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return;
}
return css`
rotate: ${props.$rotation}deg;
`;
}};
`;
const CloseButton = styled.div<{
$rotation: Rotation;
}>`
position: absolute;
top: 15%;
right: 5%;
z-index: 9999;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
top: 5%;
right: auto;
left: 5%;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
top: auto;
left: auto;
bottom: 5%;
right: 5%;
`;
}
}}
`;
type PlayerMenuProps = { type PlayerMenuProps = {
player: Player; player: Player;
@@ -214,9 +144,31 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const extraCountersSize = isSide ? '8vmin' : '4vmax'; const extraCountersSize = isSide ? '8vmin' : '4vmax';
const closeButtonSize = isSide ? '6vmin' : '3vmax'; const closeButtonSize = isSide ? '6vmin' : '3vmax';
const calcRotation =
player.settings.rotation === Rotation.Side
? `${player.settings.rotation - 180}deg`
: player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '';
return ( return (
<PlayerMenuWrapper $rotation={player.settings.rotation}> <PlayerMenuWrapper
<CloseButton $rotation={player.settings.rotation}> //TODO: Fix hacky solution to rotation for SideFlipped
style={{
rotate:
player.settings.rotation === Rotation.SideFlipped ? '180deg' : '',
}}
>
<CloseButton
$rotation={player.settings.rotation}
style={{
rotate:
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '',
}}
>
<Button <Button
variant="text" variant="text"
onClick={handleOnClick} onClick={handleOnClick}
@@ -230,8 +182,12 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
<Cross size={closeButtonSize} /> <Cross size={closeButtonSize} />
</Button> </Button>
</CloseButton> </CloseButton>
<SettingsContainer <SettingsContainer
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{
rotate: calcRotation,
}}
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<ColorPicker <ColorPicker
@@ -244,7 +200,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
{player.settings.useCommanderDamage && ( {player.settings.useCommanderDamage && (
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="usePartner" name="usePartner"
checked={player.settings.usePartner} checked={player.settings.usePartner}
@@ -272,7 +228,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
</CheckboxContainer> </CheckboxContainer>
)} )}
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
checked={player.settings.usePoison} checked={player.settings.usePoison}
@@ -299,7 +255,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
checked={player.settings.useEnergy} checked={player.settings.useEnergy}
@@ -326,7 +282,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
checked={player.settings.useExperience} checked={player.settings.useExperience}
@@ -353,8 +309,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
</TogglesSection> </TogglesSection>
<Spacer height="1rem" /> <ButtonsSections className="mt-4">
<ButtonsSections>
<Button <Button
variant="text" variant="text"
style={{ style={{
@@ -366,7 +321,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
> >
<Exit size={iconSize} style={{ rotate: '180deg' }} /> <Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button> </Button>
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="fullscreen" name="fullscreen"
checked={document.fullscreenElement ? true : false} checked={document.fullscreenElement ? true : false}
@@ -418,33 +373,27 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={dialogRef} ref={dialogRef}
style={{ className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute top-[10%]"
zIndex: 9999,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRadius: '1rem',
border: 'none',
position: 'absolute',
top: '10%',
}}
> >
<h1>Reset Game?</h1> <div className="h-full flex flex-col p-4 gap-2">
<div style={{ display: 'flex', justifyContent: 'space-evenly' }}> <h1 className="text-center">Reset Game?</h1>
<Button <div className="flex justify-evenly gap-4">
variant="contained" <Button
onClick={() => dialogRef.current?.close()} variant="contained"
> onClick={() => dialogRef.current?.close()}
No >
</Button> No
<Button </Button>
variant="contained" <Button
onClick={() => { variant="contained"
handleResetGame(); onClick={() => {
dialogRef.current?.close(); handleResetGame();
}} dialogRef.current?.close();
> }}
Yes >
</Button> Yes
</Button>
</div>
</div> </div>
</dialog> </dialog>
</SettingsContainer> </SettingsContainer>

View File

@@ -1,24 +1,67 @@
import styled from 'styled-components'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import Counters from '../Counters/Counters'; import { usePlayers } from '../../Hooks/usePlayers';
import { Orientation } from '../../Types/Settings';
import { Player } from '../Player/Player';
import { twc } from 'react-twc';
const MainWrapper = styled.div` const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`;
width: 100vmax;
height: 100vmin;
width: 100dvmax;
height: 100dvmin;
overflow: hidden;
`;
type PlayProps = { export const Play = () => {
gridAreas: string; 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>;
}; };
const Play = ({ gridAreas }: PlayProps) => {
return (
<MainWrapper>
<Counters gridAreas={gridAreas} />
</MainWrapper>
);
};
export default Play;

View File

@@ -1,49 +1,48 @@
import React from 'react';
import styled from 'styled-components';
import { GridTemplateAreas } from '../../../Data/GridTemplateAreas';
import { FormControlLabel, Radio, RadioGroup } from '@mui/material'; import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import React from 'react';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { import {
OnePlayerPortrait, FivePlayers,
TwoPlayersOppositeLandscape,
TwoPlayersOppositePortrait,
ThreePlayers,
ThreePlayersSide,
FourPlayers, FourPlayers,
FourPlayersSide, FourPlayersSide,
FivePlayers, OnePlayerPortrait,
SixPlayers, SixPlayers,
ThreePlayers,
ThreePlayersSide,
TwoPlayersOppositeLandscape,
TwoPlayersOppositePortrait,
TwoPlayersSameSide, TwoPlayersSameSide,
} from '../../../Icons/generated/Layouts'; } from '../../../Icons/generated/Layouts';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
const LayoutWrapper = styled.div` import { twc } from 'react-twc';
flex-direction: row; import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
display: flex; import { Orientation } from '../../../Types/Settings';
justify-content: space-evenly;
`; const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
type LayoutOptionsProps = { type LayoutOptionsProps = {
numberOfPlayers: number; numberOfPlayers: number;
gridAreas: GridTemplateAreas; selectedOrientation: Orientation;
onChange: (gridAreas: GridTemplateAreas) => void; onChange: (orientation: Orientation) => void;
}; };
const LayoutOptions: React.FC<LayoutOptionsProps> = ({ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
numberOfPlayers, numberOfPlayers,
gridAreas, selectedOrientation,
onChange, onChange,
}) => { }) => {
const iconHeight = '30vmin'; const iconWidth = '21vmin';
const iconWidth = '20vmin'; const iconHeight = '40vmin';
const iconMaxWidth = '124px';
const iconMaxHeight = '196px';
const renderLayoutOptions = () => { const renderLayoutOptions = () => {
switch (numberOfPlayers) { switch (numberOfPlayers) {
case 1: case 1:
return ( return (
<> <div>
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.OnePlayerLandscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
icon={ icon={
@@ -61,12 +60,13 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.OnePlayerPortrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
icon={ icon={
@@ -84,19 +84,21 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
</> </div>
); );
case 2: case 2:
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.TwoPlayersSameSide} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersSameSide <TwoPlayersSameSide
height={iconHeight} height={iconHeight}
@@ -117,9 +119,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.TwoPlayersOppositePortrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositePortrait <TwoPlayersOppositePortrait
height={iconHeight} height={iconHeight}
@@ -140,9 +143,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.TwoPlayersOppositeLandscape} value={Orientation.OppositeLandscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositeLandscape <TwoPlayersOppositeLandscape
height={iconHeight} height={iconHeight}
@@ -168,9 +172,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.ThreePlayers} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayers <ThreePlayers
height={iconHeight} height={iconHeight}
@@ -191,9 +196,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.ThreePlayersSide} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayersSide <ThreePlayersSide
height={iconHeight} height={iconHeight}
@@ -220,9 +226,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.FourPlayers} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayers <FourPlayers
height={iconHeight} height={iconHeight}
@@ -243,9 +250,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
label="" label=""
/> />
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.FourPlayersSide} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayersSide <FourPlayersSide
height={iconHeight} height={iconHeight}
@@ -272,9 +280,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.FivePlayers} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayers <FivePlayers
height={iconHeight} height={iconHeight}
@@ -324,9 +333,10 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
return ( return (
<> <>
<FormControlLabel <FormControlLabel
value={GridTemplateAreas.SixPlayers} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayers <SixPlayers
height={iconHeight} height={iconHeight}
@@ -382,9 +392,9 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
<RadioGroup <RadioGroup
row row
onChange={(_e, value) => { onChange={(_e, value) => {
onChange(value as GridTemplateAreas); onChange(value as Orientation);
}} }}
value={gridAreas} value={selectedOrientation}
style={{ justifyContent: 'center' }} style={{ justifyContent: 'center' }}
> >
{renderLayoutOptions()} {renderLayoutOptions()}
@@ -392,5 +402,3 @@ const LayoutOptions: React.FC<LayoutOptionsProps> = ({
</LayoutWrapper> </LayoutWrapper>
); );
}; };
export default LayoutOptions;

View File

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

View File

@@ -1,15 +0,0 @@
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,6 +1,5 @@
import { Player, Rotation } from '../Types/Player'; import { Player, Rotation } from '../Types/Player';
import { InitialGameSettings } from '../Types/Settings'; import { InitialGameSettings, Orientation } from '../Types/Settings';
import { GridTemplateAreas } from './GridTemplateAreas';
const presetColors = [ const presetColors = [
'#F06292', // Light Pink '#F06292', // Light Pink
@@ -13,184 +12,182 @@ const presetColors = [
'#FF8A80', // Coral '#FF8A80', // Coral
]; ];
const getRotation = (index: number, gridAreas: GridTemplateAreas): Rotation => { const getOrientationRotations = (
if (gridAreas === GridTemplateAreas.OnePlayerLandscape && index === 0) { index: number,
return Rotation.Normal; numberOfPlayers: number,
orientation: Orientation
): Rotation => {
switch (numberOfPlayers) {
case 1:
switch (orientation) {
default:
case Orientation.Landscape:
return Rotation.Normal;
case Orientation.Portrait:
return Rotation.Side;
}
case 2:
switch (orientation) {
default:
case Orientation.Landscape:
return Rotation.Normal;
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.SideFlipped;
case 1:
return Rotation.Side;
default:
return Rotation.Normal;
}
case Orientation.OppositeLandscape:
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
case 3:
switch (orientation) {
default:
case Orientation.Landscape:
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Normal;
case 2:
return Rotation.Normal;
default:
return Rotation.Normal;
}
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Normal;
case 2:
return Rotation.Side;
default:
return Rotation.Normal;
}
}
case 4:
switch (orientation) {
default:
case Orientation.Landscape:
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Normal;
case 3:
return Rotation.Normal;
default:
return Rotation.Normal;
}
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.SideFlipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Normal;
case 3:
return Rotation.Side;
default:
return Rotation.Normal;
}
}
case 5:
switch (orientation) {
default:
case Orientation.Landscape:
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Normal;
case 3:
return Rotation.Normal;
case 4:
return Rotation.Normal;
default:
return Rotation.Normal;
}
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.Side;
case 1:
return Rotation.Side;
case 2:
return Rotation.SideFlipped;
case 3:
return Rotation.SideFlipped;
case 4:
return Rotation.SideFlipped;
default:
return Rotation.Normal;
}
}
case 6:
switch (orientation) {
default:
case Orientation.Landscape:
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Flipped;
case 3:
return Rotation.Normal;
case 4:
return Rotation.Normal;
case 5:
return Rotation.Normal;
default:
return Rotation.Normal;
}
case Orientation.Portrait:
switch (index) {
case 0:
return Rotation.Side;
case 1:
return Rotation.Side;
case 2:
return Rotation.Side;
case 3:
return Rotation.SideFlipped;
case 4:
return Rotation.SideFlipped;
case 5:
return Rotation.SideFlipped;
default:
return Rotation.Normal;
}
}
default:
return Rotation.Normal;
} }
if (gridAreas === GridTemplateAreas.OnePlayerPortrait && index === 0) {
return Rotation.Side;
}
if (gridAreas === GridTemplateAreas.TwoPlayersOppositePortrait) {
switch (index) {
case 0:
return Rotation.SideFlipped;
case 1:
return Rotation.Side;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.TwoPlayersOppositeLandscape) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.TwoPlayersSameSide) {
switch (index) {
case 0:
return Rotation.Normal;
case 1:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.ThreePlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Normal;
case 2:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.ThreePlayersSide) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Normal;
case 2:
return Rotation.Side;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.FourPlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Normal;
case 3:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.FourPlayersSide) {
switch (index) {
case 0:
return Rotation.SideFlipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Normal;
case 3:
return Rotation.Side;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.FivePlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Normal;
case 3:
return Rotation.Normal;
case 4:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.FivePlayersSide) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Side;
case 3:
return Rotation.Normal;
case 4:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.SixPlayers) {
switch (index) {
case 0:
return Rotation.Flipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Flipped;
case 3:
return Rotation.Normal;
case 4:
return Rotation.Normal;
case 5:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
if (gridAreas === GridTemplateAreas.SixPlayersSide) {
switch (index) {
case 0:
return Rotation.SideFlipped;
case 1:
return Rotation.Flipped;
case 2:
return Rotation.Flipped;
case 3:
return Rotation.Side;
case 4:
return Rotation.Normal;
case 5:
return Rotation.Normal;
default:
return Rotation.Normal;
}
}
return Rotation.Normal;
}; };
export const createInitialPlayers = ({ export const createInitialPlayers = ({
numberOfPlayers, numberOfPlayers,
startingLifeTotal, startingLifeTotal,
useCommanderDamage, useCommanderDamage,
gridAreas, orientation,
}: InitialGameSettings): Player[] => { }: InitialGameSettings): Player[] => {
const players: Player[] = []; const players: Player[] = [];
const availableColors = [...presetColors]; // Create a copy of the colors array const availableColors = [...presetColors]; // Create a copy of the colors array
@@ -213,7 +210,7 @@ export const createInitialPlayers = ({
}); });
} }
const rotation = getRotation(i, gridAreas); const rotation = getOrientationRotations(i, numberOfPlayers, orientation);
const player: Player = { const player: Player = {
lifeTotal: startingLifeTotal, lifeTotal: startingLifeTotal,

View File

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

View File

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

View File

@@ -1,4 +1,16 @@
import { GridTemplateAreas } from '../Data/GridTemplateAreas'; 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',
}
export type Settings = { export type Settings = {
keepAwake: boolean; keepAwake: boolean;
@@ -9,9 +21,15 @@ export type Settings = {
export type InitialGameSettings = { export type InitialGameSettings = {
startingLifeTotal: number; startingLifeTotal: number;
useCommanderDamage: boolean; useCommanderDamage: boolean;
gameFormat: GameFormat; gameFormat?: GameFormat;
numberOfPlayers: number; numberOfPlayers: number;
gridAreas: GridTemplateAreas; orientation: Orientation;
}; };
type GameFormat = 'commander' | 'standard' | 'two-headed-giant'; export const InitialGameSettingsSchema = z.object({
startingLifeTotal: z.number().min(1).max(200).default(20),
useCommanderDamage: z.boolean().default(false),
gameFormat: z.nativeEnum(GameFormat).optional(),
numberOfPlayers: z.number().min(1).max(6).default(2),
orientation: z.nativeEnum(Orientation).default(Orientation.Landscape),
});

View File

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

102
tailwind.config.ts Normal file
View File

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

8862
yarn.lock Normal file

File diff suppressed because it is too large Load Diff