Compare commits

...

70 Commits
0.5.3 ... 0.9.1

Author SHA1 Message Date
Viktor Rådberg
28c2ff536f bump 2024-03-30 10:28:49 +01:00
Viktor Rådberg
6beddf06e2 better invalid settings handling 2024-03-30 10:27:16 +01:00
Viktor Rådberg
2a885f9a43 bump 2024-03-29 23:25:50 +01:00
Viktor Rådberg
9c27f34261 More Pre-Start modes. (#34)
* do

* parse settings before setting
2024-03-29 23:24:35 +01:00
Viktor Rådberg
fa5829b402 fix keep away state 2024-03-23 16:23:03 +01:00
Viktor Rådberg
71f26d0dc5 fix keep awake toggle and layout styling 2024-03-23 16:22:48 +01:00
Viktor Rådberg
3a568fc3ab Better scaling on small devices 2024-03-23 16:05:29 +01:00
Viktor Rådberg
355f4bd4cd track only on prod, and add life changed amount tracking 2024-03-23 12:39:07 +01:00
Viktor Rådberg
17e174bfe1 new deploy 2024-03-23 11:24:14 +01:00
Viktor Rådberg
e1e8da858b remove prod check 2024-03-23 11:23:46 +01:00
Viktor Rådberg
e02f071415 new deploy 2024-03-23 11:23:06 +01:00
Viktor Rådberg
e04f31bb67 prod log 2024-03-23 11:22:12 +01:00
Viktor Rådberg
e5386d08a4 fix settings cog color 2024-03-17 19:01:15 +01:00
Viktor Rådberg
d6cd678e9f fix random interval wrapper showing if enabled but show player is disabled 2024-03-17 18:41:58 +01:00
Viktor Rådberg
334b46db6e bump 2024-03-16 22:29:25 +01:00
Viktor Rådberg
e03ecc6f51 Merge pull request #33 from Vikeo/random-player-interval
random player interval
2024-03-16 22:28:49 +01:00
Viktor Rådberg
d4dc44076d fix lint 2024-03-16 22:28:37 +01:00
Viktor Rådberg
a1b5cfd871 fix tsc 2024-03-16 22:26:06 +01:00
Viktor Rådberg
f11eea5e53 better styling 2024-03-16 22:23:03 +01:00
Viktor Rådberg
905912a7fd fix random interval 2024-03-16 21:59:24 +01:00
Viktor Rådberg
a90dd7c9ea wip 2024-03-16 14:40:18 +01:00
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
40 changed files with 3992 additions and 954 deletions

View File

@@ -1,8 +1,12 @@
robots.txt,1693082171694,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 index.html,1711189442688,fa2549e32940c356ac5cee88c8db61076ad62fb4e599858c8e45cfc68cd901c4
manifest.json,1693082171694,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a manifest.json,1711189442512,7ff5111aa04a42adff3b38924ec467b6f345ed0309dba1486dc9b783b60c2a9d
assets/index-5265c558.css,1693082171837,08c4451946bbdf520fe337edb365417a8bbf91914c018b83866723ef52d57b43 registerSW.js,1711189442688,5b6445b5215737c53ef0d379c151d57de165a056de2d1c5812ed614f158ebcbd
index.html,1693082171837,09e1919fbaaa3a0bf08f43eb46c29136d62a7747b41f8b5d0f4a7ed23337c344 sw.js,1711189443521,9c09d33ea573bb818864bfad526fa911839637171773eca8e31905458679846d
logo192.png,1693082171693,4309255bccbdbb341b5ab88708677e3d43b9e171d2666528ff932295a8257e4e robots.txt,1711189442512,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
favicon.ico,1693082171692,48d8c1b9714dbc9bcb012d9c9f04112d229f20e6c889bda588ac159f973e6a8d manifest.webmanifest,1711189442688,f2bf253209f6e292a6b0dbfa06fb4ac188eb5f2dba568c3ad5511b9ed52c1f51
logo512.png,1693082171694,92c7c05dc98170596d04f48e5e60eaae9535f409bcaeff129fd98fef8aba9f4e workbox-3e911b1d.js,1711189443521,d5dbc868a5c07af633d29de7ba3ffe37542aaaabdf33713b4298df31f92f11ff
assets/index-5023e89e.js,1693082171838,8a6177168e95e1ca90e5ad8774252a8a02a9a78765bd329b7deae729c01aedf3 assets/index-WLCHZTqE.css,1711189442688,877e5ea9bfd3a1ca0e6449e8213da8a3c7717e530370f12669bb5c70dd21e700
favicon.ico,1711189442511,8cefe5adbf00d337d8633fb744b9f2c4914f769b319be4bb7e184b7a4aa17160
logo192.png,1711189442511,3d1e2e6f064d4fd325828f21bb6493ff0dbf2390b0e7d2aba9f2b6def4829799
logo512.png,1711189442511,892a4da1cc5434929a83a71fcbcb0d0d80aa82f44e3c21e9b20ffe9267197133
assets/index-OHs0lOr7.js,1711189442688,aa0dca732cd5b6f621ecb7c6dbcbfdbccde78941cfad954f6626d4ff83040c7f

View File

@@ -4,32 +4,32 @@ name: Deploy to Firebase Hosting
tags: tags:
- '*' - '*'
jobs: jobs:
# build_and_deploy: build_and_deploy:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# env: env:
# REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }} REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }}
# steps: steps:
# - name: Checkout repository - name: Checkout repository
# uses: actions/checkout@v3 uses: actions/checkout@v3
# - name: Setup bun - name: Setup bun
# uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
# - name: Build, lint, and deploy - name: Build, lint, and deploy
# run: | run: |
# bun install bun install
# bun run build bun run build
# bun run lint bun run lint
# - name: Deploy to Firebase Hosting - name: Deploy to Firebase Hosting
# uses: FirebaseExtended/action-hosting-deploy@v0 uses: FirebaseExtended/action-hosting-deploy@v0
# with: with:
# repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
# firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_LIFE_TRINKET }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_LIFE_TRINKET }}'
# channelId: live channelId: live
# projectId: life-trinket projectId: life-trinket
release: release:
# needs: build_and_deploy needs: build_and_deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
@@ -52,7 +52,7 @@ jobs:
- name: Create Release - name: Create Release
uses: ncipollo/release-action@v1.13.0 uses: ncipollo/release-action@v1.13.0
with: with:
body: release_note.txt bodyFile: release_note.txt
commit: ${{ github.sha }} commit: ${{ github.sha }}
tag: '${{ steps.version.outputs.prop }}' tag: '${{ steps.version.outputs.prop }}'
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{ {
"name": "life-trinket", "name": "life-trinket",
"private": true, "private": true,
"version": "0.5.3", "version": "0.9.1",
"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,6 +22,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-screen-wake-lock": "^3.0.2", "react-screen-wake-lock": "^3.0.2",
"react-swipeable": "^7.0.1",
"react-twc": "^1.3.0", "react-twc": "^1.3.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
@@ -43,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

@@ -17,6 +17,8 @@ export type RotationButtonProps = TwcComponentProps<'button'> & {
$rotation?: number; $rotation?: number;
}; };
export const MAX_TAP_MOVE_DISTANCE = 20;
const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [ const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow', 'flex flex-grow',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
@@ -38,7 +40,7 @@ const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
: '', : '',
]); ]);
const PartnerDamageSeperator = twc.div<RotationDivProps>((props) => [ const PartnerDamageSeparator = twc.div<RotationDivProps>((props) => [
'bg-black', 'bg-black',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'w-full h-px' ? 'w-full h-px'
@@ -54,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 = ({
@@ -63,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,
@@ -107,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
@@ -145,10 +157,12 @@ 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();
@@ -171,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={(

View File

@@ -3,7 +3,7 @@ import { twc } from 'react-twc';
import { decrementTimeoutMs } from '../../Data/constants'; import { decrementTimeoutMs } from '../../Data/constants';
import { CounterType, Rotation } from '../../Types/Player'; import { CounterType, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
import { RotationDivProps } from './CommanderDamage'; import { MAX_TAP_MOVE_DISTANCE, RotationDivProps } from './CommanderDamage';
const ExtraCounterContainer = twc.div` const ExtraCounterContainer = twc.div`
flex flex
@@ -47,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;
}; };
@@ -56,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) {
@@ -73,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(() => {
@@ -82,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);

View File

@@ -1,7 +1,9 @@
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { TwcComponentProps, twc } from 'react-twc'; import { TwcComponentProps, twc } from 'react-twc';
import { lifeLongPressMultiplier } from '../../Data/constants'; import { lifeLongPressMultiplier } from '../../Data/constants';
import { Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { MAX_TAP_MOVE_DISTANCE } from './CommanderDamage';
import { checkContrast } from '../../Utils/checkContrast';
type RotationButtonProps = TwcComponentProps<'div'> & { type RotationButtonProps = TwcComponentProps<'div'> & {
$align?: string; $align?: string;
@@ -12,7 +14,6 @@ const LifeCounterButtonTwc = twc.button`
h-full h-full
w-full w-full
flex flex
text-lifeCounter-text
font-semibold font-semibold
bg-transparent bg-transparent
border-none border-none
@@ -39,29 +40,41 @@ const TextContainer = twc.div<RotationButtonProps>((props) => [
]); ]);
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(() => {
@@ -70,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);
@@ -86,7 +112,8 @@ 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';
@@ -102,8 +129,11 @@ 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>

View File

@@ -4,13 +4,11 @@ import { Rotation } from '../../Types/Player';
import { RotationDivProps } from './CommanderDamage'; import { RotationDivProps } from './CommanderDamage';
const LoseButton = twc.div<RotationDivProps>((props) => [ const LoseButton = twc.div<RotationDivProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-interface-loseButton-background rounded-lg select-none z-[1] webkit-user-select-none', '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 ',
props.$rotation === Rotation.SideFlipped props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `right-auto top-[15%] left-[27%]` ? `left-[21%]`
: props.$rotation === Rotation.Side : 'top-[21%]',
? `right-auto top-[15%] left-[27%]`
: 'right-[15%] top-1/4',
]); ]);
type LoseButtonProps = { type LoseButtonProps = {
@@ -24,6 +22,8 @@ export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
? rotation ? rotation
: rotation === Rotation.Side : rotation === Rotation.Side
? rotation - 180 ? rotation - 180
: rotation === Rotation.Flipped
? rotation - 180
: rotation; : rotation;
return ( return (
@@ -33,7 +33,7 @@ export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
aria-label={`Lose Game`} aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }} style={{ rotate: `${calcRotation}deg` }}
> >
<Skull size="5vmin" color="black" opacity={0.5} /> <Skull size="8vmin" color="black" opacity={0.5} />
</LoseButton> </LoseButton>
); );
}; };

View File

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

View File

@@ -10,6 +10,8 @@ import {
import { CounterType, Player, Rotation } from '../../Types/Player'; import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter'; import ExtraCounter from '../Buttons/ExtraCounter';
import { useEffect, useState } from 'react';
import { checkContrast } from '../../Utils/checkContrast';
const Container = twc.div<RotationDivProps>((props) => [ const Container = twc.div<RotationDivProps>((props) => [
'flex', 'flex',
@@ -19,9 +21,9 @@ const Container = twc.div<RotationDivProps>((props) => [
]); ]);
export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [ export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [
'flex absolute flex-row flex-grow pointer-events-none', 'flex absolute flex-row flex-grow pointer-events-none overflow-x-scroll overflow-y-hidden',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse h-full w-auto bottom-auto' ? 'flex-col-reverse h-full w-auto bottom-auto right-0'
: 'w-full bottom-0', : 'w-full bottom-0',
]); ]);
@@ -31,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,
@@ -93,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}
/> />
@@ -107,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}
/> />
@@ -121,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}
/> />
@@ -134,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}
/> />
@@ -147,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

@@ -8,7 +8,7 @@ import {
import LifeCounterButton from '../Buttons/LifeCounterButton'; import LifeCounterButton from '../Buttons/LifeCounterButton';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
const LifeCountainer = twc.div<RotationDivProps>((props) => [ const LifeContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow relative w-full h-full justify-between items-center', 'flex flex-grow relative w-full h-full justify-between items-center',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse' ? 'flex-col-reverse'
@@ -49,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;
@@ -99,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);
@@ -116,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}
/> />
@@ -148,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,16 +1,64 @@
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
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 } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { checkContrast } from '../../Utils/checkContrast';
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 PlayerMenu from '../Player/PlayerMenu'; import PlayerMenu from '../Players/PlayerMenu';
import { StartingPlayerCard } from '../PreStartGame/StartingPlayerCard';
import Health from './Health'; import Health from './Health';
const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none',
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped
? `right-auto top-[1vmax] left-[27%]`
: 'top-1/4 right-[1vmax]',
]);
type SettingsButtonProps = {
onClick: () => void;
rotation: Rotation;
color: string;
};
const SettingsButton = ({ onClick, rotation, color }: SettingsButtonProps) => {
const [iconColor, setIconColor] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const contrast = checkContrast(color, '#00000080');
if (contrast === 'Fail') {
setIconColor('light');
} else {
setIconColor('dark');
}
}, [color]);
return (
<SettingsButtonTwc
onClick={onClick}
$rotation={rotation}
aria-label={`Settings`}
>
<Cog
size="5vmin"
data-contrast={iconColor}
className="data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
/>
</SettingsButtonTwc>
);
};
const LifeCounterContentWrapper = twc.div` const LifeCounterContentWrapper = twc.div`
relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`; relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
@@ -21,17 +69,13 @@ const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [
: `flex-col`, : `flex-col`,
]); ]);
const StartingPlayerNoticeWrapper = twc.div`z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-primary-main`;
const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [ const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [
'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper', 'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper opacity-75',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `rotate-[${props.$rotation - 90}deg]` ? `rotate-[${props.$rotation - 90}deg]`
: '', : '',
]); ]);
const DynamicText = twc.div`text-[8vmin]`;
const hasCommanderDamageReached21 = (player: Player) => { const hasCommanderDamageReached21 = (player: Player) => {
const commanderDamageTotals = player.commanderDamage.map( const commanderDamageTotals = player.commanderDamage.map(
(commanderDamage) => commanderDamage.damageTotal (commanderDamage) => commanderDamage.damageTotal
@@ -62,36 +106,79 @@ const playerCanLose = (player: Player) => {
type LifeCounterProps = { type LifeCounterProps = {
player: Player; player: Player;
opponents: Player[]; opponents: Player[];
isStartingPlayer?: boolean;
}; };
const RECENT_DIFFERENCE_TTL = 3_000;
const LifeCounter = ({ player, opponents }: LifeCounterProps) => { const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
const { updatePlayer, updateLifeTotal } = usePlayers(); const { updatePlayer, updateLifeTotal } = usePlayers();
const { settings } = useGlobalSettings(); const { settings, playing } = useGlobalSettings();
const recentDifferenceTimerRef = useRef<NodeJS.Timeout | undefined>(
undefined
);
const [showPlayerMenu, setShowPlayerMenu] = useState(false); const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0); const [recentDifference, setRecentDifference] = useState(0);
const [differenceKey, setDifferenceKey] = useState(Date.now()); const [differenceKey, setDifferenceKey] = useState(Date.now());
const [isLandscape, setIsLandscape] = useState(false);
const calcRot = player.isSide
? player.settings.rotation - 180
: player.settings.rotation;
const rotationAngle = isLandscape ? calcRot : calcRot + 90;
const handlers = useSwipeable({
trackMouse: true,
onSwipedDown: (e) => {
e.event.stopPropagation();
setShowPlayerMenu(true);
},
onSwipedUp: (e) => {
e.event.stopPropagation();
setShowPlayerMenu(false);
},
swipeDuration: 500,
onSwiping: (e) => e.event.stopPropagation(),
rotationAngle,
});
const analytics = useAnalytics();
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { if (recentDifference === 0) {
setRecentDifference(0); clearTimeout(recentDifferenceTimerRef.current);
}, 3_000); return;
}
return () => clearTimeout(timer); recentDifferenceTimerRef.current = setTimeout(() => {
analytics.trackEvent('life_changed', {
lifeChangedAmount: recentDifference,
});
setRecentDifference(0);
}, RECENT_DIFFERENCE_TTL);
return () => {
clearTimeout(recentDifferenceTimerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference]); }, [recentDifference]);
useEffect(() => { useEffect(() => {
if (player.showStartingPlayer) { const resizeObserver = new ResizeObserver(() => {
const playingTimer = setTimeout(() => { if (document.body.clientWidth > document.body.clientHeight)
localStorage.setItem('playing', 'true'); setIsLandscape(true);
player.showStartingPlayer = false; else setIsLandscape(false);
updatePlayer(player); return () => {
}, 3_000); // Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
});
return () => clearTimeout(playingTimer); resizeObserver.observe(document.body);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [player.showStartingPlayer]); }, [document.body.clientHeight, document.body.clientWidth]);
player.settings.rotation === Rotation.SideFlipped || player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side; player.settings.rotation === Rotation.Side;
@@ -113,45 +200,39 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
? player.settings.rotation - 90 ? player.settings.rotation - 90
: player.settings.rotation; : player.settings.rotation;
const calcTextRotation = const amountOfPlayers = opponents.length + 1;
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
return ( return (
<LifeCounterContentWrapper style={{ background: player.color }}> <LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper <LifeCounterWrapper
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }} style={{ rotate: `${calcRotation}deg` }}
{...handlers}
> >
{settings.showStartingPlayer && {amountOfPlayers > 1 &&
player.isStartingPlayer && !playing &&
player.showStartingPlayer && ( settings.showStartingPlayer &&
<StartingPlayerNoticeWrapper player.isStartingPlayer && <StartingPlayerCard player={player} />}
style={{ rotate: `${calcRotation}deg` }}
>
<DynamicText style={{ rotate: `${calcTextRotation}deg` }}>
You start!
</DynamicText>
</StartingPlayerNoticeWrapper>
)}
{player.hasLost && ( {player.hasLost && (
<PlayerLostWrapper $rotation={player.settings.rotation} /> <PlayerLostWrapper $rotation={player.settings.rotation} />
)} )}
<CommanderDamageBar <CommanderDamageBar
opponents={opponents} opponents={opponents}
player={player} player={player}
key={player.index} key={player.index}
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<SettingsButton {settings.showPlayerMenuCog && (
onClick={() => { <SettingsButton
setShowPlayerMenu(!showPlayerMenu); onClick={() => {
}} setShowPlayerMenu(!showPlayerMenu);
rotation={player.settings.rotation} }}
/> rotation={player.settings.rotation}
color={player.color}
/>
)}
{playerCanLose(player) && ( {playerCanLose(player) && (
<LoseGameButton <LoseGameButton
rotation={player.settings.rotation} rotation={player.settings.rotation}
@@ -166,9 +247,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<ExtraCountersBar player={player} /> <ExtraCountersBar player={player} />
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} /> <PlayerMenu
)} isShown={showPlayerMenu}
player={player}
setShowPlayerMenu={setShowPlayerMenu}
/>
</LifeCounterWrapper> </LifeCounterWrapper>
</LifeCounterContentWrapper> </LifeCounterContentWrapper>
); );

View File

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

View File

@@ -7,7 +7,7 @@ export const Separator = ({
}) => { }) => {
return ( return (
<div <div
className={`bg-common-black bg-opacity-30 rounded-full mt-2 mb-2`} className={`bg-common-white bg-opacity-30 rounded-full mt-2 mb-2`}
style={{ width, height }} style={{ width, height }}
/> />
); );

View File

@@ -1,14 +1,16 @@
import { Button, FormLabel, Modal, Switch } from '@mui/material'; import { Button, Modal, Switch } from '@mui/material';
import { useEffect, useState } from 'react';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { Cross } from '../../Icons/generated';
import { PreStartMode } from '../../Types/Settings';
import { ModalWrapper } from './InfoModal'; import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { useEffect, useRef, useState } from 'react';
const SettingContainer = twc.div`w-full flex flex-col`; const SettingContainer = twc.div`w-full flex flex-col mb-2`;
const ToggleContainer = twc.div`flex flex-row justify-between items-center`; const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`;
const Container = twc.div`flex flex-col items-center w-full`; const Container = twc.div`flex flex-col items-center w-full`;
@@ -22,7 +24,7 @@ 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 [isLatestVersion, setIsLatestVersion] = useState(false);
const newVersion = useRef<string | undefined>(undefined); const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@@ -44,17 +46,19 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const data = await result.json(); const data = await result.json();
if (!data.name) { if (!data.name) {
setNewVersion(undefined);
setIsLatestVersion(false); setIsLatestVersion(false);
newVersion.current = undefined;
return; return;
} }
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/ /* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) { if (data.name === APP_VERSION) {
newVersion.current = data.name;
setIsLatestVersion(true); setIsLatestVersion(true);
return; return;
} }
setIsLatestVersion(false); setIsLatestVersion(false);
} catch (error) { } catch (error) {
console.error('error getting latest version string', error); console.error('error getting latest version string', error);
@@ -64,116 +68,203 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal
<ModalWrapper> open={isOpen}
<Container> onClose={closeModal}
<h2 style={{ textAlign: 'center' }}> Settings </h2> className="w-full flex justify-center"
<SettingContainer> >
<ToggleContainer> <>
<FormLabel>Show Start Player</FormLabel> <div className="flex justify-center items-center relative w-full max-w-[532px]">
<Switch <button
checked={settings.showStartingPlayer} onClick={closeModal}
onChange={() => { 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"
setSettings({ >
...settings, <Cross size="16px" className="text-text-primary " />
showStartingPlayer: !settings.showStartingPlayer, </button>
}); </div>
}} <ModalWrapper>
/> <Container>
</ToggleContainer> <h2 className="text-center text-2xl mb-2"> Settings </h2>
<Description> <SettingContainer>
On start or reset of game, will pick a random player who will <Paragraph>
start first if this is enabled. {/* @ts-expect-error is defined in vite.config.ts*/}
</Description> Current version: {APP_VERSION}{' '}
</SettingContainer> {isLatestVersion && (
<SettingContainer> <span className="text-sm text-text-secondary">(latest)</span>
<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>
<ToggleContainer>
<FormLabel>Go fullscreen on start (Android only)</FormLabel>
<Switch
checked={settings.goFullscreenOnStart}
onChange={() => {
setSettings({
...settings,
goFullscreenOnStart: !settings.goFullscreenOnStart,
});
}}
/>
</ToggleContainer>
<Description>
Will enter fullscreen mode when starting a game if this is
enabled.
</Description>
</SettingContainer>
{!isPWA && (
<>
<Separator height="1px" />
<SettingContainer>
<ToggleContainer>
<Paragraph>
<b>Tip:</b> You can{' '}
<b>add this webapp to your home page on iOS</b> or{' '}
<b>install it on Android</b> to have it act just like a
normal app!
</Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will be
automatically hidden.
</Description>
</SettingContainer>
</>
)}
<Separator height="1px" />
<SettingContainer>
<Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion.current && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion.current}) is available!{' '}
</Paragraph> </Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
)}
</SettingContainer>
{!isLatestVersion && newVersion && (
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)} )}
</SettingContainer> <Separator height="1px" />
{!isLatestVersion && newVersion.current && (
<SettingContainer>
<ToggleContainer>
<label>Show Player Menu Cog</label>
<Switch
checked={settings.showPlayerMenuCog}
onChange={() => {
setSettings({
...settings,
showPlayerMenuCog: !settings.showPlayerMenuCog,
});
}}
/>
</ToggleContainer>
<Description>
A cog on the top right of each player's card will be shown if
this is enabled.
</Description>
</SettingContainer>
<SettingContainer>
<ToggleContainer>
<label>Show Start Player</label>
<Switch
checked={settings.showStartingPlayer}
onChange={() => {
setSettings({
...settings,
showStartingPlayer: !settings.showStartingPlayer,
});
}}
/>
</ToggleContainer>
<Description>
On start or reset of game, will pick a random starting player,
according to the <b>Pre-Start mode</b>
</Description>
</SettingContainer>
<SettingContainer>
<div className="flex flex-row justify-between items-center mb-1">
<label htmlFor="pre-start-modes">Pre-Start mode</label>
<select
name="pre-start-modes"
id="pre-start-modes"
value={settings.preStartMode}
className="bg-primary-main border-none outline-none text-text-primary rounded-md p-1 text-xs disabled:bg-primary-dark"
onChange={(e) => {
setSettings({
...settings,
preStartMode: e.target.value as PreStartMode,
});
}}
disabled={!settings.showStartingPlayer}
>
<option value={PreStartMode.None}>None</option>
<option value={PreStartMode.RandomKing}>Random King</option>
<option value={PreStartMode.FingerGame}>Finger Game</option>
</select>
</div>
<div className="text-xs text-left text-text-secondary">
Different ways to determine the starting player before the game
starts.
</div>
{settings.preStartMode === PreStartMode.None && (
<div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">None:</span> The starting
player will simply be shown.
</div>
)}
{settings.preStartMode === PreStartMode.RandomKing && (
<div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Random King:</span>{' '}
Randomly pass a crown between all players, press the screen to
stop it. The player who has the crown when it stops gets to
start.
</div>
)}
{settings.preStartMode === PreStartMode.FingerGame && (
<div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Finger Game:</span> All
players put a finger on the screen, one will be chosen at
random.
</div>
)}
</SettingContainer>
<SettingContainer>
<ToggleContainer>
<label>Keep Awake</label>
<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>
<ToggleContainer>
<label>
Fullscreen on start{' '}
<span className="text-xs">(Android only)</span>
</label>
<Switch
checked={settings.goFullscreenOnStart}
onChange={() => {
setSettings({
...settings,
goFullscreenOnStart: !settings.goFullscreenOnStart,
});
}}
/>
</ToggleContainer>
<Description>
Will enter fullscreen mode when starting a game if this is
enabled.
</Description>
</SettingContainer>
{!isPWA && (
<>
<Separator height="1px" />
<SettingContainer>
<ToggleContainer>
<Paragraph>
<b>Tip:</b> You can{' '}
<b>add this webapp to your home page on iOS</b> or{' '}
<b>install it on Android</b> to have it act just like a
normal app!
</Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will
be automatically hidden.
</Description>
</SettingContainer>
</>
)}
<Separator height="1px" />
<Button <Button
variant="contained" variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }} onClick={closeModal}
onClick={() => window?.location?.reload()} style={{ marginTop: '0.25rem' }}
> >
<span>Update</span> Save and Close
<span className="text-xs">&nbsp;(reload app)</span>
</Button> </Button>
)} </Container>
<Separator height="1px" /> </ModalWrapper>
</>
<Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close
</Button>
</Container>
</ModalWrapper>
</Modal> </Modal>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { Button, Checkbox } from '@mui/material'; import { Checkbox } from '@mui/material';
import { useRef } from 'react'; import { useRef } from 'react';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme'; import { theme } from '../../Data/theme';
@@ -17,12 +17,7 @@ import {
ResetGame, ResetGame,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { import { RotationDivProps } from '../Buttons/CommanderDamage';
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
const CheckboxContainer = twc.div``;
const PlayerMenuWrapper = twc.div` const PlayerMenuWrapper = twc.div`
flex flex
@@ -31,10 +26,12 @@ const PlayerMenuWrapper = twc.div`
w-full w-full
h-full h-full
bg-background-settings bg-background-settings
backdrop-blur-[3px]
items-center items-center
justify-center justify-center
z-[2] z-[2]
webkit-user-select-none webkit-user-select-none
transition-all
`; `;
const BetterRowContainer = twc.div` const BetterRowContainer = twc.div`
@@ -43,75 +40,77 @@ const BetterRowContainer = twc.div`
flex-grow flex-grow
w-full w-full
h-full h-full
justify-end justify-between
items-stretch items-stretch
`; `;
const TogglesSection = twc.div` const TogglesSection = twc.div`
flex flex
relative
flex-row flex-row
gap-2 flex-wrap
relative
h-full
justify-evenly justify-evenly
items-center
`; `;
const ButtonsSections = twc.div` const ButtonsSections = twc.div`
flex flex
max-w-full max-w-full
gap-4 justify-evenly
justify-between
p-[3%]
items-center items-center
flex-wrap
mt-0
px-2
`; `;
const ColorPicker = twc.input` const ColorPickerButton = twc.div`
absolute
top-[5%]
left-[5%]
h-[8vmax] h-[8vmax]
w-[8vmax] w-[8vmax]
border-none relative
outline-none max-h-12
max-w-12
rounded-full
cursor-pointer cursor-pointer
bg-transparent overflow-hidden
user-select-none
text-common-white
`; `;
const SettingsContainer = twc.div<RotationDivProps>((props) => [ const SettingsContainer = twc.div<RotationDivProps>((props) => [
'flex flex-wrap h-full w-full', 'flex flex-wrap h-full w-full overflow-y-scroll',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col' ? 'flex-col'
: 'flex-row', : 'flex-row',
]); ]);
const CloseButton = twc.button<RotationButtonProps>((props) => [
'absolute border-none outline-none cursor-pointer bg-transparent z-[99]',
props.$rotation === Rotation.Side
? `top-[5%] right-auto left-[5%]`
: props.$rotation === Rotation.SideFlipped
? 'top-auto left-auto bottom-[5%] right-[5%]'
: 'top-[15%] 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 endGameDialogRef = 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 {
setShowPlayerMenu(false); fullscreen,
}; wakeLock,
const { fullscreen, wakeLock, goToStart } = useGlobalSettings(); goToStart,
settings,
setPlaying,
setRandomizingPlayer,
} = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers(); const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -129,6 +128,13 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const handleResetGame = () => { const handleResetGame = () => {
resetCurrentGame(); resetCurrentGame();
setShowPlayerMenu(false); setShowPlayerMenu(false);
setPlaying(false);
setRandomizingPlayer(true);
};
const handleGoToStart = () => {
goToStart();
setRandomizingPlayer(true);
}; };
const toggleFullscreen = () => { const toggleFullscreen = () => {
@@ -142,7 +148,6 @@ 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 = const calcRotation =
player.settings.rotation === Rotation.Side player.settings.rotation === Rotation.Side
@@ -156,33 +161,10 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
//TODO: Fix hacky solution to rotation for SideFlipped //TODO: Fix hacky solution to rotation for SideFlipped
style={{ style={{
rotate: rotate:
player.settings.rotation === Rotation.SideFlipped ? '180deg' : '', player.settings.rotation === Rotation.SideFlipped ? `180deg` : '',
translate: isShown ? '' : player.isSide ? `-100%` : `0 -100%`,
}} }}
> >
<CloseButton
$rotation={player.settings.rotation}
style={{
rotate:
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '',
}}
>
<Button
variant="text"
onClick={handleOnClick}
style={{
margin: 0,
padding: 0,
height: closeButtonSize,
width: closeButtonSize,
}}
>
<Cross size={closeButtonSize} />
</Button>
</CloseButton>
<SettingsContainer <SettingsContainer
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{ style={{
@@ -190,17 +172,25 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
}} }}
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<ColorPicker <button
type="color" onClick={() => setShowPlayerMenu(false)}
value={player.color} className="flex absolute top-0 right-2 z-10 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2 p-[0.2rem]"
onChange={handleColorChange} >
role="button" <Cross size={buttonFontSize} className="text-primary-main " />
aria-label="Color picker" </button>
/>
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
<ColorPickerButton aria-label="Color picker">
<input
onChange={handleColorChange}
type="color"
className="size-[200%] absolute -left-2 -top-2"
value={player.color}
/>
</ColorPickerButton>
{player.settings.useCommanderDamage && ( {player.settings.useCommanderDamage && (
<CheckboxContainer> <div>
<Checkbox <Checkbox
name="usePartner" name="usePartner"
checked={player.settings.usePartner} checked={player.settings.usePartner}
@@ -225,10 +215,9 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
aria-checked={player.settings.usePartner} aria-checked={player.settings.usePartner}
aria-label="Partner" aria-label="Partner"
/> />
</CheckboxContainer> </div>
)} )}
<div>
<CheckboxContainer>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
checked={player.settings.usePoison} checked={player.settings.usePoison}
@@ -253,9 +242,8 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
aria-checked={player.settings.usePoison} aria-checked={player.settings.usePoison}
aria-label="Poison" aria-label="Poison"
/> />
</CheckboxContainer> </div>
<div>
<CheckboxContainer>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
checked={player.settings.useEnergy} checked={player.settings.useEnergy}
@@ -280,9 +268,8 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
aria-checked={player.settings.useEnergy} aria-checked={player.settings.useEnergy}
aria-label="Energy" aria-label="Energy"
/> />
</CheckboxContainer> </div>
<div>
<CheckboxContainer>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
checked={player.settings.useExperience} checked={player.settings.useExperience}
@@ -307,21 +294,22 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
aria-checked={player.settings.useExperience} aria-checked={player.settings.useExperience}
aria-label="Experience" aria-label="Experience"
/> />
</CheckboxContainer> </div>
</TogglesSection> </TogglesSection>
<ButtonsSections className="mt-4"> <ButtonsSections>
<Button <button
variant="text" className="text-primary-main cursor-pointer webkit-user-select-none"
style={{ onClick={() => endGameDialogRef.current?.show()}
cursor: 'pointer',
userSelect: 'none',
}}
onClick={goToStart}
aria-label="Back to start" aria-label="Back to start"
> >
<Exit size={iconSize} style={{ rotate: '180deg' }} /> <Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button> </button>
<CheckboxContainer> <div
data-fullscreen={document.fullscreenElement ? true : false}
className="flex
data-[fullscreen=true]:bg-secondary-dark rounded-lg border border-transparent
data-[fullscreen=true]:border-primary-main"
>
<Checkbox <Checkbox
name="fullscreen" name="fullscreen"
checked={document.fullscreenElement ? true : false} checked={document.fullscreenElement ? true : false}
@@ -336,62 +324,115 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
role="checkbox" role="checkbox"
aria-checked={document.fullscreenElement ? true : false} aria-checked={document.fullscreenElement ? true : false}
aria-label="Fullscreen" aria-label="Fullscreen"
style={{ padding: '4px' }}
/> />
</CheckboxContainer> </div>
<Button <button
variant={wakeLock.active ? 'contained' : 'outlined'} data-wake-lock-active={settings.keepAwake}
style={{ style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize, fontSize: buttonFontSize,
padding: '0 4px 0 4px',
}} }}
onClick={wakeLock.toggleWakeLock} className="text-primary-main px-1 webkit-user-select-none cursor-pointer
data-[wake-lock-active=true]:bg-secondary-dark rounded-lg border border-transparent
data-[wake-lock-active=true]:border-primary-main
"
onClick={() => {
wakeLock.toggleWakeLock();
}}
role="checkbox" role="checkbox"
aria-checked={wakeLock.active} aria-checked={settings.keepAwake}
aria-label="Keep awake" aria-label="Keep awake"
> >
Keep Awake Keep Awake
</Button> </button>
<Button <button
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
userSelect: 'none', userSelect: 'none',
fontSize: buttonFontSize, fontSize: buttonFontSize,
padding: '4px', padding: '2px',
}} }}
onClick={() => dialogRef.current?.show()} className="text-primary-main"
onClick={() => resetGameDialogRef.current?.show()}
role="checkbox" role="checkbox"
aria-checked={wakeLock.active}
aria-label="Reset Game" aria-label="Reset Game"
> >
<ResetGame size={iconSize} /> <ResetGame size={iconSize} />
</Button> </button>
</ButtonsSections> </ButtonsSections>
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={dialogRef} ref={resetGameDialogRef}
className="z-[9999] bg-background-default text-text-primary rounded-2xl border-none absolute top-[10%]" className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => resetGameDialogRef.current?.close()}
> >
<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-xl border-none">
<Button <h1
variant="contained" className="text-center text-text-primary"
onClick={() => dialogRef.current?.close()} style={{ fontSize: extraCountersSize }}
> >
No Reset Game?
</Button> </h1>
<Button <div className="flex justify-evenly gap-2">
variant="contained" <button
onClick={() => { className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
handleResetGame(); style={{ fontSize: iconSize }}
dialogRef.current?.close(); onClick={() => resetGameDialogRef.current?.close()}
}} >
> No
Yes </button>
</Button> <button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => {
handleResetGame();
resetGameDialogRef.current?.close();
}}
style={{ fontSize: iconSize }}
>
Yes
</button>
</div>
</div>
</div>
</dialog>
<dialog
ref={endGameDialogRef}
className="z-[999] size-full bg-background-settings overflow-y-scroll"
onClick={() => endGameDialogRef.current?.close()}
>
<div className="flex size-full items-center justify-center">
<div className="flex flex-col justify-center p-4 gap-2 bg-background-default rounded-xl border-none">
<h1
className="text-center text-text-primary"
style={{ fontSize: extraCountersSize }}
>
End Game?
</h1>
<div className="flex justify-evenly gap-2">
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
style={{ fontSize: iconSize }}
onClick={() => endGameDialogRef.current?.close()}
>
No
</button>
<button
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
onClick={() => {
handleGoToStart();
endGameDialogRef.current?.close();
}}
style={{ fontSize: iconSize }}
>
Yes
</button>
</div>
</div>
</div> </div>
</dialog> </dialog>
</SettingsContainer> </SettingsContainer>

View File

@@ -1,6 +1,8 @@
import LifeCounter from '../LifeCounter/LifeCounter';
import { Player as PlayerType } from '../../Types/Player';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player as PlayerType } from '../../Types/Player';
import LifeCounter from '../LifeCounter/LifeCounter';
import { GridLayout } from '../Views/Play';
const getGridArea = (player: PlayerType) => { const getGridArea = (player: PlayerType) => {
switch (player.index) { switch (player.index) {
@@ -21,15 +23,16 @@ const getGridArea = (player: PlayerType) => {
} }
}; };
const PlayerWrapper = twc.div`w-full h-full bg-black`; const PlayersWrapper = twc.div`w-full h-full bg-black`;
export const Players = ({ gridLayout }: { gridLayout: GridLayout }) => {
const { players } = usePlayers();
export const Player = (players: PlayerType[], gridClasses: string) => {
return ( return (
<PlayerWrapper> <PlayersWrapper>
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}> <div className={`grid w-full h-full gap-1 box-border ${gridLayout} `}>
{players.map((player) => { {players.map((player) => {
const gridArea = getGridArea(player); const gridArea = getGridArea(player);
return ( return (
<div <div
key={player.index} key={player.index}
@@ -45,6 +48,6 @@ export const Player = (players: PlayerType[], gridClasses: string) => {
); );
})} })}
</div> </div>
</PlayerWrapper> </PlayersWrapper>
); );
}; };

View File

@@ -0,0 +1,198 @@
import { useEffect, useRef, useState } from 'react';
import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../Hooks/usePlayers';
type TouchPoint = {
x: number;
y: number;
id: number;
};
const getOrientation = () => {
return window.matchMedia('(orientation: portrait)').matches
? 'portrait'
: 'landscape';
};
export const FingerGame = () => {
const { players } = usePlayers();
const aboutToStartTimerRef = useRef<NodeJS.Timeout | null>(null);
const selectingPlayerTimerRef = useRef<NodeJS.Timeout | null>(null);
const [touchPoints, setTouchPoints] = useState<TouchPoint[]>([]);
const [selectedTouchPoint, setSelectedTouchPoint] = useState<
TouchPoint | undefined
>();
const [timerStarted, setTimerStarted] = useState(false);
const { setPlaying, goToStart } = useGlobalSettings();
useEffect(() => {
//Start playing when someone is selected and any touch point is released
if (selectedTouchPoint && touchPoints.length !== players.length) {
aboutToStartTimerRef.current = setTimeout(() => {
setSelectedTouchPoint(undefined);
setPlaying(true);
}, 500);
setTimerStarted(true);
return;
}
// If no touch point is selected, select one with a delay
if (touchPoints.length === players.length && !selectedTouchPoint) {
selectingPlayerTimerRef.current = setTimeout(() => {
const randomIndex = Math.floor(Math.random() * touchPoints.length);
const randomTouchPoint = touchPoints[randomIndex];
setSelectedTouchPoint(randomTouchPoint);
}, 250);
return;
}
if (selectingPlayerTimerRef.current) {
clearTimeout(selectingPlayerTimerRef.current);
}
return () => {
if (aboutToStartTimerRef.current) {
clearTimeout(aboutToStartTimerRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [touchPoints, players.length]);
const handleOnTouchStart = (e: React.TouchEvent) => {
if (selectedTouchPoint) {
return;
}
//Get the first touch point id that isn't already in the touchPoints array
const touch = Array.from(e.changedTouches).find(
(t) => !touchPoints.find((p) => p.id === t.identifier)
);
if (!touch) {
console.error('No touch found');
return;
}
let { clientX, clientY } = touch;
// Adjust coordinates for portrait mode
if (getOrientation() === 'portrait') {
const tempX = clientX;
clientX = clientY;
clientY = window.innerWidth - tempX;
}
const newTouchPoints = {
x: clientX,
y: clientY,
id: touch.identifier,
};
setTouchPoints([...touchPoints, newTouchPoints]);
};
const handleOnTouchEnd = (e: React.TouchEvent) => {
if (selectedTouchPoint) {
aboutToStartTimerRef.current = setTimeout(() => {
setSelectedTouchPoint(undefined);
setPlaying(true);
}, 500);
setTimerStarted(true);
return;
}
// Get the touch point that was just released
const touch = e.changedTouches[e.changedTouches.length - 1];
// Get the index of the touch point that was just released
const index = touchPoints.findIndex((p) => p.id === touch.identifier);
// Remove the touch point that was just released
setTouchPoints([
...touchPoints.slice(0, index),
...touchPoints.slice(index + 1),
]);
};
return (
<div
className="absolute flex justify-center items-center w-full h-full portrait:h-[100dvw] portrait:w-[100dvh] z-50 bg-secondary-main overflow-hidden"
onTouchStart={handleOnTouchStart}
onTouchEnd={handleOnTouchEnd}
// FIXEME: This code is not performant, but updates a touch point's position when it moves
// onTouchMove={(e) => {
// e.preventDefault();
// // Get the touch point that was just moved
// const touch = Array.from(e.changedTouches).find((t) =>
// touchPoints.find((p) => p.id === t.identifier)
// );
// if (!touch) {
// console.error('No touch found');
// return;
// }
// let { clientX, clientY } = touch;
// // Adjust coordinates for portrait mode
// if (getOrientation() === 'portrait') {
// const tempX = clientX;
// clientX = clientY;
// clientY = window.innerWidth - tempX;
// }
// // Get the index of the touch point that was just moved
// const index = touchPoints.findIndex(
// (p) => p.id === touch.identifier
// );
// // Update the touch point that was just moved
// setTouchPoints([
// ...touchPoints.slice(0, index),
// { x: clientX, y: clientY, id: touch.identifier },
// ...touchPoints.slice(index + 1),
// ]);
// }}
>
<button
className="absolute flex top-4 left-4 rounded-lg px-2 py-1 justify-center bg-primary-main text-text-primary text-xs"
onClick={goToStart}
>
<div className="text-xl leading-4">{'<'}&nbsp;</div>
Back
</button>
{touchPoints.length !== players.length && (
<div className="flex flex-col items-center text-[13vmin] whitespace-nowrap pointer-events-none webkit-user-select-none">
Waiting for fingers <br />
<div className="tabular-nums">
{touchPoints.length}/{players.length}
</div>
</div>
)}
{touchPoints.map((point, index) => (
<div
key={`touch-point-${index}`}
data-is-selected={selectedTouchPoint?.id === point.id}
data-unloading={timerStarted}
className="absolute rounded-full translate-x-[-50%] translate-y-[-50%] transition-all duration-1000
h-[75px] w-[75px]
data-[unloading=false]:data-[is-selected=true]:h-[250px] data-[unloading=false]:data-[is-selected=true]:w-[250px]
data-[unloading=true]:h-[0px] data-[unloading=true]:w-[0px] data-[unloading=true]:duration-[400ms]
pointer-events-none
"
style={{
left: point.x,
top: point.y,
backgroundColor: players[index]?.color ?? 'red',
}}
/>
))}
</div>
);
};

View File

@@ -0,0 +1,49 @@
import { usePlayers } from '../../../../Hooks/usePlayers';
import { Player } from '../../../../Types/Player';
import { GridLayout } from '../../../Views/Play';
import { RoulettePlayerCard } from './RoulettePlayerCard';
const getGridArea = (player: Player) => {
switch (player.index) {
case 0:
return 'grid-in-player0';
case 1:
return 'grid-in-player1';
case 2:
return 'grid-in-player2';
case 3:
return 'grid-in-player3';
case 4:
return 'grid-in-player4';
case 5:
return 'grid-in-player5';
default:
throw new Error('Invalid player index');
}
};
export const RandomKingPlayers = ({
gridLayout,
}: {
gridLayout: GridLayout;
}) => {
const { players } = usePlayers();
return (
<div className="w-full h-full bg-black">
<div className={`grid w-full h-full gap-1 box-border ${gridLayout} `}>
{players.map((player) => {
const gridArea = getGridArea(player);
return (
<div
key={player.index}
className={`flex justify-center items-center align-middle ${gridArea}`}
>
<RoulettePlayerCard player={player} />
</div>
);
})}
</div>
</div>
);
};

View File

@@ -0,0 +1,94 @@
import { useEffect, useRef } from 'react';
import { useGlobalSettings } from '../../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../../Hooks/usePlayers';
export const RandomKingSelectWrapper = () => {
const { setRandomizingPlayer } = useGlobalSettings();
const randomIntervalRef = useRef<NodeJS.Timeout | null>(null);
const prevRandomIndexRef = useRef<number>(-1);
const { settings, randomizingPlayer, setPreStartCompleted } =
useGlobalSettings();
const { players, setPlayers } = usePlayers();
useEffect(() => {
if (
players.length > 1 &&
settings.showStartingPlayer &&
randomizingPlayer
) {
randomIntervalRef.current = setInterval(() => {
let randomIndex: number;
do {
randomIndex = Math.floor(Math.random() * players.length);
} while (randomIndex === prevRandomIndexRef.current);
prevRandomIndexRef.current = randomIndex;
setPlayers(
players.map((p) =>
p.index === prevRandomIndexRef.current
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
}, 100);
}
const randomPlayerIndex = Math.floor(Math.random() * players.length);
setPlayers(
players.map((p) =>
p.index === randomPlayerIndex
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
return () => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [players.length, setPlayers, randomizingPlayer]);
const gradientColors = players.map((player) => player.color).join(', ');
return (
<div
className="absolute flex justify-center items-center h-screen w-screen portrait:h-[100vw] portrait:w-[100vh] z-40 cursor-pointer text-5xl"
onClick={() => {
if (randomIntervalRef.current) {
clearInterval(randomIntervalRef.current);
randomIntervalRef.current = null;
}
setRandomizingPlayer(false);
setPreStartCompleted(true);
}}
>
<div className="absolute flex top-[30%] justify-center items-center px-8 py-4">
<div
className="absolute size-full blur-[3px] rounded-2xl opacity-90 saturate-150"
style={{
backgroundImage: `linear-gradient(60deg, ${gradientColors})`,
}}
/>
<p className="relative z-10 text-[5vmax]">PRESS TO SELECT PLAYER</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,58 @@
import { useEffect, useRef } from 'react';
import { useGlobalSettings } from '../../../../Hooks/useGlobalSettings';
import { Player, Rotation } from '../../../../Types/Player';
import { Paragraph } from '../../../Misc/TextComponents';
import { DynamicText } from '../../StartingPlayerCard';
export const RoulettePlayerCard = ({ player }: { player: Player }) => {
const startPlayingTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const { settings, randomizingPlayer, playing, setPlaying } =
useGlobalSettings();
useEffect(() => {
if (
player.isStartingPlayer &&
((!playing && randomizingPlayer) || !playing)
) {
startPlayingTimerRef.current = setTimeout(() => {
setPlaying(true);
}, 10_000);
}
return () => clearTimeout(startPlayingTimerRef.current);
}, [
player.isStartingPlayer,
playing,
setPlaying,
settings.preStartMode,
randomizingPlayer,
]);
const calcTextRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
return (
<div className="relative flex flex-grow flex-col items-center w-full h-full overflow-hidden">
<div
className="flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none z-10"
style={{ backgroundColor: player.color }}
>
{player.isStartingPlayer && (
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
>
<div className="flex flex-col justify-center items-center">
<Paragraph>👑</Paragraph>
</div>
</DynamicText>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,30 @@
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { PreStartMode } from '../../Types/Settings';
import { GridLayout } from '../Views/Play';
import { FingerGame } from './Games/FingerGame';
import { RandomKingPlayers } from './Games/RandomKing/RandomKingPlayers';
import { RandomKingSelectWrapper } from './Games/RandomKing/RandomKingSelectWrapper';
export const PreStart = ({ gridLayout }: { gridLayout: GridLayout }) => {
const { settings, randomizingPlayer, goToStart } = useGlobalSettings();
if (settings.preStartMode === PreStartMode.RandomKing) {
if (!randomizingPlayer) {
return null;
}
return (
<>
<RandomKingSelectWrapper />
<RandomKingPlayers gridLayout={gridLayout} />
</>
);
}
if (settings.preStartMode === PreStartMode.FingerGame) {
return <FingerGame />;
}
goToStart();
return null;
};

View File

@@ -0,0 +1,60 @@
import { twc } from 'react-twc';
import { baseColors } from '../../../tailwind.config';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { Player, Rotation } from '../../Types/Player';
import { PreStartMode } from '../../Types/Settings';
import { Paragraph } from '../Misc/TextComponents';
export const DynamicText = twc.div`text-[8vmin] whitespace-nowrap`;
export const StartingPlayerCard = ({ player }: { player: Player }) => {
const { settings, setPlaying, randomizingPlayer } = useGlobalSettings();
const calcTextRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
const calcRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 90
: player.settings.rotation;
return (
<div
className="z-20 flex absolute w-full h-full justify-center items-center select-none cursor-pointer webkit-user-select-none"
style={{
rotate: `${calcRotation}deg`,
backgroundImage:
!randomizingPlayer ||
(settings.preStartMode !== PreStartMode.None &&
settings.preStartMode !== PreStartMode.FingerGame)
? `radial-gradient(circle at center, ${player.color}, ${baseColors.primary.main})`
: 'none',
}}
onClick={() => {
setPlaying(true);
}}
>
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
>
<div className="flex flex-col justify-center items-center">
<Paragraph>👑</Paragraph>
{(!randomizingPlayer ||
(settings.preStartMode !== PreStartMode.None &&
settings.preStartMode !== PreStartMode.FingerGame)) && (
<>
<Paragraph>You start!</Paragraph>
<Paragraph className="text-xl">(Press to hide)</Paragraph>
</>
)}
</div>
</DynamicText>
</div>
);
};

View File

@@ -1,67 +1,115 @@
import { useEffect } from 'react';
import { twc } from 'react-twc';
import { twGridTemplateAreas } from '../../../tailwind.config';
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, PreStartMode } from '../../Types/Settings';
import { Player } from '../Player/Player'; import { Players } from '../Players/Players';
import { twc } from 'react-twc'; import { PreStart } from '../PreStartGame/PreStart';
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`; const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden, setPlayers`;
type GridTemplateAreasKeys = keyof typeof twGridTemplateAreas;
export type GridLayout = `grid-areas-${GridTemplateAreasKeys}`;
export const Play = () => { export const Play = () => {
const { players } = usePlayers(); const { players, setPlayers } = usePlayers();
const { initialGameSettings } = useGlobalSettings(); const { initialGameSettings, playing, settings, preStartCompleted } =
useGlobalSettings();
let Layout: JSX.Element; let gridLayout: GridLayout;
switch (players.length) { switch (players.length) {
case 1: case 1:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-onePlayerPortrait'); gridLayout = 'grid-areas-onePlayerPortrait';
} }
Layout = Player(players, 'grid-areas-onePlayerLandscape'); gridLayout = 'grid-areas-onePlayerLandscape';
break; break;
case 2: case 2:
switch (initialGameSettings?.orientation) { switch (initialGameSettings?.orientation) {
case Orientation.Portrait: case Orientation.Portrait:
Layout = Player(players, 'grid-areas-twoPlayersOppositePortrait'); gridLayout = 'grid-areas-twoPlayersOppositePortrait';
break; break;
default: default:
case Orientation.Landscape: case Orientation.Landscape:
Layout = Player(players, 'grid-areas-twoPlayersSameSideLandscape'); gridLayout = 'grid-areas-twoPlayersSameSideLandscape';
break; break;
case Orientation.OppositeLandscape: case Orientation.OppositeLandscape:
Layout = Player(players, 'grid-areas-twoPlayersOppositeLandscape'); gridLayout = 'grid-areas-twoPlayersOppositeLandscape';
break; break;
} }
break; break;
case 3: case 3:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-threePlayersSide'); gridLayout = 'grid-areas-threePlayersSide';
break; break;
} }
Layout = Player(players, 'grid-areas-threePlayers'); gridLayout = 'grid-areas-threePlayers';
break; break;
default: default:
case 4: case 4:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-fourPlayerPortrait'); gridLayout = 'grid-areas-fourPlayerPortrait';
break; break;
} }
Layout = Player(players, 'grid-areas-fourPlayer'); gridLayout = 'grid-areas-fourPlayer';
break; break;
case 5: case 5:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-fivePlayersSide'); gridLayout = 'grid-areas-fivePlayersSide';
break; break;
} }
Layout = Player(players, 'grid-areas-fivePlayers'); gridLayout = 'grid-areas-fivePlayers';
break; break;
case 6: case 6:
if (initialGameSettings?.orientation === Orientation.Portrait) { if (initialGameSettings?.orientation === Orientation.Portrait) {
Layout = Player(players, 'grid-areas-sixPlayersSide'); gridLayout = 'grid-areas-sixPlayersSide';
break; break;
} }
Layout = Player(players, 'grid-areas-sixPlayers'); gridLayout = 'grid-areas-sixPlayers';
break; break;
} }
return <MainWrapper>{Layout}</MainWrapper>; useEffect(() => {
if (settings.preStartMode !== PreStartMode.None) {
return;
}
const randomIndex = Math.floor(Math.random() * players.length);
setPlayers(
players.map((p) =>
p.index === randomIndex
? {
...p,
isStartingPlayer: true,
}
: {
...p,
isStartingPlayer: false,
}
)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (
!preStartCompleted &&
settings.preStartMode !== PreStartMode.None &&
!playing &&
settings.showStartingPlayer
) {
return (
<MainWrapper>
<PreStart gridLayout={gridLayout} />
</MainWrapper>
);
}
return (
<MainWrapper>
<Players gridLayout={gridLayout} />
</MainWrapper>
);
}; };

View File

@@ -3,10 +3,12 @@ import React from 'react';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { import {
FivePlayers, FivePlayers,
FivePlayersSide,
FourPlayers, FourPlayers,
FourPlayersSide, FourPlayersSide,
OnePlayerPortrait, OnePlayerPortrait,
SixPlayers, SixPlayers,
SixPlayersSide,
ThreePlayers, ThreePlayers,
ThreePlayersSide, ThreePlayersSide,
TwoPlayersOppositeLandscape, TwoPlayersOppositeLandscape,
@@ -18,7 +20,7 @@ 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 = twc.div`flex flex-row justify-between self-center`; const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
type LayoutOptionsProps = { type LayoutOptionsProps = {
numberOfPlayers: number; numberOfPlayers: number;
@@ -31,8 +33,10 @@ 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) {
@@ -58,6 +62,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
@@ -81,6 +86,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
@@ -94,6 +100,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}
@@ -117,6 +124,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}
@@ -140,6 +148,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}
@@ -168,6 +177,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}
@@ -191,6 +201,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}
@@ -220,6 +231,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}
@@ -243,6 +255,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}
@@ -272,6 +285,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}
@@ -291,20 +305,21 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
} }
label="" label=""
/> />
{/* <FormControlLabel <FormControlLabel
value={GridTemplateAreas.FivePlayersSide} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayersSide <FivePlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.secondary.main} fill={theme.palette.secondary.main}
/> />
} }
checkedIcon={ checkedIcon={
<FivePlayersSide <FivePlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.primary.main} fill={theme.palette.primary.main}
/> />
@@ -313,7 +328,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
label="" label=""
/> */} />
</> </>
); );
@@ -324,6 +339,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}
@@ -343,20 +359,21 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
} }
label="" label=""
/> />
{/* <FormControlLabel <FormControlLabel
value={GridTemplateAreas.SixPlayersSide} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayersSide <SixPlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.secondary.main} fill={theme.palette.secondary.main}
/> />
} }
checkedIcon={ checkedIcon={
<SixPlayersSide <SixPlayersSide
height={iconHeight} height={iconHeight}
width={iconWidth} width={iconWidth}
fill={theme.palette.primary.main} fill={theme.palette.primary.main}
/> />
@@ -365,7 +382,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
label="" label=""
/> */} />
</> </>
); );

View File

@@ -9,9 +9,10 @@ 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 { import {
GameFormat,
InitialGameSettings, InitialGameSettings,
Orientation, Orientation,
PreStartMode,
defaultInitialGameSettings,
} from '../../../Types/Settings'; } from '../../../Types/Settings';
import { InfoModal } from '../../Misc/InfoModal'; import { InfoModal } from '../../Misc/InfoModal';
import { SettingsModal } from '../../Misc/SettingsModal'; import { SettingsModal } from '../../Misc/SettingsModal';
@@ -20,7 +21,9 @@ import { LayoutOptions } from './LayoutOptions';
const MainWrapper = twc.div`w-[100dvw] h-fit pb-14 overflow-hidden items-center flex flex-col`; const MainWrapper = twc.div`w-[100dvw] h-fit pb-14 overflow-hidden items-center flex flex-col`;
const StartButtonFooter = twc.div`fixed bottom-4 z-1`; const StartButtonFooter = twc.div`w-full max-w-[548px] fixed bottom-4 z-1 items-center flex flex-col px-4 z-10`;
const SliderWrapper = twc.div`mx-8`;
const ToggleButtonsWrapper = twc.div`flex flex-row justify-between items-center`; const ToggleButtonsWrapper = twc.div`flex flex-row justify-between items-center`;
@@ -87,19 +90,14 @@ const Start = () => {
setInitialGameSettings, setInitialGameSettings,
settings, settings,
isPWA, isPWA,
setRandomizingPlayer,
} = useGlobalSettings(); } = useGlobalSettings();
const [openInfoModal, setOpenInfoModal] = useState(false); const [openInfoModal, setOpenInfoModal] = useState(false);
const [openSettingsModal, setOpenSettingsModal] = useState(false); const [openSettingsModal, setOpenSettingsModal] = useState(false);
const [playerOptions, setPlayerOptions] = useState<InitialGameSettings>( const [playerOptions, setPlayerOptions] = useState<InitialGameSettings>(
initialGameSettings || { initialGameSettings || defaultInitialGameSettings
numberOfPlayers: 4,
startingLifeTotal: 40,
useCommanderDamage: true,
orientation: Orientation.Portrait,
gameFormat: GameFormat.Commander,
}
); );
const doStartGame = () => { const doStartGame = () => {
@@ -124,6 +122,7 @@ const Start = () => {
setInitialGameSettings(initialGameSettings); setInitialGameSettings(initialGameSettings);
setPlayers(createInitialPlayers(initialGameSettings)); setPlayers(createInitialPlayers(initialGameSettings));
setShowPlay(true); setShowPlay(true);
setRandomizingPlayer(settings.preStartMode === PreStartMode.RandomKing);
localStorage.setItem('playing', 'false'); localStorage.setItem('playing', 'false');
localStorage.setItem('showPlay', 'true'); localStorage.setItem('showPlay', 'true');
}; };
@@ -174,124 +173,118 @@ const Start = () => {
Life Trinket Life Trinket
</h1> </h1>
<FormControl focused={false} style={{ width: '80vw' }}> <div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
<FormLabel>Number of Players</FormLabel> <FormControl focused={false} style={{ width: '100%' }}>
<Slider <FormLabel>Number of Players</FormLabel>
title="Number of Players" <SliderWrapper>
max={6} <Slider
min={1} title="Number of Players"
aria-label="Custom marks" max={6}
value={playerOptions?.numberOfPlayers ?? 4} min={1}
getAriaValueText={valuetext} aria-label="Custom marks"
step={null} value={playerOptions?.numberOfPlayers ?? 4}
marks={playerMarks} getAriaValueText={valuetext}
onChange={(_e, value) => { step={null}
setPlayerOptions({ marks={playerMarks}
...playerOptions,
numberOfPlayers: value as number,
orientation: Orientation.Landscape,
});
}}
/>
<FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
<Slider
title="Starting Health"
max={60}
min={20}
aria-label="Custom marks"
value={playerOptions?.startingLifeTotal ?? 40}
getAriaValueText={valuetext}
step={10}
marks={healthMarks}
onChange={(_e, value) =>
setPlayerOptions({
...playerOptions,
startingLifeTotal: value as number,
orientation: Orientation.Landscape,
})
}
/>
<ToggleButtonsWrapper className="mt-4">
<ToggleContainer>
<FormLabel>Commander</FormLabel>
<Switch
checked={
playerOptions.useCommanderDamage ??
initialGameSettings?.useCommanderDamage ??
true
}
onChange={(_e, value) => { onChange={(_e, value) => {
if (value) {
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
numberOfPlayers: 4,
startingLifeTotal: 40,
orientation: Orientation.Landscape,
});
return;
}
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
useCommanderDamage: value, numberOfPlayers: value as number,
numberOfPlayers: 2,
startingLifeTotal: 20,
orientation: Orientation.Landscape, orientation: Orientation.Landscape,
}); });
}} }}
/> />
</ToggleContainer> </SliderWrapper>
<Button
variant="contained" <FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
style={{ height: '2rem' }} <SliderWrapper>
onClick={() => { <Slider
setOpenSettingsModal(true); title="Starting Health"
max={60}
min={20}
aria-label="Custom marks"
value={playerOptions?.startingLifeTotal ?? 40}
getAriaValueText={valuetext}
step={10}
marks={healthMarks}
onChange={(_e, value) =>
setPlayerOptions({
...playerOptions,
startingLifeTotal: value as number,
orientation: Orientation.Landscape,
})
}
/>
</SliderWrapper>
<ToggleButtonsWrapper className="mt-4">
<ToggleContainer>
<FormLabel>Commander</FormLabel>
<Switch
checked={
playerOptions.useCommanderDamage ??
initialGameSettings?.useCommanderDamage ??
true
}
onChange={(_e, value) => {
if (value) {
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
numberOfPlayers: 4,
startingLifeTotal: 40,
orientation: Orientation.Landscape,
});
return;
}
setPlayerOptions({
...playerOptions,
useCommanderDamage: value,
numberOfPlayers: 2,
startingLifeTotal: 20,
orientation: Orientation.Landscape,
});
}}
/>
</ToggleContainer>
<Button
variant="contained"
style={{ height: '2rem' }}
onClick={() => {
setOpenSettingsModal(true);
}}
>
<Cog /> &nbsp; Other settings
</Button>
</ToggleButtonsWrapper>
<FormLabel>Layout</FormLabel>
<LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation}
onChange={(orientation) => {
setPlayerOptions({
...playerOptions,
orientation,
});
}} }}
> />
<Cog /> &nbsp; Other settings </FormControl>
</Button> {!isPWA && (
</ToggleButtonsWrapper> <p className="text-center text-xs text-text-primary w-11/12 mt-4">
If you're on iOS, this page works better if you{' '}
<FormLabel>Layout</FormLabel> <strong>hide the toolbar</strong> or{' '}
{/* <LayoutOptions <strong>add the app to your home screen</strong>.
numberOfPlayers={playerOptions.numberOfPlayers} </p>
gridAreas={playerOptions.gridAreas} )}
onChange={(gridAreas) => </div>
setPlayerOptions({
...playerOptions,
gridAreas,
//TODO fix the layout selection
orientation: Orientation.Portrait,
})
}
/> */}
<LayoutOptions
numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation}
onChange={(orientation) => {
setPlayerOptions({
...playerOptions,
orientation,
});
}}
/>
</FormControl>
{!isPWA && (
<p className="text-center, max-w-[75%] text-xs text-text-primary">
If you're on iOS, this page works better if you{' '}
<strong>hide the toolbar</strong> or{' '}
<strong>add the app to your home screen</strong>.
</p>
)}
<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

@@ -22,7 +22,13 @@ export type GlobalSettingsContextType = {
setInitialGameSettings: (initialGameSettings: InitialGameSettings) => void; setInitialGameSettings: (initialGameSettings: InitialGameSettings) => void;
settings: Settings; settings: Settings;
setSettings: (settings: Settings) => void; setSettings: (settings: Settings) => void;
playing: boolean;
setPlaying: (playing: boolean) => void;
randomizingPlayer: boolean;
setRandomizingPlayer: (stopRandom: boolean) => void;
isPWA: boolean; isPWA: boolean;
preStartCompleted: boolean;
setPreStartCompleted: (completed: boolean) => void;
}; };
export const GlobalSettingsContext = export const GlobalSettingsContext =

View File

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

View File

@@ -1,7 +1,7 @@
import { Player, Rotation } from '../Types/Player'; import { Player, Rotation } from '../Types/Player';
import { InitialGameSettings, Orientation } from '../Types/Settings'; import { InitialGameSettings, Orientation } from '../Types/Settings';
const presetColors = [ export const presetColors = [
'#F06292', // Light Pink '#F06292', // Light Pink
'#4DB6AC', // Teal '#4DB6AC', // Teal
'#FFA726', // Orange '#FFA726', // Orange
@@ -127,15 +127,15 @@ const getOrientationRotations = (
case Orientation.Portrait: case Orientation.Portrait:
switch (index) { switch (index) {
case 0: case 0:
return Rotation.Side; return Rotation.Flipped;
case 1: case 1:
return Rotation.Side; return Rotation.Flipped;
case 2: case 2:
return Rotation.SideFlipped; return Rotation.Side;
case 3: case 3:
return Rotation.SideFlipped; return Rotation.Normal;
case 4: case 4:
return Rotation.SideFlipped; return Rotation.Normal;
default: default:
return Rotation.Normal; return Rotation.Normal;
} }
@@ -163,17 +163,17 @@ const getOrientationRotations = (
case Orientation.Portrait: case Orientation.Portrait:
switch (index) { switch (index) {
case 0: case 0:
return Rotation.Side; return Rotation.SideFlipped;
case 1: case 1:
return Rotation.Side; return Rotation.Flipped;
case 2: case 2:
return Rotation.Side; return Rotation.Flipped;
case 3: case 3:
return Rotation.SideFlipped; return Rotation.Side;
case 4: case 4:
return Rotation.SideFlipped; return Rotation.Normal;
case 5: case 5:
return Rotation.SideFlipped; return Rotation.Normal;
default: default:
return Rotation.Normal; return Rotation.Normal;
} }
@@ -191,10 +191,8 @@ export const createInitialPlayers = ({
}: InitialGameSettings): Player[] => { }: InitialGameSettings): Player[] => {
const players: Player[] = []; const players: Player[] = [];
const availableColors = [...presetColors]; // Create a copy of the colors array const availableColors = [...presetColors]; // Create a copy of the colors array
const firstPlayerIndex = Math.floor(Math.random() * numberOfPlayers);
for (let i = 0; i <= numberOfPlayers - 1; i++) { for (let i = 0; i <= numberOfPlayers - 1; i++) {
const isStartingPlayer = i === firstPlayerIndex;
const colorIndex = Math.floor(Math.random() * availableColors.length); const colorIndex = Math.floor(Math.random() * availableColors.length);
const color = availableColors[colorIndex]; const color = availableColors[colorIndex];
@@ -224,11 +222,11 @@ export const createInitialPlayers = ({
usePoison: false, usePoison: false,
rotation, rotation,
}, },
isStartingPlayer,
showStartingPlayer: isStartingPlayer,
extraCounters: [], extraCounters: [],
commanderDamage, commanderDamage,
hasLost: false, hasLost: false,
isStartingPlayer: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
}; };
players.push(player); players.push(player);

View File

@@ -18,6 +18,11 @@ export const useAnalytics = () => {
eventName: string, eventName: string,
eventParams?: { [key: string]: unknown } eventParams?: { [key: string]: unknown }
) => { ) => {
if (process.env.NODE_ENV === 'development') {
console.info('Event not tracked:', { eventName, eventParams });
return;
}
logEvent(analytics, eventName, eventParams); logEvent(analytics, eventName, eventParams);
}; };

View File

@@ -0,0 +1,52 @@
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;
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

@@ -7,8 +7,11 @@ import {
import { useAnalytics } from '../Hooks/useAnalytics'; import { useAnalytics } from '../Hooks/useAnalytics';
import { import {
InitialGameSettings, InitialGameSettings,
InitialGameSettingsSchema,
Settings, Settings,
defaultInitialGameSettings,
defaultSettings,
initialGameSettingsSchema,
settingsSchema,
} from '../Types/Settings'; } from '../Types/Settings';
export const GlobalSettingsProvider = ({ export const GlobalSettingsProvider = ({
@@ -21,57 +24,109 @@ export const GlobalSettingsProvider = ({
const savedShowPlay = localStorage.getItem('showPlay'); const savedShowPlay = localStorage.getItem('showPlay');
const savedGameSettings = localStorage.getItem('initialGameSettings'); const savedGameSettings = localStorage.getItem('initialGameSettings');
const savedSettings = localStorage.getItem('settings'); const savedSettings = localStorage.getItem('settings');
const savedPlaying = localStorage.getItem('playing');
const savedPreStartComplete = localStorage.getItem('preStartComplete');
const [playing, setPlaying] = useState<boolean>(
savedPlaying ? savedPlaying === 'true' : false
);
const setPlayingAndLocalStorage = (playing: boolean) => {
setPlaying(playing);
localStorage.setItem('playing', String(playing));
};
const [preStartCompleted, setPreStartCompleted] = useState<boolean>(
savedPreStartComplete ? savedPreStartComplete === 'true' : false
);
const [showPlay, setShowPlay] = useState<boolean>( const [showPlay, setShowPlay] = useState<boolean>(
savedShowPlay ? savedShowPlay === 'true' : false savedShowPlay ? savedShowPlay === 'true' : false
); );
const [randomizingPlayer, setRandomizingPlayer] = useState<boolean>(
savedSettings
? Boolean(JSON.parse(savedSettings).preStartMode === 'random-king')
: true
);
const [initialGameSettings, setInitialGameSettings] = const [initialGameSettings, setInitialGameSettings] =
useState<InitialGameSettings | null>( useState<InitialGameSettings | null>(
savedGameSettings ? JSON.parse(savedGameSettings) : null savedGameSettings ? JSON.parse(savedGameSettings) : null
); );
const setInitialGameSettingsAndLocalStorage = (
initialGameSettings: InitialGameSettings
) => {
setInitialGameSettings(initialGameSettings);
localStorage.setItem(
'initialGameSettings',
JSON.stringify(initialGameSettings)
);
};
const [settings, setSettings] = useState<Settings>( const [settings, setSettings] = useState<Settings>(
savedSettings savedSettings ? JSON.parse(savedSettings) : defaultSettings
? JSON.parse(savedSettings)
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true }
); );
const setSettingsAndLocalStorage = (settings: Settings) => {
setSettings(settings);
localStorage.setItem('settings', JSON.stringify(settings));
};
const removeLocalStorage = async () => { const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings'); localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players'); localStorage.removeItem('players');
localStorage.removeItem('playing'); localStorage.removeItem('playing');
localStorage.removeItem('showPlay'); localStorage.removeItem('showPlay');
localStorage.removeItem('preStartComplete');
setPlaying(false);
setShowPlay(false); setShowPlay(false);
setPreStartCompleted(false);
}; };
// Set settings if they are not valid
useEffect(() => { useEffect(() => {
if (savedGameSettings && JSON.parse(savedGameSettings).gridAreas) { // If there are no saved settings, set default settings
removeLocalStorage(); if (!savedSettings) {
window.location.reload(); setSettingsAndLocalStorage(defaultSettings);
return;
}
const parsedSettings = settingsSchema.safeParse(JSON.parse(savedSettings));
// If saved settings are not valid, remove them
if (!parsedSettings.success) {
console.error('invalid settings, resetting to default settings');
setSettingsAndLocalStorage(defaultSettings);
return;
}
localStorage.setItem('settings', JSON.stringify(parsedSettings.data));
}, [settings, savedSettings]);
// Set initial game settings if they are not valid
useEffect(() => {
if (!savedGameSettings) {
setInitialGameSettingsAndLocalStorage(defaultInitialGameSettings);
return; return;
} }
//parse existing game settings with zod schema //parse existing game settings with zod schema
const parsedInitialGameSettings = const parsedInitialGameSettings =
InitialGameSettingsSchema.safeParse(initialGameSettings); initialGameSettingsSchema.safeParse(initialGameSettings);
if (!parsedInitialGameSettings.success) { if (!parsedInitialGameSettings.success) {
removeLocalStorage(); console.error('invalid game settings, resetting to default settings');
window.location.reload(); setInitialGameSettingsAndLocalStorage(defaultInitialGameSettings);
return; return;
} }
localStorage.setItem( localStorage.setItem(
'initialGameSettings', 'initialGameSettings',
JSON.stringify(initialGameSettings) JSON.stringify(parsedInitialGameSettings.data)
); );
}, [initialGameSettings, savedGameSettings]); }, [initialGameSettings, savedGameSettings]);
useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings));
}, [settings]);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => { useEffect(() => {
@@ -135,6 +190,11 @@ export const GlobalSettingsProvider = ({
} }
}; };
const setPreStartCompletedAndLocalStorage = (preStartComplete: boolean) => {
setPreStartCompleted(preStartComplete);
localStorage.setItem('playing', String(playing));
};
return { return {
fullscreen: { isFullscreen, enableFullscreen, disableFullscreen }, fullscreen: { isFullscreen, enableFullscreen, disableFullscreen },
wakeLock: { wakeLock: {
@@ -148,23 +208,32 @@ export const GlobalSettingsProvider = ({
goToStart, goToStart,
showPlay, showPlay,
setShowPlay, setShowPlay,
playing,
setPlaying: setPlayingAndLocalStorage,
initialGameSettings, initialGameSettings,
setInitialGameSettings, setInitialGameSettings,
settings, settings,
setSettings, setSettings: setSettingsAndLocalStorage,
randomizingPlayer,
setRandomizingPlayer,
isPWA: window?.matchMedia('(display-mode: standalone)').matches, isPWA: window?.matchMedia('(display-mode: standalone)').matches,
preStartCompleted,
setPreStartCompleted: setPreStartCompletedAndLocalStorage,
}; };
}, [ }, [
active,
analytics,
initialGameSettings,
isFullscreen, isFullscreen,
isSupported, isSupported,
release, release,
active,
request, request,
settings,
showPlay,
type, type,
showPlay,
playing,
initialGameSettings,
settings,
randomizingPlayer,
preStartCompleted,
analytics,
]); ]);
return ( return (

View File

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

View File

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

View File

@@ -12,10 +12,18 @@ export enum GameFormat {
TwoHeadedGiant = 'two-headed-giant', TwoHeadedGiant = 'two-headed-giant',
} }
export enum PreStartMode {
None = 'none',
RandomKing = 'random-king',
FingerGame = 'finger-game',
}
export type Settings = { export type Settings = {
keepAwake: boolean; keepAwake: boolean;
showStartingPlayer: boolean; showStartingPlayer: boolean;
showPlayerMenuCog: boolean;
goFullscreenOnStart: boolean; goFullscreenOnStart: boolean;
preStartMode: PreStartMode;
}; };
export type InitialGameSettings = { export type InitialGameSettings = {
@@ -26,10 +34,34 @@ export type InitialGameSettings = {
orientation: Orientation; orientation: Orientation;
}; };
export const InitialGameSettingsSchema = z.object({ export const initialGameSettingsSchema = z.object({
startingLifeTotal: z.number().min(1).max(200).default(20), startingLifeTotal: z.number().min(1).max(200),
useCommanderDamage: z.boolean().default(false), useCommanderDamage: z.boolean(),
gameFormat: z.nativeEnum(GameFormat).optional(), gameFormat: z.nativeEnum(GameFormat),
numberOfPlayers: z.number().min(1).max(6).default(2), numberOfPlayers: z.number().min(1).max(6),
orientation: z.nativeEnum(Orientation).default(Orientation.Landscape), orientation: z.nativeEnum(Orientation),
}); });
export const defaultInitialGameSettings = {
numberOfPlayers: 4,
startingLifeTotal: 40,
useCommanderDamage: true,
orientation: Orientation.Landscape,
gameFormat: GameFormat.Commander,
};
export const settingsSchema = z.object({
keepAwake: z.boolean(),
showStartingPlayer: z.boolean(),
showPlayerMenuCog: z.boolean(),
goFullscreenOnStart: z.boolean(),
preStartMode: z.nativeEnum(PreStartMode),
});
export const defaultSettings: Settings = {
goFullscreenOnStart: true,
keepAwake: true,
showStartingPlayer: true,
showPlayerMenuCog: true,
preStartMode: PreStartMode.None,
};

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,8 +2,18 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html {
overflow: hidden;
}
body {
overflow: auto;
}
html, html,
body { body {
height: 100%;
position: relative;
background-color: theme('colors.background.default'); 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',

View File

@@ -2,79 +2,91 @@
import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas'; import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas';
import type { Config } from 'tailwindcss'; 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',
},
},
};
export const twGridTemplateAreas = {
onePlayerLandscape: ['player0 player0'],
onePlayerPortrait: ['player0', 'player0'],
twoPlayersOppositeLandscape: ['player0', 'player1'],
twoPlayersOppositePortrait: ['player0 player1', 'player0 player1'],
twoPlayersSameSideLandscape: ['player0 player1'],
threePlayers: ['player0 player0', 'player1 player2'],
threePlayersSide: [
'player0 player0 player0 player2',
'player1 player1 player1 player2',
],
fourPlayerPortrait: [
'player0 player1 player1 player1 player1 player3',
'player0 player2 player2 player2 player2 player3',
],
fourPlayer: ['player0 player1', 'player2 player3'],
fivePlayers: [
'player0 player0 player0 player1 player1 player1',
'player2 player2 player3 player3 player4 player4',
],
fivePlayersSide: [
'player0 player0 player0 player0 player0 player1 player1 player1 player1 player1 player2',
'player3 player3 player3 player3 player3 player4 player4 player4 player4 player4 player2',
],
sixPlayers: ['player0 player1 player2', 'player3 player4 player5'],
sixPlayersSide: [
'player0 player1 player1 player1 player1 player1 player1 player2 player2 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 player4 player4 player5 player5 player5 player5 player5 player5 player3',
],
};
/** @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: twGridTemplateAreas,
onePlayerLandscape: ['player0 player0'], colors: baseColors,
onePlayerPortrait: ['player0', 'player0'],
twoPlayersOppositeLandscape: ['player0', 'player1'],
twoPlayersOppositePortrait: ['player0 player1', 'player0 player1'],
twoPlayersSameSideLandscape: ['player0 player1'],
threePlayers: ['player0 player0', 'player1 player2'],
threePlayersSide: [
'player0 player0 player0 player2',
'player1 player1 player1 player2',
],
fourPlayerPortrait: [
'player0 player1 player1 player1 player1 player3',
'player0 player2 player2 player2 player2 player3',
],
fourPlayer: ['player0 player1', 'player2 player3'],
fivePlayers: [
'player0 player0 player0 player1 player1 player1',
'player2 player2 player3 player3 player4 player4',
],
fivePlayersSide: [
'player0 player0 player0 player0 player0 player1 player1 player1 player1 player1 player2',
'player3 player3 player3 player3 player3 player4 player4 player4 player4 player4 player2',
],
sixPlayers: ['player0 player1 player2', 'player3 player4 player5'],
sixPlayersSide: [
'player0 player1 player1 player1 player1 player2 player2 player2 player2 player3',
'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3',
],
},
colors: {
primary: {
main: '#7F9172',
dark: '#57654F',
},
secondary: {
main: '#5E714C',
},
background: {
default: '#495E35',
backdrop: 'rgba(0, 0, 0, 0.3)',
settings: 'rgba(20, 20, 0, 0.9)',
},
text: {
primary: '#F5F5F5',
secondary: '#b3b39b',
},
action: {
disabled: '#5E714C',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#00000070',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
},
keyframes: { keyframes: {
fadeOut: { fadeOut: {
'0%': { '0%': {
@@ -95,15 +107,4 @@ export default {
}, },
plugins: [tailwindcssGridAreas], plugins: [tailwindcssGridAreas],
} satisfies Config; } satisfies Config;
// #98FF98
// const fadeOut = keyframes`
// 0% {
// opacity: 1;
// }
// 33% {
// opacity: 0.6;
// }
// 100% {
// opacity: 0;
// }
// `;

View File

@@ -1,9 +1,19 @@
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: {

2231
yarn.lock

File diff suppressed because it is too large Load Diff