Compare commits

...

113 Commits

Author SHA1 Message Date
Viktor Rådberg
ef1310d674 bump 2024-03-16 13:22:47 +01:00
Viktor Rådberg
fe3bb6c78c show starting player untill press 2024-03-16 13:22:03 +01:00
Viktor Rådberg
6d2b3b6a6f Add option to show player menu cog 2024-03-16 12:29:16 +01:00
Viktor Rådberg
0f86928cb3 Merge pull request #32 from Vikeo/better-colors
Better colors
2024-03-16 10:42:13 +01:00
Viktor Rådberg
efbfb7719c tsc 2024-03-16 10:40:18 +01:00
Viktor Rådberg
71e5614f52 bump to new version 2024-03-16 10:38:23 +01:00
Viktor Rådberg
677fd79bee fix long press down 2024-03-16 10:23:15 +01:00
Viktor Rådberg
1bff41bc10 remove colorful 2024-03-16 10:04:35 +01:00
Viktor Rådberg
7852520f8e minus plus icon color 2024-03-16 09:59:40 +01:00
Viktor Rådberg
04c3d60967 use normal picker again 2024-03-16 09:31:59 +01:00
Viktor Rådberg
664e2e5688 round color picker 2024-02-19 07:38:17 +01:00
Viktor Rådberg
6eb7ac9f50 Merge branch 'main' into better-colors 2024-02-18 16:08:09 +01:00
Viktor Rådberg
ef06e0d125 bump 2024-02-09 23:04:29 +01:00
Viktor Rådberg
ae9f5707b2 update blur 2024-02-09 23:04:14 +01:00
Viktor Rådberg
a18c253624 bump 2024-01-31 23:12:46 +01:00
Viktor Rådberg
3f319c4f3c add some blur to settings 2024-01-31 23:12:31 +01:00
Viktor Rådberg
8b33a2a38a wip 2024-01-28 17:04:30 +01:00
Viktor Rådberg
cc915dff36 better color picker 2024-01-28 11:54:37 +01:00
Viktor Rådberg
db80e563f2 bump 2024-01-27 18:05:54 +01:00
Viktor Rådberg
573af42b75 fix taps and some settings stuff 2024-01-27 18:05:18 +01:00
Viktor Rådberg
89e1eaff4e bump 2024-01-27 16:25:40 +01:00
Viktor Rådberg
0f4e896342 Merge pull request #31 from Vikeo/swipable-settings
Swipable settings
2024-01-27 16:23:54 +01:00
Viktor Rådberg
dc1d5fe01d tsc 2024-01-27 16:20:09 +01:00
Viktor Rådberg
41e73d2c0c swipe 2024-01-27 11:05:54 +01:00
Viktor Rådberg
724dcf086c is side 2024-01-27 09:32:00 +01:00
Viktor Rådberg
51f9c4d20e initial test 2024-01-26 21:24:40 +01:00
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
bc97e459cd Merge pull request #26 from Vikeo/develop
style fixes
2023-12-24 14:48:14 +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
42 changed files with 3683 additions and 1810 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.7",
"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,7 +22,9 @@
"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-swipeable": "^7.0.1",
"react-twc": "^1.3.0",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
@@ -42,8 +44,9 @@
"install": "^0.13.0", "install": "^0.13.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "2.8.8", "prettier": "2.8.8",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.1",
"typescript": "^5.0.2", "typescript": "^5.3.3",
"vite": "^4.4.5" "vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.4"
} }
} }

View File

@@ -1,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,51 @@
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<{ export const MAX_TAP_MOVE_DISTANCE = 20;
$rotation: number;
}>`
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-variant-numeric: tabular-nums;
pointer-events: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => { const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
if ( 'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'flex-col'
) { : 'flex-row',
return css` ]);
rotate: 270deg;
`;
}
}}
`;
const PartnerDamageSeperator = styled.div<{ const CommanderDamageButton = twc.button<RotationButtonProps>((props) => [
$rotation: number; 'flex flex-grow border-none outline-none cursor-pointer m-0 p-0 webkit-user-select-none',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
width: 1px; ? 'w-[6vmax] h-auto'
background-color: rgba(0, 0, 0, 1); : 'h-[10vmin] w-1/2',
]);
${(props) => { const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
if ( '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.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'rotate-[270deg]'
) { : '',
return css` ]);
width: auto;
height: 1px; const PartnerDamageSeparator = twc.div<RotationDivProps>((props) => [
`; 'bg-black',
} props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
}} ? 'w-full h-px'
`; : 'w-px',
]);
type CommanderDamageButtonComponentProps = { type CommanderDamageButtonComponentProps = {
player: Player; player: Player;
@@ -115,6 +56,7 @@ type CommanderDamageButtonComponentProps = {
type InputProps = { type InputProps = {
opponentIndex: number; opponentIndex: number;
isPartner: boolean; isPartner: boolean;
event: React.PointerEvent<HTMLButtonElement>;
}; };
export const CommanderDamage = ({ export const CommanderDamage = ({
@@ -124,12 +66,8 @@ export const CommanderDamage = ({
}: CommanderDamageButtonComponentProps) => { }: CommanderDamageButtonComponentProps) => {
const { updatePlayer } = usePlayers(); const { updatePlayer } = usePlayers();
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false); const [downLongPressed, setDownLongPressed] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); const downPositionRef = useRef({ x: 0, y: 0 });
const isSide =
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped;
const handleCommanderDamageChange = ( const handleCommanderDamageChange = (
index: number, index: number,
@@ -168,34 +106,47 @@ export const CommanderDamage = ({
handleLifeChange(player.lifeTotal - increment); handleLifeChange(player.lifeTotal - increment);
}; };
const handleDownInput = ({ opponentIndex, isPartner }: InputProps) => { const handleDownInput = ({ opponentIndex, isPartner, event }: InputProps) => {
setTimeoutFinished(false); downPositionRef.current = { x: event.clientX, y: event.clientY };
setHasPressedDown(true); setDownLongPressed(false);
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true); setDownLongPressed(true);
handleCommanderDamageChange(opponentIndex, -1, isPartner); handleCommanderDamageChange(opponentIndex, -1, isPartner);
}, decrementTimeoutMs); }, decrementTimeoutMs);
}; };
const handleUpInput = ({ opponentIndex, isPartner }: InputProps) => { const handleUpInput = ({ opponentIndex, isPartner, event }: InputProps) => {
if (!(hasPressedDown && !timeoutFinished)) { if (downLongPressed) {
return; return;
} }
const upPosition = { x: event.clientX, y: event.clientY };
const hasMoved =
Math.abs(upPosition.x - downPositionRef.current.x) >
MAX_TAP_MOVE_DISTANCE ||
Math.abs(upPosition.y - downPositionRef.current.y) >
MAX_TAP_MOVE_DISTANCE;
if (hasMoved) {
return;
}
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
handleCommanderDamageChange(opponentIndex, 1, isPartner); handleCommanderDamageChange(opponentIndex, 1, isPartner);
setHasPressedDown(false);
}; };
const handleLeaveInput = () => { const handleLeaveInput = () => {
setTimeoutFinished(true); setDownLongPressed(true);
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
setHasPressedDown(false);
}; };
const opponentIndex = opponent.index; const opponentIndex = opponent.index;
const fontSize = isSide ? '4vmax' : '7vmin'; const fontSize = player.isSide ? '4vmax' : '7vmin';
const fontWeight = 'bold'; const fontWeight = 'bold';
const strokeWidth = isSide ? '0.4vmax' : '0.7vmin'; const strokeWidth = player.isSide ? '0.4vmax' : '0.7vmin';
return ( return (
<CommanderDamageContainer <CommanderDamageContainer
@@ -206,16 +157,18 @@ export const CommanderDamage = ({
<CommanderDamageButton <CommanderDamageButton
key={opponentIndex} key={opponentIndex}
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
onPointerDown={() => onPointerDown={(e) =>
handleDownInput({ opponentIndex, isPartner: false }) handleDownInput({ opponentIndex, isPartner: false, event: e })
}
onPointerUp={(e) =>
handleUpInput({ opponentIndex, isPartner: false, event: e })
} }
onPointerUp={() => handleUpInput({ opponentIndex, isPartner: false })}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
}} }}
$backgroundColor={opponent.color}
aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`} aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
> >
<CommanderDamageTextContainer $rotation={player.settings.rotation}> <CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText <OutlinedText
@@ -232,15 +185,15 @@ export const CommanderDamage = ({
{opponent.settings.usePartner && ( {opponent.settings.usePartner && (
<> <>
<PartnerDamageSeperator $rotation={player.settings.rotation} /> <PartnerDamageSeparator $rotation={player.settings.rotation} />
<CommanderDamageButton <CommanderDamageButton
key={opponentIndex} key={opponentIndex}
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
onPointerDown={() => onPointerDown={(e) =>
handleDownInput({ opponentIndex, isPartner: true }) handleDownInput({ opponentIndex, isPartner: true, event: e })
} }
onPointerUp={() => onPointerUp={(e) =>
handleUpInput({ opponentIndex, isPartner: true }) handleUpInput({ opponentIndex, isPartner: true, event: e })
} }
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
onContextMenu={( onContextMenu={(
@@ -248,8 +201,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,59 +1,44 @@
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 { MAX_TAP_MOVE_DISTANCE, 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`
display: flex; flex
justify-content: center; justify-center
align-items: center; items-center
position: relative; relative
flex-grow: 1; flex-grow
border: none; border-none
outline: none; outline-none
cursor: pointer; cursor-pointer
background-color: transparent; bg-transparent
user-select: none; select-none
-webkit-touch-callout: none; h-full
-webkit-tap-highlight-color: transparent; webkit-user-select-none
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-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 = {
@@ -62,6 +47,7 @@ type ExtraCounterProps = {
type: CounterType; type: CounterType;
setCounterTotal: (updatedCounterTotal: number, type: CounterType) => void; setCounterTotal: (updatedCounterTotal: number, type: CounterType) => void;
rotation: number; rotation: number;
isSide: boolean;
playerIndex: number; playerIndex: number;
}; };
@@ -71,14 +57,13 @@ const ExtraCounter = ({
setCounterTotal, setCounterTotal,
type, type,
rotation, rotation,
isSide,
playerIndex, playerIndex,
}: ExtraCounterProps) => { }: ExtraCounterProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false); const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false); const [hasPressedDown, setHasPressedDown] = useState(false);
const downPositionRef = useRef({ x: 0, y: 0 });
const isSide =
rotation === Rotation.Side || rotation === Rotation.SideFlipped;
const handleCountChange = (increment: number) => { const handleCountChange = (increment: number) => {
if (!counterTotal) { if (!counterTotal) {
@@ -88,7 +73,8 @@ const ExtraCounter = ({
setCounterTotal(counterTotal + increment, type); setCounterTotal(counterTotal + increment, type);
}; };
const handleDownInput = () => { const handleDownInput = (event: React.PointerEvent<HTMLButtonElement>) => {
downPositionRef.current = { x: event.clientX, y: event.clientY };
setTimeoutFinished(false); setTimeoutFinished(false);
setHasPressedDown(true); setHasPressedDown(true);
timeoutRef.current = setTimeout(() => { timeoutRef.current = setTimeout(() => {
@@ -97,10 +83,23 @@ const ExtraCounter = ({
}, decrementTimeoutMs); }, decrementTimeoutMs);
}; };
const handleUpInput = () => { const handleUpInput = (event: React.PointerEvent<HTMLButtonElement>) => {
if (!(hasPressedDown && !timeoutFinished)) { if (!(hasPressedDown && !timeoutFinished)) {
return; return;
} }
const upPosition = { x: event.clientX, y: event.clientY };
const hasMoved =
Math.abs(upPosition.x - downPositionRef.current.x) >
MAX_TAP_MOVE_DISTANCE ||
Math.abs(upPosition.y - downPositionRef.current.y) >
MAX_TAP_MOVE_DISTANCE;
if (hasMoved) {
return;
}
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
handleCountChange(1); handleCountChange(1);
setHasPressedDown(false); setHasPressedDown(false);
@@ -118,7 +117,7 @@ const ExtraCounter = ({
return ( return (
<ExtraCounterContainer> <ExtraCounterContainer>
<StyledExtraCounterButton <ExtraCounterButton
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -139,7 +138,7 @@ const ExtraCounter = ({
</OutlinedText> </OutlinedText>
</TextContainer> </TextContainer>
</IconContainer> </IconContainer>
</StyledExtraCounterButton> </ExtraCounterButton>
</ExtraCounterContainer> </ExtraCounterContainer>
); );
}; };

View File

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

View File

@@ -1,43 +1,15 @@
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 py-2 px-4 ',
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 || props.$rotation === Rotation.Side
if (props.$rotation === Rotation.SideFlipped) { ? `left-[21%]`
return css` : 'top-[21%]',
right: auto; ]);
top: 15%;
left: 27%;
rotate: ${props.$rotation}deg;
`;
} else if (props.$rotation === Rotation.Side) {
return css`
right: auto;
top: 15%;
left: 27%;
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
type LoseButtonProps = { type LoseButtonProps = {
onClick: () => void; onClick: () => void;
@@ -45,9 +17,23 @@ type LoseButtonProps = {
}; };
export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => { export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
const calcRotation =
rotation === Rotation.SideFlipped
? rotation
: rotation === Rotation.Side
? rotation - 180
: rotation === Rotation.Flipped
? rotation - 180
: rotation;
return ( return (
<LoseButton $rotation={rotation} onClick={onClick} aria-label={`Lose Game`}> <LoseButton
<Skull size="5vmin" color="black" opacity={0.5} /> $rotation={rotation}
onClick={onClick}
aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }}
>
<Skull size="8vmin" color="black" opacity={0.5} />
</LoseButton> </LoseButton>
); );
}; };

View File

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

View File

@@ -1,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,49 +7,25 @@ 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';
import { useEffect, useState } from 'react';
import { checkContrast } from '../../Utils/checkContrast';
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;
`;
}
}}
`;
type ExtraCountersBarProps = { type ExtraCountersBarProps = {
player: Player; player: Player;
@@ -60,6 +33,17 @@ type ExtraCountersBarProps = {
const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => { const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
const { updatePlayer } = usePlayers(); const { updatePlayer } = usePlayers();
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(player.color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [player.color]);
const handleCounterChange = ( const handleCounterChange = (
updatedCounterTotal: number, updatedCounterTotal: number,
@@ -122,13 +106,20 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useCommanderDamage && ( {useCommanderDamage && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={<CommanderTax size={iconSize} opacity="0.5" color="black" />} Icon={
<CommanderTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.CommanderTax} type={CounterType.CommanderTax}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'commanderTax' (counter) => counter.type === 'commanderTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -136,13 +127,20 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{Boolean(useCommanderDamage && usePartner) && ( {Boolean(useCommanderDamage && usePartner) && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={<PartnerTax size={iconSize} opacity="0.5" color="black" />} Icon={
<PartnerTax
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.PartnerTax} type={CounterType.PartnerTax}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'partnerTax' (counter) => counter.type === 'partnerTax'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -150,12 +148,19 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{usePoison && ( {usePoison && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={<Poison size={iconSize} opacity="0.5" color="black" />} Icon={
<Poison
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Poison} type={CounterType.Poison}
counterTotal={ counterTotal={
player.extraCounters?.find((counter) => counter.type === 'poison') player.extraCounters?.find((counter) => counter.type === 'poison')
?.value ?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -163,12 +168,19 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useEnergy && ( {useEnergy && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={<Energy size={iconSize} opacity="0.5" color="black" />} Icon={
<Energy
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Energy} type={CounterType.Energy}
counterTotal={ counterTotal={
player.extraCounters?.find((counter) => counter.type === 'energy') player.extraCounters?.find((counter) => counter.type === 'energy')
?.value ?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />
@@ -176,13 +188,20 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
{useExperience && ( {useExperience && (
<ExtraCounter <ExtraCounter
rotation={player.settings.rotation} rotation={player.settings.rotation}
Icon={<Experience size={iconSize} opacity="0.5" color="black" />} Icon={
<Experience
size={iconSize}
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
}
type={CounterType.Experience} type={CounterType.Experience}
counterTotal={ counterTotal={
player.extraCounters?.find( player.extraCounters?.find(
(counter) => counter.type === 'experience' (counter) => counter.type === 'experience'
)?.value )?.value
} }
isSide={player.isSide}
setCounterTotal={handleCounterChange} setCounterTotal={handleCounterChange}
playerIndex={player.index} playerIndex={player.index}
/> />

View File

@@ -1,115 +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 LifeContainer = 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<{ $rotation: Rotation }>`
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;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
top: 27%;
left: 30%;
transform: translate(-50%, -50%);
rotate: 270deg;
`;
}
}}
`;
type HealthProps = { type HealthProps = {
player: Player; player: Player;
@@ -121,28 +49,13 @@ type HealthProps = {
const Health = ({ const Health = ({
player, player,
rotation,
handleLifeChange, handleLifeChange,
differenceKey, differenceKey,
recentDifference, recentDifference,
}: HealthProps) => { }: HealthProps) => {
const [showStartingPlayer, setShowStartingPlayer] = useState(
localStorage.getItem('playing') === 'true'
);
const [fontSize, setFontSize] = useState(16); const [fontSize, setFontSize] = useState(16);
const textContainerRef = useRef<HTMLDivElement | null>(null); const textContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!showStartingPlayer) {
const playingTimer = setTimeout(() => {
localStorage.setItem('playing', 'true');
setShowStartingPlayer(localStorage.getItem('playing') === 'true');
}, 3_000);
return () => clearTimeout(playingTimer);
}
}, [showStartingPlayer]);
useEffect(() => { useEffect(() => {
if (!textContainerRef.current) { if (!textContainerRef.current) {
return; return;
@@ -151,7 +64,6 @@ const Health = ({
const textContainer = textContainerRef.current; const textContainer = textContainerRef.current;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
const calcFontSize = calculateFontSize(textContainer); const calcFontSize = calculateFontSize(textContainer);
console.log(calcFontSize);
setFontSize(calcFontSize); setFontSize(calcFontSize);
}); });
@@ -172,12 +84,13 @@ const Health = ({
}, [textContainerRef]); }, [textContainerRef]);
const calculateFontSize = (container: HTMLDivElement) => { const calculateFontSize = (container: HTMLDivElement) => {
const isSide = const widthRatio = player.isSide
rotation === Rotation.SideFlipped || rotation === Rotation.Side; ? container.clientHeight
: container.clientWidth;
const widthRatio = isSide ? container.clientHeight : container.clientWidth; const heightRatio = player.isSide
? container.clientWidth
const heightRatio = isSide ? container.clientWidth : container.clientHeight; : container.clientHeight;
const minRatio = Math.min(widthRatio, heightRatio); const minRatio = Math.min(widthRatio, heightRatio);
@@ -189,11 +102,10 @@ const Health = ({
}; };
return ( return (
<LifeCountainer $rotation={player.settings.rotation}> <LifeContainer $rotation={player.settings.rotation}>
<LifeCounterButton <LifeCounterButton
lifeTotal={player.lifeTotal} player={player}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="subtract" operation="subtract"
increment={-1} increment={-1}
/> />
@@ -221,13 +133,12 @@ const Health = ({
</LifeCounterTextContainer> </LifeCounterTextContainer>
</TextWrapper> </TextWrapper>
<LifeCounterButton <LifeCounterButton
lifeTotal={player.lifeTotal} player={player}
setLifeTotal={handleLifeChange} setLifeTotal={handleLifeChange}
rotation={player.settings.rotation}
operation="add" operation="add"
increment={1} increment={1}
/> />
</LifeCountainer> </LifeContainer>
); );
}; };

View File

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

View File

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

View File

@@ -1,57 +1,29 @@
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; const fullConfig = resolveConfig(tailwindConfig);
justify-content: center;
const Container = twc.div`
flex
relative
w-full
h-full
items-center
justify-center
`; `;
const CenteredText = styled.div<{ const CenteredText = twc.div`absolute select-none text-common-black text-[6vmin] stroke-common-white
strokeWidth?: string; webkit-user-select-none tabular-nums`;
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 CenteredTextOutline = twc.span`
font-size: ${(props) => props.fontSize || '6vmin'}; absolute
-webkit-text-stroke: ${(props) => props.strokeWidth || '1vmin'}${(props) => props.strokeColor || theme.palette.common.white}; left-0
-webkit-text-fill-color: ${(props) => stroke-none
props.fillColor || theme.palette.common.black}; pointer-events-none
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: 270deg;
`;
}
}}
`;
const CenteredTextOutline = styled.span`
position: absolute;
left: 0;
-webkit-text-stroke: 0;
pointer-events: none;
`; `;
type OutlinedTextProps = { type OutlinedTextProps = {
@@ -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,19 @@
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';
import { Cross } from '../../Icons/generated';
const SettingContainer = styled.div` const SettingContainer = twc.div`w-full flex flex-col mb-2`;
width: 100%;
display: flex;
flex-direction: column;
`;
const ToggleContainer = styled.div` const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`;
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,12 +22,69 @@ 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}
className="w-full flex justify-center"
>
<>
<div className="flex justify-center items-center relative w-full max-w-[532px]">
<button
onClick={closeModal}
className="flex absolute top-12 right-0 z-10 w-10 h-10 bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
>
<Cross size="16px" className="text-text-primary " />
</button>
</div>
<ModalWrapper> <ModalWrapper>
<Container> <Container>
<h2 style={{ textAlign: 'center' }}> Settings </h2> <h2 className="text-center text-2xl mb-2"> Settings </h2>
<Separator height="1px" />
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Show Start Player</FormLabel> <FormLabel>Show Start Player</FormLabel>
@@ -67,19 +105,40 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer> </SettingContainer>
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Keep Awake</FormLabel> <FormLabel>Show Player Menu Cog</FormLabel>
<Switch <Switch
checked={settings.keepAwake} checked={settings.showPlayerMenuCog}
onChange={() => { onChange={() => {
setSettings({ ...settings, keepAwake: !settings.keepAwake }); setSettings({
...settings,
showPlayerMenuCog: !settings.showPlayerMenuCog,
});
}} }}
/> />
</ToggleContainer> </ToggleContainer>
<Description> <Description>
Will prevent device from going to sleep while this app is open if A cog on the top right of each player's card will be shown if
this is enabled. this is enabled.
</Description> </Description>
</SettingContainer> </SettingContainer>
<SettingContainer>
<ToggleContainer>
<FormLabel>Keep Awake</FormLabel>
<Switch
checked={settings.keepAwake}
onChange={() => {
setSettings({
...settings,
keepAwake: !settings.keepAwake,
});
}}
/>
</ToggleContainer>
<Description>
Will prevent device from going to sleep while this app is open
if this is enabled.
</Description>
</SettingContainer>
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Go fullscreen on start (Android only)</FormLabel> <FormLabel>Go fullscreen on start (Android only)</FormLabel>
@@ -100,7 +159,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer> </SettingContainer>
{!isPWA && ( {!isPWA && (
<> <>
<Separator height="2px" /> <Separator height="1px" />
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<Paragraph> <Paragraph>
@@ -110,24 +169,50 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
normal app! normal app!
</Paragraph> </Paragraph>
</ToggleContainer> </ToggleContainer>
<Description> <Description className="mt-1">
If you do, this app will work offline and the toolbar will be If you do, this app will work offline and the toolbar will
automatically hidden. be automatically hidden.
</Description> </Description>
</SettingContainer> </SettingContainer>
</> </>
)} )}
<Separator height="2px" /> <Separator height="1px" />
<SettingContainer> <SettingContainer>
<Paragraph>Version: 0.4.0</Paragraph> <Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
)}
</SettingContainer> </SettingContainer>
<Separator height="2px" /> {!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
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close Save and Close
</Button> </Button>
</Container> </Container>
</ModalWrapper> </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,42 +1,29 @@
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 = () => {
@@ -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

@@ -1,5 +1,6 @@
import LifeCounter from '../LifeCounter/LifeCounter'; import LifeCounter from '../LifeCounter/LifeCounter';
import { Player as PlayerType } from '../../Types/Player'; import { Player as PlayerType } from '../../Types/Player';
import { twc } from 'react-twc';
const getGridArea = (player: PlayerType) => { const getGridArea = (player: PlayerType) => {
switch (player.index) { switch (player.index) {
@@ -20,9 +21,11 @@ const getGridArea = (player: PlayerType) => {
} }
}; };
const PlayerWrapper = twc.div`w-full h-full bg-black`;
export const Player = (players: PlayerType[], gridClasses: string) => { export const Player = (players: PlayerType[], gridClasses: string) => {
return ( return (
<div className="w-full h-full bg-black"> <PlayerWrapper>
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}> <div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => { {players.map((player) => {
const gridArea = getGridArea(player); const gridArea = getGridArea(player);
@@ -42,6 +45,6 @@ export const Player = (players: PlayerType[], gridClasses: string) => {
); );
})} })}
</div> </div>
</div> </PlayerWrapper>
); );
}; };

View File

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

View File

@@ -1,16 +1,10 @@
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { Orientation } from '../../Types/Settings'; import { Orientation } from '../../Types/Settings';
import { Player } from '../Player/Player'; 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;
`;
export const Play = () => { export const Play = () => {
const { players } = usePlayers(); const { players } = usePlayers();

View File

@@ -1,6 +1,5 @@
import { FormControlLabel, Radio, RadioGroup } from '@mui/material'; import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { import {
FivePlayers, FivePlayers,
@@ -15,14 +14,11 @@ import {
TwoPlayersSameSide, TwoPlayersSameSide,
} from '../../../Icons/generated/Layouts'; } from '../../../Icons/generated/Layouts';
import { twc } from 'react-twc';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape'; import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
import { Orientation } from '../../../Types/Settings'; import { Orientation } from '../../../Types/Settings';
const LayoutWrapper = styled.div` const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
flex-direction: row;
display: flex;
justify-content: space-evenly;
`;
type LayoutOptionsProps = { type LayoutOptionsProps = {
numberOfPlayers: number; numberOfPlayers: number;
@@ -35,14 +31,16 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
selectedOrientation, 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={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
@@ -62,6 +60,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
@@ -85,11 +84,12 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
</> </div>
); );
case 2: case 2:
return ( return (
@@ -98,6 +98,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersSameSide <TwoPlayersSameSide
height={iconHeight} height={iconHeight}
@@ -121,6 +122,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositePortrait <TwoPlayersOppositePortrait
height={iconHeight} height={iconHeight}
@@ -144,6 +146,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.OppositeLandscape} value={Orientation.OppositeLandscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositeLandscape <TwoPlayersOppositeLandscape
height={iconHeight} height={iconHeight}
@@ -172,6 +175,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayers <ThreePlayers
height={iconHeight} height={iconHeight}
@@ -195,6 +199,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayersSide <ThreePlayersSide
height={iconHeight} height={iconHeight}
@@ -224,6 +229,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayers <FourPlayers
height={iconHeight} height={iconHeight}
@@ -247,6 +253,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayersSide <FourPlayersSide
height={iconHeight} height={iconHeight}
@@ -276,6 +283,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayers <FivePlayers
height={iconHeight} height={iconHeight}
@@ -328,6 +336,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayers <SixPlayers
height={iconHeight} height={iconHeight}

View File

@@ -1,50 +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 { createInitialPlayers } from '../../../Data/getInitialPlayers'; import { createInitialPlayers } from '../../../Data/getInitialPlayers';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { useAnalytics } from '../../../Hooks/useAnalytics'; import { useAnalytics } from '../../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../Hooks/usePlayers'; import { usePlayers } from '../../../Hooks/usePlayers';
import { Cog, Info } from '../../../Icons/generated'; import { Cog, Info } from '../../../Icons/generated';
import { InitialGameSettings, Orientation } from '../../../Types/Settings'; import {
GameFormat,
InitialGameSettings,
Orientation,
} from '../../../Types/Settings';
import { InfoModal } from '../../Misc/InfoModal'; import { InfoModal } from '../../Misc/InfoModal';
import { SettingsModal } from '../../Misc/SettingsModal'; import { SettingsModal } from '../../Misc/SettingsModal';
import { Spacer } from '../../Misc/Spacer';
import { SupportMe } from '../../Misc/SupportMe'; import { SupportMe } from '../../Misc/SupportMe';
import { H1, Paragraph } from '../../Misc/TextComponents';
import { LayoutOptions } from './LayoutOptions'; 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 z-10`;
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 +100,7 @@ const Start = () => {
startingLifeTotal: 40, startingLifeTotal: 40,
useCommanderDamage: true, useCommanderDamage: true,
orientation: Orientation.Portrait, orientation: Orientation.Portrait,
gameFormat: 'commander', gameFormat: GameFormat.Commander,
} }
); );
@@ -190,9 +172,14 @@ 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
</h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
<FormControl focused={false} style={{ width: '100%' }}>
<FormLabel>Number of Players</FormLabel> <FormLabel>Number of Players</FormLabel>
<SliderWrapper>
<Slider <Slider
title="Number of Players" title="Number of Players"
max={6} max={6}
@@ -210,8 +197,10 @@ const Start = () => {
}); });
}} }}
/> />
<Spacer height="0.7rem" /> </SliderWrapper>
<FormLabel>Starting Health</FormLabel>
<FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
<SliderWrapper>
<Slider <Slider
title="Starting Health" title="Starting Health"
max={60} max={60}
@@ -229,9 +218,9 @@ const Start = () => {
}) })
} }
/> />
<Spacer height="1rem" /> </SliderWrapper>
<ToggleButtonsWrapper> <ToggleButtonsWrapper className="mt-4">
<ToggleContainer> <ToggleContainer>
<FormLabel>Commander</FormLabel> <FormLabel>Commander</FormLabel>
<Switch <Switch
@@ -289,7 +278,6 @@ const Start = () => {
numberOfPlayers={playerOptions.numberOfPlayers} numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation} selectedOrientation={playerOptions.orientation}
onChange={(orientation) => { onChange={(orientation) => {
console.log('orientation', { orientation });
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
orientation, orientation,
@@ -297,23 +285,21 @@ const Start = () => {
}} }}
/> />
</FormControl> </FormControl>
{!isPWA && ( {!isPWA && (
<Paragraph <p className="text-center text-xs text-text-primary w-11/12 mt-4">
style={{ textAlign: 'center', maxWidth: '75%', fontSize: '0.7rem' }}
>
If you're on iOS, this page works better if you{' '} If you're on iOS, this page works better if you{' '}
<strong>hide the toolbar</strong> or{' '} <strong>hide the toolbar</strong> or{' '}
<strong>add the app to your home screen</strong>. <strong>add the app to your home screen</strong>.
</Paragraph> </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

@@ -229,6 +229,7 @@ export const createInitialPlayers = ({
extraCounters: [], extraCounters: [],
commanderDamage, commanderDamage,
hasLost: false, hasLost: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
}; };
players.push(player); players.push(player);

View File

@@ -1,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

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
export interface OrientationState {
angle: number;
type: string;
}
const defaultState: OrientationState = {
angle: 0,
type: 'landscape-primary',
};
export default function useOrientation(
initialState: OrientationState = defaultState
) {
const [state, setState] = useState(initialState);
const [isLandscape, setIsLandscape] = useState(false);
useEffect(() => {
const screen = window.screen;
let mounted = true;
const onChange = () => {
if (mounted) {
const { orientation } = screen;
console.log(orientation);
if (orientation) {
const { angle, type } = orientation;
setState({ angle, type });
if (type.includes('landscape')) {
setIsLandscape(true);
} else if (type.includes('portrait')) {
setIsLandscape(false);
}
} else if (window.orientation !== undefined) {
setState({
angle:
typeof window.orientation === 'number' ? window.orientation : 0,
type: '',
});
}
}
};
onChange();
return () => {
mounted = false;
};
}, [isLandscape]);
return { state, isLandscape };
}

View File

@@ -5,7 +5,11 @@ import {
GlobalSettingsContextType, GlobalSettingsContextType,
} from '../Contexts/GlobalSettingsContext'; } from '../Contexts/GlobalSettingsContext';
import { useAnalytics } from '../Hooks/useAnalytics'; import { useAnalytics } from '../Hooks/useAnalytics';
import { InitialGameSettings, Orientation, Settings } from '../Types/Settings'; import {
InitialGameSettings,
InitialGameSettingsSchema,
Settings,
} from '../Types/Settings';
export const GlobalSettingsProvider = ({ export const GlobalSettingsProvider = ({
children, children,
@@ -22,34 +26,52 @@ export const GlobalSettingsProvider = ({
savedShowPlay ? savedShowPlay === 'true' : false savedShowPlay ? savedShowPlay === 'true' : false
); );
const [initialGameSettings, setInitialSettings] = const [initialGameSettings, setInitialGameSettings] =
useState<InitialGameSettings | null>( useState<InitialGameSettings | null>(
savedGameSettings ? JSON.parse(savedGameSettings) : null savedGameSettings ? JSON.parse(savedGameSettings) : null
); );
const setInitialGameSettings = (initialGameSettings: InitialGameSettings) => {
const defaultSettings: InitialGameSettings = {
numberOfPlayers: 4,
startingLifeTotal: 40,
useCommanderDamage: true,
orientation: Orientation.Landscape,
gameFormat: 'commander',
};
setInitialSettings({ ...defaultSettings, ...initialGameSettings });
};
const [settings, setSettings] = useState<Settings>( const [settings, setSettings] = useState<Settings>(
savedSettings savedSettings
? JSON.parse(savedSettings) ? JSON.parse(savedSettings)
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true } : {
goFullscreenOnStart: true,
keepAwake: true,
showStartingPlayer: true,
showPlayerMenuCog: true,
}
); );
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));
@@ -78,14 +100,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');
@@ -100,7 +114,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

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

View File

@@ -1,12 +1,21 @@
import { z } from 'zod';
export enum Orientation { export enum Orientation {
OppositeLandscape = 'opposite-landscape', OppositeLandscape = 'opposite-landscape',
Landscape = 'landscape', Landscape = 'landscape',
Portrait = 'portrait', Portrait = 'portrait',
} }
export enum GameFormat {
Commander = 'commander',
Standard = 'standard',
TwoHeadedGiant = 'two-headed-giant',
}
export type Settings = { export type Settings = {
keepAwake: boolean; keepAwake: boolean;
showStartingPlayer: boolean; showStartingPlayer: boolean;
showPlayerMenuCog: boolean;
goFullscreenOnStart: boolean; goFullscreenOnStart: boolean;
}; };
@@ -18,4 +27,10 @@ export type InitialGameSettings = {
orientation: Orientation; 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

@@ -0,0 +1,87 @@
type RGBA = {
red: number;
green: number;
blue: number;
alpha: number;
};
export const hexToRgb = (hex: string): RGBA => {
hex = hex.replace(/^#/, '');
let alpha = 255;
if (hex.length === 8) {
alpha = parseInt(hex.slice(6, 8), 16);
hex = hex.substring(0, 6);
}
if (hex.length === 4) {
alpha = parseInt(hex.slice(3, 4).repeat(2), 16);
hex = hex.substring(0, 3);
}
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const num = parseInt(hex, 16);
const red = num >> 16;
const green = (num >> 8) & 255;
const blue = num & 255;
return { red, green, blue, alpha };
};
export const luminance = (a: number, b: number) => {
const l1 = Math.max(a, b);
const l2 = Math.min(a, b);
return (l1 + 0.05) / (l2 + 0.05);
};
export const rgbContrast = (a: RGBA, b: RGBA) => {
return luminance(relativeLuminance(a), relativeLuminance(b));
};
// calculate the color contrast ratio
export const checkContrast = (hexC1: string, hexC2: string) => {
const color1rgb = hexToRgb(hexC1);
const color2rgb = hexToRgb(hexC2);
const contrast = rgbContrast(color1rgb, color2rgb);
if (contrast >= 7) {
return 'AAA';
}
if (contrast >= 4.5) {
return 'AA';
}
if (contrast >= 3) {
return 'AA Large';
}
return 'Fail';
};
// red, green, and blue coefficients
const rc = 0.2126;
const gc = 0.7152;
const bc = 0.0722;
// low-gamma adjust coefficient
const lowc = 1 / 12.92;
function adjustGamma(input: number) {
return Math.pow((input + 0.055) / 1.055, 2.4);
}
export const relativeLuminance = (rgb: RGBA) => {
const rsrgb = rgb.red / 255;
const gsrgb = rgb.green / 255;
const bsrgb = rgb.blue / 255;
const r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb);
const g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb);
const b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb);
return r * rc + g * gc + b * bc;
};

View File

@@ -2,7 +2,19 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html {
overflow: hidden;
}
body { body {
overflow: auto;
}
html,
body {
height: 100%;
position: relative;
background-color: theme('colors.background.default');
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -10,8 +22,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;
}
}

View File

@@ -1,9 +1,58 @@
//@ts-expect-error - tailwindcss-grid-areas does not have typescript support
import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas'; import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas';
import type { Config } from 'tailwindcss';
export const baseColors = {
primary: {
main: '#3E7D78',
dark: '#2D5F5B',
},
secondary: {
main: '#284F4C',
dark: '#1B3B38',
},
background: {
default: '#08253B',
backdrop: 'rgba(0, 0, 0, 0.3)',
settings: 'rgba(20, 20, 0, 0.9)',
},
icons: {
dark: '#00000080',
light: '#ffffff4f',
},
text: {
primary: '#F5F5F5',
secondary: '#76A6A5',
},
action: {
disabled: '#234A47',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#000000',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
};
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
screens: {
modalSm: '548px',
},
extend: { extend: {
gridTemplateAreas: { gridTemplateAreas: {
onePlayerLandscape: ['player0 player0'], onePlayerLandscape: ['player0 player0'],
@@ -35,7 +84,25 @@ export default {
'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3', 'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3',
], ],
}, },
colors: baseColors,
keyframes: {
fadeOut: {
'0%': {
opacity: '1',
},
'33%': {
opacity: '0.6',
},
'100%': {
opacity: '0',
},
},
},
animation: {
fadeOut: 'fadeOut 3s 1s ease-out forwards',
},
}, },
}, },
plugins: [tailwindcssGridAreas], 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),
},
}); });

2345
yarn.lock

File diff suppressed because it is too large Load Diff