mirror of
https://github.com/Vikeo/LifeTrinket.git
synced 2025-11-18 08:48:00 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58fb63eef1 | ||
|
|
b6a402bc9f | ||
|
|
ea5da632a8 | ||
|
|
ca4e3edb5f | ||
|
|
c6039c2a53 | ||
|
|
6d6da2ad79 | ||
|
|
51acebb50e | ||
|
|
35c1cac691 |
@@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
18
.github/workflows/firebase-release.yml
vendored
18
.github/workflows/firebase-release.yml
vendored
@@ -13,14 +13,22 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup bun
|
- name: Setup pnpm
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Build, lint, and deploy
|
- name: Build, lint, and deploy
|
||||||
run: |
|
run: |
|
||||||
bun install
|
pnpm install
|
||||||
bun run build
|
pnpm run build
|
||||||
bun run lint
|
pnpm 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:
|
||||||
|
|||||||
22
.github/workflows/lint_and_build.yml
vendored
22
.github/workflows/lint_and_build.yml
vendored
@@ -14,22 +14,24 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
version: 9
|
||||||
|
|
||||||
- name: Set up bun
|
- name: Setup Node.js
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: bun run lint
|
run: pnpm run lint
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: bun run build
|
run: pnpm run build
|
||||||
|
|||||||
26
eslint.config.mjs
Normal file
26
eslint.config.mjs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
66
package.json
66
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "life-trinket",
|
"name": "life-trinket",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.99",
|
"version": "1.0.5",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20",
|
"node": ">=20",
|
||||||
@@ -11,45 +11,45 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-icons": "npx @svgr/cli src/Icons/svgs",
|
"generate-icons": "npx @svgr/cli src/Icons/svgs",
|
||||||
"deploy": "bun run build && firebase deploy --only hosting"
|
"force-deploy": "pnpm run build && firebase deploy --only hosting",
|
||||||
|
"release": "bash scripts/create-release.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase": "^10.3.0",
|
"firebase": "^12.6.0",
|
||||||
"ga-4-react": "^0.1.281",
|
"react": "^19.2.0",
|
||||||
"react": "^18.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-screen-wake-lock": "^3.1.1",
|
||||||
"react-screen-wake-lock": "^3.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-twc": "^1.5.1",
|
||||||
"react-twc": "^1.3.0",
|
"semver": "^7.7.3",
|
||||||
"semver": "^7.6.2",
|
"zod": "^4.1.12"
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@eslint/js": "^9.39.1",
|
||||||
"@emotion/styled": "^11.11.5",
|
|
||||||
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@types/react": "^18.3.1",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/prop-types": "^15.7.15",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/react": "^19.2.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/parser": "^7.8.0",
|
"@types/semver": "^7.7.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"@typescript-eslint/parser": "^8.47.0",
|
||||||
"eslint": "^8.45.0",
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"autoprefixer": "^10.4.22",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint": "^9.39.1",
|
||||||
"firebase-tools": "^13.7.5",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"install": "^0.13.0",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"postcss": "^8.4.38",
|
"firebase-tools": "^14.25.0",
|
||||||
"prettier": "2.8.8",
|
"postcss": "^8.5.6",
|
||||||
|
"prettier": "3.6.2",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.2.10",
|
"typescript-eslint": "^8.47.0",
|
||||||
"vite-plugin-pwa": "^0.20.0"
|
"vite": "^7.2.2",
|
||||||
|
"vite-plugin-pwa": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10121
pnpm-lock.yaml
generated
10121
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
74
scripts/README.md
Normal file
74
scripts/README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Release Scripts
|
||||||
|
|
||||||
|
## create-release.sh
|
||||||
|
|
||||||
|
This script automates the process of creating a new release for LifeTrinket.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release
|
||||||
|
# or
|
||||||
|
pnpm release
|
||||||
|
# or
|
||||||
|
bash scripts/create-release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
1. **Reads the current version** from `package.json`
|
||||||
|
2. **Checks for existing tags** - If a tag with the current version already exists, it will prompt you to update the version in `package.json` first
|
||||||
|
3. **Warns about uncommitted changes** - Prompts for confirmation if you have uncommitted changes
|
||||||
|
4. **Prompts for release description** - You can enter a multi-line description for the release
|
||||||
|
5. **Creates an annotated git tag** with the version and description
|
||||||
|
6. **Pushes the tag to remote** - This triggers the GitHub Actions workflow that builds and deploys the app
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
When you push a tag, the following happens:
|
||||||
|
|
||||||
|
1. The `firebase-release.yml` workflow is triggered
|
||||||
|
2. The app is built and deployed to Firebase Hosting
|
||||||
|
3. A GitHub release is created with the version number
|
||||||
|
|
||||||
|
### Before running
|
||||||
|
|
||||||
|
Make sure to:
|
||||||
|
|
||||||
|
1. **Update the version** in `package.json` if needed
|
||||||
|
2. **Commit all changes** you want to include in the release
|
||||||
|
3. **Test the build** with `npm run build` to ensure everything works
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Update version in package.json to 1.0.3
|
||||||
|
# 2. Commit your changes
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: add new features for v1.0.3"
|
||||||
|
|
||||||
|
# 3. Run the release script
|
||||||
|
npm run release
|
||||||
|
|
||||||
|
# The script will:
|
||||||
|
# - Show current version: 1.0.3
|
||||||
|
# - Prompt for confirmation
|
||||||
|
# - Ask for release description
|
||||||
|
# - Create and push the tag
|
||||||
|
# - Trigger the deployment workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**"Tag already exists" error:**
|
||||||
|
|
||||||
|
- Update the version in `package.json` before creating a new release
|
||||||
|
|
||||||
|
**"Failed to push tag" error:**
|
||||||
|
|
||||||
|
- Check your git remote permissions
|
||||||
|
- Try pushing manually: `git push origin <version>`
|
||||||
|
|
||||||
|
**Script won't run:**
|
||||||
|
|
||||||
|
- Make sure the script is executable: `chmod +x scripts/create-release.sh`
|
||||||
87
scripts/create-release.sh
Executable file
87
scripts/create-release.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Color codes for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== LifeTrinket Release Script ===${NC}\n"
|
||||||
|
|
||||||
|
# Get current version from package.json
|
||||||
|
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
|
||||||
|
if [ -z "$CURRENT_VERSION" ]; then
|
||||||
|
echo -e "${RED}Error: Could not read version from package.json${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Current version in package.json:${NC} ${GREEN}$CURRENT_VERSION${NC}"
|
||||||
|
|
||||||
|
# Check if we're on a clean working tree
|
||||||
|
if [[ -n $(git status -s) ]]; then
|
||||||
|
echo -e "${YELLOW}Warning: You have uncommitted changes.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch latest tags from remote
|
||||||
|
echo -e "\n${BLUE}Fetching latest tags from remote...${NC}"
|
||||||
|
git fetch --tags
|
||||||
|
|
||||||
|
# Check if tag already exists locally or remotely
|
||||||
|
if git rev-parse "$CURRENT_VERSION" >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}Error: Tag '$CURRENT_VERSION' already exists!${NC}"
|
||||||
|
echo -e "${YELLOW}Please update the version in package.json before creating a new release.${NC}"
|
||||||
|
echo -e "${YELLOW}Current version: $CURRENT_VERSION${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the latest tag (if any)
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$LATEST_TAG" ]; then
|
||||||
|
echo -e "${BLUE}Latest existing tag:${NC} ${YELLOW}$LATEST_TAG${NC}"
|
||||||
|
|
||||||
|
# Compare versions
|
||||||
|
if [ "$LATEST_TAG" = "$CURRENT_VERSION" ]; then
|
||||||
|
echo -e "${RED}Error: Latest tag matches current version ($CURRENT_VERSION)${NC}"
|
||||||
|
echo -e "${YELLOW}Please update the version in package.json before creating a new release.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}No existing tags found. This will be the first release.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get release description from user
|
||||||
|
echo -e "\n${BLUE}Enter release description (optional, press Enter to skip):${NC}"
|
||||||
|
read -r RELEASE_DESCRIPTION
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_DESCRIPTION" ]; then
|
||||||
|
RELEASE_DESCRIPTION="Release $CURRENT_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create annotated tag with description
|
||||||
|
echo -e "\n${BLUE}Creating tag '$CURRENT_VERSION'...${NC}"
|
||||||
|
git tag -a "$CURRENT_VERSION" -m "$RELEASE_DESCRIPTION"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}Error: Failed to create tag${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Tag created successfully${NC}"
|
||||||
|
|
||||||
|
# Push tag to remote
|
||||||
|
echo -e "\n${BLUE}Pushing tag to remote...${NC}"
|
||||||
|
git push origin "$CURRENT_VERSION"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}Error: Failed to push tag${NC}"
|
||||||
|
echo -e "${YELLOW}Tag was created locally. You can try pushing manually:${NC}"
|
||||||
|
echo -e " git push origin $CURRENT_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}✓ Tag pushed successfully!${NC}"
|
||||||
|
echo -e "${BLUE}GitHub Actions will now build and deploy version $CURRENT_VERSION${NC}"
|
||||||
|
echo -e "${BLUE}Check the progress at:${NC} https://github.com/Vikeo/LifeTrinket/actions"
|
||||||
@@ -30,7 +30,7 @@ const CommanderDamageButton = twc.button<RotationButtonProps>((props) => [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
|
const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
|
||||||
'relative top-1/2 left-1/2 tabular-nums pointer-events-none select-none webkit-user-select-none',
|
'relative -translate-y-1/2 top-1/2 left-1/2 tabular-nums pointer-events-none select-none webkit-user-select-none',
|
||||||
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
|
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
|
||||||
? 'rotate-[270deg]'
|
? 'rotate-[270deg]'
|
||||||
: '',
|
: '',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CommanderTax,
|
CommanderTax,
|
||||||
Energy,
|
Energy,
|
||||||
Experience,
|
Experience,
|
||||||
|
Monarch,
|
||||||
PartnerTax,
|
PartnerTax,
|
||||||
Poison,
|
Poison,
|
||||||
} from '../../Icons/generated';
|
} from '../../Icons/generated';
|
||||||
@@ -108,6 +109,9 @@ export const InfoDialog = ({
|
|||||||
<li className="flex items-center">
|
<li className="flex items-center">
|
||||||
<Experience className="size-6" /> - Experience
|
<Experience className="size-6" /> - Experience
|
||||||
</li>
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<Monarch className="size-6" /> - Monarch
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 className="text-lg font-bold mb-2">Other functionality</h3>
|
<h3 className="text-lg font-bold mb-2">Other functionality</h3>
|
||||||
|
|||||||
@@ -242,6 +242,23 @@ export const SettingsDialog = ({
|
|||||||
</ul>
|
</ul>
|
||||||
</Description>
|
</Description>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
|
<SettingContainer>
|
||||||
|
<ToggleContainer>
|
||||||
|
<label>Show Match Score</label>
|
||||||
|
<ToggleButton
|
||||||
|
checked={settings.showMatchScore}
|
||||||
|
onChange={() => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
showMatchScore: !settings.showMatchScore,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToggleContainer>
|
||||||
|
<Description>
|
||||||
|
Shows a score badge on each player's card to track wins across multiple games.
|
||||||
|
</Description>
|
||||||
|
</SettingContainer>
|
||||||
<Separator height="1px" />
|
<Separator height="1px" />
|
||||||
<div className="flex w-full justify-center">
|
<div className="flex w-full justify-center">
|
||||||
<button
|
<button
|
||||||
|
|||||||
85
src/Components/GameOver/GameOver.tsx
Normal file
85
src/Components/GameOver/GameOver.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { twc } from 'react-twc';
|
||||||
|
import { Player } from '../../Types/Player';
|
||||||
|
|
||||||
|
const Overlay = twc.div`
|
||||||
|
fixed top-0 left-0 w-[100dvmax] h-[100dvmin]
|
||||||
|
bg-black/80 backdrop-blur-sm
|
||||||
|
flex items-center justify-center
|
||||||
|
z-50
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Modal = twc.div`
|
||||||
|
bg-background-default
|
||||||
|
rounded-2xl p-8
|
||||||
|
max-w-md w-[90%]
|
||||||
|
shadow-2xl
|
||||||
|
flex flex-col gap-6
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = twc.h2`
|
||||||
|
text-[7vmin] font-bold text-center
|
||||||
|
text-text-primary
|
||||||
|
-mb-4
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ButtonContainer = twc.div`
|
||||||
|
flex flex-col gap-3
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WinnerName = twc.div`
|
||||||
|
text-[6vmin] font-bold text-center
|
||||||
|
py-[2vmin] px-[3vmin] rounded-xl
|
||||||
|
text-white
|
||||||
|
mb-0
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PrimaryButton = twc.button`
|
||||||
|
py-[2vmin] px-[3vmin] rounded-xl
|
||||||
|
text-[4vmin] font-semibold
|
||||||
|
bg-interface-primary
|
||||||
|
text-white
|
||||||
|
transition-all duration-200
|
||||||
|
hover:scale-105 active:scale-95
|
||||||
|
border-3 border-white/50
|
||||||
|
shadow-lg shadow-interface-primary/50
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SecondaryButton = twc.button`
|
||||||
|
py-[2vmin] px-[3vmin] rounded-xl
|
||||||
|
text-[4vmin] font-semibold
|
||||||
|
bg-secondary-main
|
||||||
|
text-text-primary
|
||||||
|
transition-all duration-200
|
||||||
|
hover:scale-105 active:scale-95
|
||||||
|
border-3 border-primary-main
|
||||||
|
shadow-lg shadow-secondary-main/50
|
||||||
|
`;
|
||||||
|
|
||||||
|
type GameOverProps = {
|
||||||
|
winner: Player;
|
||||||
|
onStartNextGame: () => void;
|
||||||
|
onStay: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameOver = ({
|
||||||
|
winner,
|
||||||
|
onStartNextGame,
|
||||||
|
onStay,
|
||||||
|
}: GameOverProps) => {
|
||||||
|
return (
|
||||||
|
<Overlay>
|
||||||
|
<Modal>
|
||||||
|
<Title>Winner</Title>
|
||||||
|
<WinnerName style={{ backgroundColor: winner.color }}>
|
||||||
|
{winner.name || `Player ${winner.index + 1}`}
|
||||||
|
</WinnerName>
|
||||||
|
<ButtonContainer>
|
||||||
|
<SecondaryButton onClick={onStartNextGame}>
|
||||||
|
Start Next Game
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton onClick={onStay}>Close</PrimaryButton>
|
||||||
|
</ButtonContainer>
|
||||||
|
</Modal>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { twc } from 'react-twc';
|
import { twc } from 'react-twc';
|
||||||
|
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
|
||||||
import { Player, Rotation } from '../../Types/Player';
|
import { Player, Rotation } from '../../Types/Player';
|
||||||
import { RotationDivProps } from '../Buttons/CommanderDamage';
|
import { RotationDivProps } from '../Buttons/CommanderDamage';
|
||||||
import LifeCounterButton from '../Buttons/LifeCounterButton';
|
import LifeCounterButton from '../Buttons/LifeCounterButton';
|
||||||
|
import { MonarchCrown } from '../Misc/MonarchCrown';
|
||||||
import { OutlinedText } from '../Misc/OutlinedText';
|
import { OutlinedText } from '../Misc/OutlinedText';
|
||||||
|
|
||||||
const LifeContainer = twc.div<RotationDivProps>((props) => [
|
const LifeContainer = twc.div<RotationDivProps>((props) => [
|
||||||
@@ -33,7 +35,7 @@ const RecentDifference = twc.div`
|
|||||||
absolute min-w-[20vmin] drop-shadow-none text-center bg-interface-recentDifference-background tabular-nums rounded-full p-[6px 12px] text-[8vmin] text-interface-recentDifference-text animate-fadeOut
|
absolute min-w-[20vmin] drop-shadow-none text-center bg-interface-recentDifference-background tabular-nums rounded-full p-[6px 12px] text-[8vmin] text-interface-recentDifference-text animate-fadeOut
|
||||||
|
|
||||||
top-1/4 left-[50%] -translate-x-1/2
|
top-1/4 left-[50%] -translate-x-1/2
|
||||||
data-[isSide=true]:top-1/3 data-[isSide=true]:translate-x-1/4 data-[isSide=true]:translate-y-1/2 data-[isSide=true]:rotate-[270deg] data-[isSide=true]:left-auto
|
data-[is-side=true]:top-1/3 data-[is-side=true]:translate-x-1/4 data-[is-side=true]:translate-y-1/2 data-[is-side=true]:rotate-[270deg] data-[is-side=true]:left-auto
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type HealthProps = {
|
type HealthProps = {
|
||||||
@@ -53,6 +55,8 @@ const Health = ({
|
|||||||
const [fontSize, setFontSize] = useState(16);
|
const [fontSize, setFontSize] = useState(16);
|
||||||
const textContainerRef = useRef<HTMLDivElement | null>(null);
|
const textContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { settings } = useGlobalSettings();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!textContainerRef.current) {
|
if (!textContainerRef.current) {
|
||||||
return;
|
return;
|
||||||
@@ -104,6 +108,8 @@ const Health = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LifeContainer $rotation={player.settings.rotation}>
|
<LifeContainer $rotation={player.settings.rotation}>
|
||||||
|
{settings.useMonarch && <MonarchCrown player={player} />}
|
||||||
|
|
||||||
<LifeCounterButton
|
<LifeCounterButton
|
||||||
player={player}
|
player={player}
|
||||||
setLifeTotal={handleLifeChange}
|
setLifeTotal={handleLifeChange}
|
||||||
@@ -111,29 +117,32 @@ const Health = ({
|
|||||||
increment={-1}
|
increment={-1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{player.name && isSide ? (
|
<div
|
||||||
<div className="size-full relative flex items-center justify-start">
|
data-is-side={isSide}
|
||||||
<div className="fixed flex justify-center -rotate-90 left-[5.4vmax] ">
|
className="size-full absolute flex items-start justify-center pointer-events-none webkit-user-select-none
|
||||||
|
data-[is-side=true]:items-center data-[is-side=true]:justify-start
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{player.name && isSide ? (
|
||||||
|
<div className="fixed flex justify-center -rotate-90 left-[5.4vmax]">
|
||||||
<div
|
<div
|
||||||
data-contrast={player.iconTheme}
|
data-contrast={player.iconTheme}
|
||||||
className="absolute text-[4vmin] opacity-50 font-bold
|
className="absolute text-[4vmin] opacity-50 font-bold text-center text-nowrap
|
||||||
data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
|
data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
|
||||||
>
|
>
|
||||||
{player.name}
|
{player.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
|
||||||
<div className="w-full h-full relative flex items-start justify-center">
|
|
||||||
<div
|
<div
|
||||||
data-contrast={player.iconTheme}
|
data-contrast={player.iconTheme}
|
||||||
className="absolute text-[4vmin] -top-[1.1vmin] opacity-50 font-bold
|
className="absolute text-[4vmin] -top-[1.1vmin] opacity-50 font-bold text-center
|
||||||
data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
|
data-[contrast=dark]:text-icons-dark data-[contrast=light]:text-icons-light"
|
||||||
>
|
>
|
||||||
{player.name}
|
{player.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<TextWrapper>
|
<TextWrapper>
|
||||||
<LifeCounterTextContainer
|
<LifeCounterTextContainer
|
||||||
@@ -148,7 +157,7 @@ const Health = ({
|
|||||||
{player.lifeTotal}
|
{player.lifeTotal}
|
||||||
</OutlinedText>
|
</OutlinedText>
|
||||||
{recentDifference !== 0 && (
|
{recentDifference !== 0 && (
|
||||||
<RecentDifference data-isSide={isSide} key={differenceKey}>
|
<RecentDifference data-is-side={isSide} key={differenceKey}>
|
||||||
{recentDifference > 0 ? '+' : ''}
|
{recentDifference > 0 ? '+' : ''}
|
||||||
{recentDifference}
|
{recentDifference}
|
||||||
</RecentDifference>
|
</RecentDifference>
|
||||||
|
|||||||
@@ -24,6 +24,27 @@ const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [
|
|||||||
: 'top-1/4 right-[1vmax]',
|
: 'top-1/4 right-[1vmax]',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
type MatchScoreBadgeProps = RotationDivProps & {
|
||||||
|
$useCommanderDamage: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MatchScoreBadge = twc.div<MatchScoreBadgeProps>((props) => [
|
||||||
|
'absolute flex items-center justify-center',
|
||||||
|
'bg-black/70 backdrop-blur-sm',
|
||||||
|
'rounded-full',
|
||||||
|
'w-[5vmin] h-[5vmin]',
|
||||||
|
'text-white font-bold',
|
||||||
|
'text-[3vmin]',
|
||||||
|
'z-[1]',
|
||||||
|
'pointer-events-none',
|
||||||
|
'select-none webkit-user-select-none',
|
||||||
|
props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped
|
||||||
|
? `left-[6.5vmax] bottom-[1vmax]`
|
||||||
|
: props.$useCommanderDamage
|
||||||
|
? 'left-[0.5vmax] top-[11.5vmin]'
|
||||||
|
: 'left-[0.5vmax] top-[1vmax]',
|
||||||
|
]);
|
||||||
|
|
||||||
type SettingsButtonProps = {
|
type SettingsButtonProps = {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
rotation: Rotation;
|
rotation: Rotation;
|
||||||
@@ -98,11 +119,12 @@ type LifeCounterProps = {
|
|||||||
player: Player;
|
player: Player;
|
||||||
opponents: Player[];
|
opponents: Player[];
|
||||||
isStartingPlayer?: boolean;
|
isStartingPlayer?: boolean;
|
||||||
|
matchScore?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RECENT_DIFFERENCE_TTL = 3_000;
|
const RECENT_DIFFERENCE_TTL = 3_000;
|
||||||
|
|
||||||
const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
|
const LifeCounter = ({ player, opponents, matchScore }: LifeCounterProps) => {
|
||||||
const { updatePlayer, updateLifeTotal } = usePlayers();
|
const { updatePlayer, updateLifeTotal } = usePlayers();
|
||||||
const { settings, playing } = useGlobalSettings();
|
const { settings, playing } = useGlobalSettings();
|
||||||
const recentDifferenceTimerRef = useRef<NodeJS.Timeout | undefined>(
|
const recentDifferenceTimerRef = useRef<NodeJS.Timeout | undefined>(
|
||||||
@@ -173,9 +195,6 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [document.body.clientHeight, document.body.clientWidth]);
|
}, [document.body.clientHeight, document.body.clientWidth]);
|
||||||
|
|
||||||
player.settings.rotation === Rotation.SideFlipped ||
|
|
||||||
player.settings.rotation === Rotation.Side;
|
|
||||||
|
|
||||||
const handleLifeChange = (updatedLifeTotal: number) => {
|
const handleLifeChange = (updatedLifeTotal: number) => {
|
||||||
const difference = updateLifeTotal(player, updatedLifeTotal);
|
const difference = updateLifeTotal(player, updatedLifeTotal);
|
||||||
setRecentDifference(recentDifference + difference);
|
setRecentDifference(recentDifference + difference);
|
||||||
@@ -215,6 +234,21 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
|
|||||||
key={player.index}
|
key={player.index}
|
||||||
handleLifeChange={handleLifeChange}
|
handleLifeChange={handleLifeChange}
|
||||||
/>
|
/>
|
||||||
|
{matchScore !== undefined && matchScore > 0 && (
|
||||||
|
<MatchScoreBadge
|
||||||
|
$rotation={player.settings.rotation}
|
||||||
|
$useCommanderDamage={player.settings.useCommanderDamage}
|
||||||
|
style={{
|
||||||
|
rotate:
|
||||||
|
player.settings.rotation === Rotation.Side ||
|
||||||
|
player.settings.rotation === Rotation.SideFlipped
|
||||||
|
? `-90deg`
|
||||||
|
: '0deg',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{matchScore}
|
||||||
|
</MatchScoreBadge>
|
||||||
|
)}
|
||||||
{settings.showPlayerMenuCog && (
|
{settings.showPlayerMenuCog && (
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -238,11 +272,14 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
|
|||||||
recentDifference={recentDifference}
|
recentDifference={recentDifference}
|
||||||
handleLifeChange={handleLifeChange}
|
handleLifeChange={handleLifeChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExtraCountersBar player={player} />
|
<ExtraCountersBar player={player} />
|
||||||
<PlayerMenu
|
<PlayerMenu
|
||||||
isShown={showPlayerMenu}
|
isShown={showPlayerMenu}
|
||||||
player={player}
|
player={player}
|
||||||
setShowPlayerMenu={setShowPlayerMenu}
|
setShowPlayerMenu={setShowPlayerMenu}
|
||||||
|
onForfeit={toggleGameLost}
|
||||||
|
totalPlayers={opponents.length + 1}
|
||||||
/>
|
/>
|
||||||
</LifeCounterWrapper>
|
</LifeCounterWrapper>
|
||||||
</LifeCounterContentWrapper>
|
</LifeCounterContentWrapper>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ReactElement } from 'react';
|
||||||
|
|
||||||
export const IconCheckbox = ({
|
export const IconCheckbox = ({
|
||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
@@ -7,8 +9,8 @@ export const IconCheckbox = ({
|
|||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
icon: JSX.Element;
|
icon: ReactElement;
|
||||||
checkedIcon: JSX.Element;
|
checkedIcon: ReactElement;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BeforeInstallPromptEvent } from '../../global';
|
import { BeforeInstallPromptEvent } from '../../global';
|
||||||
import { useAnalytics } from '../../Hooks/useAnalytics';
|
import { useAnalytics } from '../../Hooks/useAnalytics';
|
||||||
|
|
||||||
export const InstallPWAButton = () => {
|
export const InstallPWAButton = () => {
|
||||||
const supportsPWARef = useRef<boolean>(false);
|
|
||||||
const [promptInstall, setPromptInstall] =
|
const [promptInstall, setPromptInstall] =
|
||||||
useState<BeforeInstallPromptEvent | null>(null);
|
useState<BeforeInstallPromptEvent | null>(null);
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@ export const InstallPWAButton = () => {
|
|||||||
|
|
||||||
const handler = (e: BeforeInstallPromptEvent) => {
|
const handler = (e: BeforeInstallPromptEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
supportsPWARef.current = true;
|
|
||||||
setPromptInstall(e);
|
setPromptInstall(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,7 +19,7 @@ export const InstallPWAButton = () => {
|
|||||||
return () => window.removeEventListener('transitionend', handler);
|
return () => window.removeEventListener('transitionend', handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!supportsPWARef.current) {
|
if (!promptInstall) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
src/Components/Misc/MonarchCrown.tsx
Normal file
67
src/Components/Misc/MonarchCrown.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { usePlayers } from '../../Hooks/usePlayers';
|
||||||
|
import { Monarch } from '../../Icons/generated';
|
||||||
|
import { Player, Rotation } from '../../Types/Player';
|
||||||
|
import { IconCheckbox } from './IconCheckbox';
|
||||||
|
|
||||||
|
export const MonarchCrown = ({ player }: { player: Player }) => {
|
||||||
|
const { players, setPlayers } = usePlayers();
|
||||||
|
|
||||||
|
const iconSize =
|
||||||
|
player.settings.rotation === Rotation.SideFlipped ||
|
||||||
|
player.settings.rotation === Rotation.Side
|
||||||
|
? '5vmax'
|
||||||
|
: '10vmin';
|
||||||
|
|
||||||
|
const rotationIsSide =
|
||||||
|
player.settings.rotation === Rotation.SideFlipped ||
|
||||||
|
player.settings.rotation === Rotation.Side;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-rotation-is-side={rotationIsSide}
|
||||||
|
className="absolute w-full h-full flex items-start justify-center pointer-events-none z-[1]
|
||||||
|
data-[rotation-is-side=true]:justify-start data-[rotation-is-side=true]:items-center
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-rotation-is-side={rotationIsSide}
|
||||||
|
className="data-[rotation-is-side=true]:-rotate-90"
|
||||||
|
>
|
||||||
|
<IconCheckbox
|
||||||
|
className="pointer-events-all"
|
||||||
|
name="useMonarch"
|
||||||
|
checked={player.isMonarch}
|
||||||
|
icon={<Monarch size={iconSize} color={player.color} stroke="white" />}
|
||||||
|
checkedIcon={
|
||||||
|
<div>
|
||||||
|
<Monarch
|
||||||
|
size={iconSize}
|
||||||
|
stroke="white"
|
||||||
|
className="absolute blur z-[-1] text-icons-gold"
|
||||||
|
/>
|
||||||
|
<Monarch
|
||||||
|
size={iconSize}
|
||||||
|
stroke="white"
|
||||||
|
className="text-icons-gold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedPlayer = { ...player, isMonarch: e.target.checked };
|
||||||
|
|
||||||
|
const updatedPlayers = players.map((p) => {
|
||||||
|
if (p.index === player.index) {
|
||||||
|
return updatedPlayer;
|
||||||
|
}
|
||||||
|
return { ...p, isMonarch: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
setPlayers(updatedPlayers);
|
||||||
|
}}
|
||||||
|
aria-checked={player.isMonarch}
|
||||||
|
aria-label="Monarch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,10 +2,7 @@ import { Rotation } from '../../Types/Player';
|
|||||||
|
|
||||||
import { twc } from 'react-twc';
|
import { twc } from 'react-twc';
|
||||||
//TODO Create provider for this
|
//TODO Create provider for this
|
||||||
import tailwindConfig from './../../../tailwind.config';
|
import { baseColors } from './../../../tailwind.config';
|
||||||
import resolveConfig from 'tailwindcss/resolveConfig';
|
|
||||||
|
|
||||||
const fullConfig = resolveConfig(tailwindConfig);
|
|
||||||
|
|
||||||
const Container = twc.div`
|
const Container = twc.div`
|
||||||
flex
|
flex
|
||||||
@@ -59,12 +56,12 @@ export const OutlinedText: React.FC<OutlinedTextProps> = ({
|
|||||||
fontSize,
|
fontSize,
|
||||||
fontWeight,
|
fontWeight,
|
||||||
strokeWidth: strokeWidth || '1vmin',
|
strokeWidth: strokeWidth || '1vmin',
|
||||||
color: fillColor || fullConfig.theme.colors.common.black,
|
color: fillColor || baseColors.common.black,
|
||||||
WebkitTextStroke: `${strokeWidth || '1vmin'} ${
|
WebkitTextStroke: `${strokeWidth || '1vmin'} ${
|
||||||
strokeColor || fullConfig.theme.colors.common.white
|
strokeColor || baseColors.common.white
|
||||||
}`,
|
}`,
|
||||||
WebkitTextFillColor:
|
WebkitTextFillColor:
|
||||||
fillColor || fullConfig.theme.colors.common.black,
|
fillColor || baseColors.common.black,
|
||||||
rotate: `${calcRotation}deg`,
|
rotate: `${calcRotation}deg`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ import { twc } from 'react-twc';
|
|||||||
|
|
||||||
export const Paragraph = twc.p`text-text-primary`;
|
export const Paragraph = twc.p`text-text-primary`;
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
export const H1 = twc.h1`text-text-primary;`;
|
export const H1 = twc.h1`text-text-primary;`;
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
Experience,
|
Experience,
|
||||||
FullscreenOff,
|
FullscreenOff,
|
||||||
FullscreenOn,
|
FullscreenOn,
|
||||||
|
Monarch,
|
||||||
NameTag,
|
NameTag,
|
||||||
PartnerTax,
|
PartnerTax,
|
||||||
Poison,
|
Poison,
|
||||||
ResetGame,
|
ResetGame,
|
||||||
|
Skull,
|
||||||
} from '../../Icons/generated';
|
} from '../../Icons/generated';
|
||||||
import { Player, Rotation } from '../../Types/Player';
|
import { Player, Rotation } from '../../Types/Player';
|
||||||
import { PreStartMode } from '../../Types/Settings';
|
import { PreStartMode } from '../../Types/Settings';
|
||||||
@@ -89,16 +91,21 @@ type PlayerMenuProps = {
|
|||||||
player: Player;
|
player: Player;
|
||||||
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
|
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
|
||||||
isShown: boolean;
|
isShown: boolean;
|
||||||
|
onForfeit?: () => void;
|
||||||
|
totalPlayers: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlayerMenu = ({
|
const PlayerMenu = ({
|
||||||
player,
|
player,
|
||||||
setShowPlayerMenu,
|
setShowPlayerMenu,
|
||||||
isShown,
|
isShown,
|
||||||
|
onForfeit,
|
||||||
|
totalPlayers,
|
||||||
}: PlayerMenuProps) => {
|
}: PlayerMenuProps) => {
|
||||||
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
|
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
|
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
const endGameDialogRef = useRef<HTMLDialogElement | null>(null);
|
const endGameDialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
|
const forfeitGameDialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
|
|
||||||
const { isSide } = useSafeRotate({
|
const { isSide } = useSafeRotate({
|
||||||
rotation: player.settings.rotation,
|
rotation: player.settings.rotation,
|
||||||
@@ -110,11 +117,13 @@ const PlayerMenu = ({
|
|||||||
wakeLock,
|
wakeLock,
|
||||||
goToStart,
|
goToStart,
|
||||||
settings,
|
settings,
|
||||||
|
setSettings,
|
||||||
setPlaying,
|
setPlaying,
|
||||||
setRandomizingPlayer,
|
setRandomizingPlayer,
|
||||||
saveCurrentGame,
|
saveCurrentGame,
|
||||||
initialGameSettings,
|
initialGameSettings,
|
||||||
setPreStartCompleted,
|
setPreStartCompleted,
|
||||||
|
gameScore,
|
||||||
} = useGlobalSettings();
|
} = useGlobalSettings();
|
||||||
|
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
@@ -157,7 +166,7 @@ const PlayerMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGoToStart = () => {
|
const handleGoToStart = () => {
|
||||||
saveCurrentGame({ players, initialGameSettings });
|
saveCurrentGame({ players, initialGameSettings, gameScore });
|
||||||
goToStart();
|
goToStart();
|
||||||
setRandomizingPlayer(true);
|
setRandomizingPlayer(true);
|
||||||
};
|
};
|
||||||
@@ -360,6 +369,36 @@ const PlayerMenu = ({
|
|||||||
aria-label="Experience"
|
aria-label="Experience"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconCheckbox
|
||||||
|
name="useMonarch"
|
||||||
|
checked={settings.useMonarch}
|
||||||
|
icon={
|
||||||
|
<Monarch
|
||||||
|
size={extraCountersSize}
|
||||||
|
color="black"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checkedIcon={
|
||||||
|
<Monarch
|
||||||
|
size={extraCountersSize}
|
||||||
|
color={player.color}
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
analytics.trackEvent('toggle_monarch', {
|
||||||
|
checked: e.target.checked,
|
||||||
|
});
|
||||||
|
setSettings({ ...settings, useMonarch: e.target.checked });
|
||||||
|
}}
|
||||||
|
aria-checked={settings.useMonarch}
|
||||||
|
aria-label="Monarch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TogglesSection>
|
</TogglesSection>
|
||||||
<ButtonsSections>
|
<ButtonsSections>
|
||||||
<button
|
<button
|
||||||
@@ -447,6 +486,32 @@ const PlayerMenu = ({
|
|||||||
>
|
>
|
||||||
<ResetGame size={iconSize} />
|
<ResetGame size={iconSize} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
fontSize: buttonFontSize,
|
||||||
|
padding: '2px',
|
||||||
|
}}
|
||||||
|
className="text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
if (totalPlayers === 2) {
|
||||||
|
forfeitGameDialogRef.current?.show();
|
||||||
|
} else {
|
||||||
|
if (onForfeit) {
|
||||||
|
analytics.trackEvent('forfeit_game', {
|
||||||
|
player: player.index,
|
||||||
|
});
|
||||||
|
onForfeit();
|
||||||
|
setShowPlayerMenu(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="Forfeit Game"
|
||||||
|
>
|
||||||
|
<Skull size={iconSize} />
|
||||||
|
</button>
|
||||||
</ButtonsSections>
|
</ButtonsSections>
|
||||||
</BetterRowContainer>
|
</BetterRowContainer>
|
||||||
|
|
||||||
@@ -527,6 +592,48 @@ const PlayerMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
ref={forfeitGameDialogRef}
|
||||||
|
className="z-[999] size-full bg-background-settings overflow-y-scroll"
|
||||||
|
onClick={() => forfeitGameDialogRef.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 }}
|
||||||
|
>
|
||||||
|
Forfeit 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={() => forfeitGameDialogRef.current?.close()}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-primary-main border border-primary-dark text-text-primary rounded-lg flex-grow"
|
||||||
|
onClick={() => {
|
||||||
|
if (onForfeit) {
|
||||||
|
analytics.trackEvent('forfeit_game', {
|
||||||
|
player: player.index,
|
||||||
|
});
|
||||||
|
onForfeit();
|
||||||
|
setShowPlayerMenu(false);
|
||||||
|
forfeitGameDialogRef.current?.close();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ fontSize: iconSize }}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</PlayerMenuWrapper>
|
</PlayerMenuWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const PlayersWrapper = twc.div`w-full h-full bg-black`;
|
|||||||
export const Players = ({ gridLayout }: { gridLayout: GridLayout }) => {
|
export const Players = ({ gridLayout }: { gridLayout: GridLayout }) => {
|
||||||
const { players } = usePlayers();
|
const { players } = usePlayers();
|
||||||
|
|
||||||
const { playing, settings, preStartCompleted } = useGlobalSettings();
|
const { playing, settings, preStartCompleted, gameScore } = useGlobalSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlayersWrapper>
|
<PlayersWrapper>
|
||||||
@@ -48,6 +48,11 @@ export const Players = ({ gridLayout }: { gridLayout: GridLayout }) => {
|
|||||||
opponents={players.filter(
|
opponents={players.filter(
|
||||||
(opponent) => opponent.index !== player.index
|
(opponent) => opponent.index !== player.index
|
||||||
)}
|
)}
|
||||||
|
matchScore={
|
||||||
|
settings.showMatchScore
|
||||||
|
? gameScore[player.index]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{settings.preStartMode === PreStartMode.RandomKing &&
|
{settings.preStartMode === PreStartMode.RandomKing &&
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const Trivia = () => {
|
|||||||
const { setPlaying, goToStart } = useGlobalSettings();
|
const { setPlaying, goToStart } = useGlobalSettings();
|
||||||
|
|
||||||
const [randomQuestion, setRandomQuestion] = useState(
|
const [randomQuestion, setRandomQuestion] = useState(
|
||||||
questions[Math.floor(Math.random() * questions.length)]
|
() => questions[Math.floor(Math.random() * questions.length)]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setUniqueRandomQuestion = () => {
|
const setUniqueRandomQuestion = () => {
|
||||||
|
|||||||
71
src/Components/ScoreDisplay/ScoreDisplay.tsx
Normal file
71
src/Components/ScoreDisplay/ScoreDisplay.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { twc } from 'react-twc';
|
||||||
|
import { Player } from '../../Types/Player';
|
||||||
|
import { GameScore } from '../../Contexts/GlobalSettingsContext';
|
||||||
|
|
||||||
|
const ScoreContainer = twc.div`
|
||||||
|
absolute bottom-4 left-1/2 -translate-x-1/2
|
||||||
|
bg-background-default/90 backdrop-blur-sm
|
||||||
|
rounded-lg p-4
|
||||||
|
shadow-lg
|
||||||
|
z-40
|
||||||
|
min-w-[200px]
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = twc.h3`
|
||||||
|
text-sm font-semibold text-text-secondary
|
||||||
|
uppercase tracking-wide mb-3
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ScoreList = twc.div`
|
||||||
|
flex flex-col gap-2
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ScoreItem = twc.div`
|
||||||
|
flex items-center justify-between gap-4
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PlayerInfo = twc.div`
|
||||||
|
flex items-center gap-2
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PlayerColor = twc.div`
|
||||||
|
w-4 h-4 rounded-full
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PlayerName = twc.span`
|
||||||
|
text-text-primary font-medium
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Score = twc.span`
|
||||||
|
text-text-primary font-bold text-lg
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ScoreDisplayProps = {
|
||||||
|
players: Player[];
|
||||||
|
gameScore: GameScore;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScoreDisplay = ({ players, gameScore }: ScoreDisplayProps) => {
|
||||||
|
const hasAnyScore = Object.values(gameScore).some((score) => score > 0);
|
||||||
|
|
||||||
|
if (!hasAnyScore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScoreContainer>
|
||||||
|
<Title>Match Score</Title>
|
||||||
|
<ScoreList>
|
||||||
|
{players.map((player) => (
|
||||||
|
<ScoreItem key={player.index}>
|
||||||
|
<PlayerInfo>
|
||||||
|
<PlayerColor style={{ backgroundColor: player.color }} />
|
||||||
|
<PlayerName>{player.name || `Player ${player.index + 1}`}</PlayerName>
|
||||||
|
</PlayerInfo>
|
||||||
|
<Score>{gameScore[player.index] || 0}</Score>
|
||||||
|
</ScoreItem>
|
||||||
|
))}
|
||||||
|
</ScoreList>
|
||||||
|
</ScoreContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { twc } from 'react-twc';
|
import { twc } from 'react-twc';
|
||||||
import { twGridTemplateAreas } from '../../../tailwind.config';
|
import { twGridTemplateAreas } from '../../../tailwind.config';
|
||||||
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
|
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
|
||||||
@@ -6,6 +6,7 @@ import { usePlayers } from '../../Hooks/usePlayers';
|
|||||||
import { Orientation, PreStartMode } from '../../Types/Settings';
|
import { Orientation, PreStartMode } from '../../Types/Settings';
|
||||||
import { Players } from '../Players/Players';
|
import { Players } from '../Players/Players';
|
||||||
import { PreStart } from '../PreStartGame/PreStart';
|
import { PreStart } from '../PreStartGame/PreStart';
|
||||||
|
import { GameOver } from '../GameOver/GameOver';
|
||||||
|
|
||||||
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden, setPlayers`;
|
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden, setPlayers`;
|
||||||
|
|
||||||
@@ -14,9 +15,10 @@ type GridTemplateAreasKeys = keyof typeof twGridTemplateAreas;
|
|||||||
export type GridLayout = `grid-areas-${GridTemplateAreasKeys}`;
|
export type GridLayout = `grid-areas-${GridTemplateAreasKeys}`;
|
||||||
|
|
||||||
export const Play = () => {
|
export const Play = () => {
|
||||||
const { players, setPlayers } = usePlayers();
|
const { players, setPlayers, resetCurrentGame, setStartingPlayerIndex } = usePlayers();
|
||||||
const { initialGameSettings, playing, settings, preStartCompleted } =
|
const { initialGameSettings, playing, settings, preStartCompleted, gameScore, setGameScore } =
|
||||||
useGlobalSettings();
|
useGlobalSettings();
|
||||||
|
const [winner, setWinner] = useState<number | null>(null);
|
||||||
|
|
||||||
let gridLayout: GridLayout;
|
let gridLayout: GridLayout;
|
||||||
switch (players.length) {
|
switch (players.length) {
|
||||||
@@ -94,6 +96,57 @@ export const Play = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check for game over when only one player remains
|
||||||
|
useEffect(() => {
|
||||||
|
if (players.length < 2 || winner !== null || !settings.showMatchScore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activePlayers = players.filter((p) => !p.hasLost);
|
||||||
|
|
||||||
|
// If only one player is alive, they are the winner
|
||||||
|
if (activePlayers.length === 1) {
|
||||||
|
setWinner(activePlayers[0].index);
|
||||||
|
}
|
||||||
|
}, [players, winner, settings.showMatchScore]);
|
||||||
|
|
||||||
|
const handleStartNextGame = () => {
|
||||||
|
if (winner === null) return;
|
||||||
|
|
||||||
|
// Update score
|
||||||
|
const newScore = { ...gameScore };
|
||||||
|
newScore[winner] = (newScore[winner] || 0) + 1;
|
||||||
|
setGameScore(newScore);
|
||||||
|
|
||||||
|
// Set the loser as the starting player for next game
|
||||||
|
const loserIndex = players.find((p) => p.index !== winner)?.index ?? 0;
|
||||||
|
setStartingPlayerIndex(loserIndex);
|
||||||
|
|
||||||
|
// Reset game
|
||||||
|
resetCurrentGame();
|
||||||
|
setWinner(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStay = () => {
|
||||||
|
if (winner === null) return;
|
||||||
|
|
||||||
|
// Update score
|
||||||
|
const newScore = { ...gameScore };
|
||||||
|
newScore[winner] = (newScore[winner] || 0) + 1;
|
||||||
|
setGameScore(newScore);
|
||||||
|
|
||||||
|
// Reset hasLost state for all players
|
||||||
|
setPlayers(
|
||||||
|
players.map((p) => ({
|
||||||
|
...p,
|
||||||
|
hasLost: false,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear winner to allow new game over detection
|
||||||
|
setWinner(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainWrapper>
|
<MainWrapper>
|
||||||
{players.length > 1 &&
|
{players.length > 1 &&
|
||||||
@@ -103,6 +156,14 @@ export const Play = () => {
|
|||||||
settings.showStartingPlayer && <PreStart />}
|
settings.showStartingPlayer && <PreStart />}
|
||||||
|
|
||||||
<Players gridLayout={gridLayout} />
|
<Players gridLayout={gridLayout} />
|
||||||
|
|
||||||
|
{winner !== null && (
|
||||||
|
<GameOver
|
||||||
|
winner={players[winner]}
|
||||||
|
onStartNextGame={handleStartNextGame}
|
||||||
|
onStay={handleStay}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MainWrapper>
|
</MainWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { Orientation } from '../../../Types/Settings';
|
|||||||
|
|
||||||
const LayoutsRadioGroup = twc.div`flex flex-row justify-center items-center gap-4 self-center w-full`;
|
const LayoutsRadioGroup = twc.div`flex flex-row justify-center items-center gap-4 self-center w-full`;
|
||||||
|
|
||||||
const Label = twc.label`flex flex-row relative max-w-[118px] hover:bg-primary-main hover:bg-opacity-5 rounded-2xl cursor-pointer`;
|
const Label = twc.label`flex flex-row relative max-w-[118px] hover:bg-white/[0.03] rounded-2xl cursor-pointer`;
|
||||||
|
|
||||||
const Input = twc.input`peer sr-only`;
|
const Input = twc.input`peer sr-only`;
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const Start = () => {
|
|||||||
setPlaying,
|
setPlaying,
|
||||||
savedGame,
|
savedGame,
|
||||||
saveCurrentGame,
|
saveCurrentGame,
|
||||||
|
setGameScore,
|
||||||
} = useGlobalSettings();
|
} = useGlobalSettings();
|
||||||
|
|
||||||
const infoDialogRef = useRef<HTMLDialogElement | null>(null);
|
const infoDialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
@@ -213,6 +214,9 @@ const Start = () => {
|
|||||||
|
|
||||||
setInitialGameSettings(savedGame.initialGameSettings);
|
setInitialGameSettings(savedGame.initialGameSettings);
|
||||||
setPlayers(savedGame.players);
|
setPlayers(savedGame.players);
|
||||||
|
if (savedGame.gameScore) {
|
||||||
|
setGameScore(savedGame.gameScore);
|
||||||
|
}
|
||||||
saveCurrentGame(null);
|
saveCurrentGame(null);
|
||||||
setRandomizingPlayer(false);
|
setRandomizingPlayer(false);
|
||||||
setShowPlay(true);
|
setShowPlay(true);
|
||||||
@@ -407,15 +411,31 @@ const Start = () => {
|
|||||||
{savedGame && (
|
{savedGame && (
|
||||||
<button
|
<button
|
||||||
className="flex flex-grow basis-0 justify-center self-center items-center bg-secondary-main px-3 py-2 rounded-md text-text-primary min-w-[150px]
|
className="flex flex-grow basis-0 justify-center self-center items-center bg-secondary-main px-3 py-2 rounded-md text-text-primary min-w-[150px]
|
||||||
|
|
||||||
duration-200 ease-in-out shadow-[1px_2px_4px_0px_rgba(0,0,0,0.3)] hover:bg-secondary-dark font-bold"
|
duration-200 ease-in-out shadow-[1px_2px_4px_0px_rgba(0,0,0,0.3)] hover:bg-secondary-dark font-bold"
|
||||||
onClick={doResumeGame}
|
onClick={doResumeGame}
|
||||||
>
|
>
|
||||||
RESUME
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-xs">
|
<div>
|
||||||
({savedGame.players.length}
|
RESUME
|
||||||
{savedGame.players.length > 1 ? 'players' : 'player'})
|
<span className="text-xs">
|
||||||
</span>
|
({savedGame.players.length}
|
||||||
|
{savedGame.players.length > 1 ? 'players' : 'player'})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{savedGame.gameScore && Object.keys(savedGame.gameScore).length > 0 && (
|
||||||
|
<div className="text-xs opacity-75">
|
||||||
|
Score: {Object.entries(savedGame.gameScore)
|
||||||
|
.map(([playerIndex, score]) => {
|
||||||
|
const player = savedGame.players.find(
|
||||||
|
(p) => p.index === Number(playerIndex)
|
||||||
|
);
|
||||||
|
return `${player?.name || `P${Number(playerIndex) + 1}`}: ${score}`;
|
||||||
|
})
|
||||||
|
.join(' | ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</StartButtonFooter>
|
</StartButtonFooter>
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ type Version = {
|
|||||||
export type SavedGame = {
|
export type SavedGame = {
|
||||||
initialGameSettings: InitialGameSettings;
|
initialGameSettings: InitialGameSettings;
|
||||||
players: Player[];
|
players: Player[];
|
||||||
|
gameScore?: GameScore;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
|
export type GameScore = {
|
||||||
|
[playerIndex: number]: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type GlobalSettingsContextType = {
|
export type GlobalSettingsContextType = {
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
@@ -45,6 +50,9 @@ export type GlobalSettingsContextType = {
|
|||||||
version: Version;
|
version: Version;
|
||||||
savedGame: SavedGame;
|
savedGame: SavedGame;
|
||||||
saveCurrentGame: (currentGame: SavedGame) => void;
|
saveCurrentGame: (currentGame: SavedGame) => void;
|
||||||
|
gameScore: GameScore;
|
||||||
|
setGameScore: (score: GameScore) => void;
|
||||||
|
resetGameScore: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobalSettingsContext =
|
export const GlobalSettingsContext =
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ export const createInitialPlayers = ({
|
|||||||
isStartingPlayer: false,
|
isStartingPlayer: false,
|
||||||
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
|
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
|
||||||
name: '',
|
name: '',
|
||||||
|
isMonarch: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
players.push(player);
|
players.push(player);
|
||||||
|
|||||||
34
src/Icons/generated/Monarch.tsx
Normal file
34
src/Icons/generated/Monarch.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { SVGProps } from 'react';
|
||||||
|
interface SVGRProps {
|
||||||
|
title?: string;
|
||||||
|
titleId?: string;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
const Monarch = ({
|
||||||
|
title,
|
||||||
|
titleId,
|
||||||
|
...props
|
||||||
|
}: SVGProps<SVGSVGElement> & SVGRProps) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={props.size || 16}
|
||||||
|
height={props.size || 16}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 52 52"
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{title ? <title id={titleId}>{title}</title> : null}
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M46.163 38.82s-8.614 2.73-14.234 3.106c-2.508.167-3.918 0-6.429 0-2.51 0-3.921.167-6.429 0-5.62-.376-14.234-3.107-14.234-3.107s.637-3.944.459-6.471C5.053 28.888 3 24.038 3 24.038s2.897 2.25 4.592 1.294C9.78 24.098 10.5 20 10.5 20s3.006 6.024 7 5.332c2.386-.414 3.327-1.974 4.5-4.016.97-1.69 1.27-4.827 1.27-4.827l1.77-4.827L25.5 10l.46 1.662 1.77 4.827s.3 3.136 1.27 4.827c1.173 2.042 2.388 3.353 4.5 4.016 4.051 1.273 7-5.332 7-5.332s.72 4.098 2.908 5.332c1.695.956 4.592-1.294 4.592-1.294s-2.053 4.85-2.296 8.31c-.178 2.527.46 6.471.46 6.471"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Monarch.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
};
|
||||||
|
export default Monarch;
|
||||||
@@ -10,6 +10,7 @@ export { default as FullscreenOn } from './FullscreenOn';
|
|||||||
export { default as Info } from './Info';
|
export { default as Info } from './Info';
|
||||||
export { default as LittleGuy } from './LittleGuy';
|
export { default as LittleGuy } from './LittleGuy';
|
||||||
export { default as Logo } from './Logo';
|
export { default as Logo } from './Logo';
|
||||||
|
export { default as Monarch } from './Monarch';
|
||||||
export { default as NameTag } from './NameTag';
|
export { default as NameTag } from './NameTag';
|
||||||
export { default as PartnerTax } from './PartnerTax';
|
export { default as PartnerTax } from './PartnerTax';
|
||||||
export { default as Poison } from './Poison';
|
export { default as Poison } from './Poison';
|
||||||
|
|||||||
3
src/Icons/svgs/Monarch.svg
Normal file
3
src/Icons/svgs/Monarch.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M46.1633 38.8191C46.1633 38.8191 37.5494 41.5504 31.9286 41.9256C29.421 42.093 28.0105 41.9256 25.5 41.9256C22.9895 41.9256 21.579 42.093 19.0714 41.9256C13.4506 41.5504 4.83673 38.8191 4.83673 38.8191C4.83673 38.8191 5.47353 34.8751 5.29592 32.3476C5.05284 28.8883 3 24.0377 3 24.0377C3 24.0377 5.89664 26.2882 7.59184 25.332C9.77975 24.0978 10.5 20 10.5 20C10.5 20 13.5058 26.0243 17.5 25.332C19.886 24.9184 20.8269 23.3583 22 21.3158C22.9708 19.6255 23.2704 16.4887 23.2704 16.4887L25.0408 11.6616L25.5 10L25.9592 11.6616L27.7296 16.4887C27.7296 16.4887 28.0292 19.6255 29 21.3158C30.1731 23.3583 31.3881 24.6686 33.5 25.332C37.5515 26.6047 40.5 20 40.5 20C40.5 20 41.2203 24.0978 43.4082 25.332C45.1034 26.2882 48 24.0377 48 24.0377C48 24.0377 45.9472 28.8883 45.7041 32.3476C45.5265 34.8751 46.1633 38.8191 46.1633 38.8191Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 964 B |
@@ -1,6 +1,7 @@
|
|||||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
import { useWakeLock } from 'react-screen-wake-lock';
|
import { useWakeLock } from 'react-screen-wake-lock';
|
||||||
import {
|
import {
|
||||||
|
GameScore,
|
||||||
GlobalSettingsContext,
|
GlobalSettingsContext,
|
||||||
GlobalSettingsContextType,
|
GlobalSettingsContextType,
|
||||||
SavedGame,
|
SavedGame,
|
||||||
@@ -61,14 +62,20 @@ export const GlobalSettingsProvider = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const savedSettings = localStorage.getItem('settings');
|
const savedSettings = localStorage.getItem('settings');
|
||||||
const [randomizingPlayer, setRandomizingPlayer] = useState<boolean>(
|
const [randomizingPlayer, setRandomizingPlayer] = useState<boolean>(() => {
|
||||||
savedSettings
|
if (!savedSettings) return true;
|
||||||
? Boolean(JSON.parse(savedSettings).preStartMode === 'random-king')
|
const parsed = JSON.parse(savedSettings);
|
||||||
: true
|
return Boolean(parsed.preStartMode === 'random-king');
|
||||||
);
|
});
|
||||||
const [settings, setSettings] = useState<Settings>(
|
const [settings, setSettings] = useState<Settings>(() => {
|
||||||
savedSettings ? JSON.parse(savedSettings) : defaultSettings
|
if (!savedSettings) return defaultSettings;
|
||||||
);
|
const parsed = settingsSchema.safeParse(JSON.parse(savedSettings));
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error('invalid settings, using default settings');
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
|
});
|
||||||
|
|
||||||
const setSettingsAndLocalStorage = (settings: Settings) => {
|
const setSettingsAndLocalStorage = (settings: Settings) => {
|
||||||
setSettings(settings);
|
setSettings(settings);
|
||||||
@@ -78,11 +85,17 @@ export const GlobalSettingsProvider = ({
|
|||||||
const savedGameSettings = localStorage.getItem('initialGameSettings');
|
const savedGameSettings = localStorage.getItem('initialGameSettings');
|
||||||
|
|
||||||
const [initialGameSettings, setInitialGameSettings] =
|
const [initialGameSettings, setInitialGameSettings] =
|
||||||
useState<InitialGameSettings>(
|
useState<InitialGameSettings>(() => {
|
||||||
savedGameSettings
|
if (!savedGameSettings) return defaultInitialGameSettings;
|
||||||
? JSON.parse(savedGameSettings)
|
const parsed = initialGameSettingsSchema.safeParse(
|
||||||
: defaultInitialGameSettings
|
JSON.parse(savedGameSettings)
|
||||||
);
|
);
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error('invalid game settings, using default settings');
|
||||||
|
return defaultInitialGameSettings;
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
|
});
|
||||||
|
|
||||||
const setInitialGameSettingsAndLocalStorage = (
|
const setInitialGameSettingsAndLocalStorage = (
|
||||||
initialGameSettings: InitialGameSettings
|
initialGameSettings: InitialGameSettings
|
||||||
@@ -94,59 +107,18 @@ export const GlobalSettingsProvider = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLocalStorage = async () => {
|
const savedGameScore = localStorage.getItem('gameScore');
|
||||||
localStorage.removeItem('initialGameSettings');
|
const [gameScore, setGameScore] = useState<GameScore>(
|
||||||
localStorage.removeItem('players');
|
savedGameScore ? JSON.parse(savedGameScore) : {}
|
||||||
localStorage.removeItem('playing');
|
);
|
||||||
localStorage.removeItem('showPlay');
|
const setGameScoreAndLocalStorage = (score: GameScore) => {
|
||||||
localStorage.removeItem('preStartComplete');
|
setGameScore(score);
|
||||||
|
localStorage.setItem('gameScore', JSON.stringify(score));
|
||||||
setPlaying(false);
|
};
|
||||||
setShowPlay(false);
|
const resetGameScore = () => {
|
||||||
setPreStartCompleted(false);
|
setGameScore({});
|
||||||
|
localStorage.removeItem('gameScore');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set settings if they are not valid
|
|
||||||
useEffect(() => {
|
|
||||||
// If there are no saved settings, set default settings
|
|
||||||
if (!savedSettings) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
//parse existing game settings with zod schema
|
|
||||||
const parsedInitialGameSettings =
|
|
||||||
initialGameSettingsSchema.safeParse(initialGameSettings);
|
|
||||||
|
|
||||||
if (!parsedInitialGameSettings.success) {
|
|
||||||
console.error('invalid game settings, resetting to default settings');
|
|
||||||
setInitialGameSettingsAndLocalStorage(defaultInitialGameSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
'initialGameSettings',
|
|
||||||
JSON.stringify(parsedInitialGameSettings.data)
|
|
||||||
);
|
|
||||||
}, [initialGameSettings, savedGameSettings]);
|
|
||||||
|
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
@@ -177,6 +149,21 @@ export const GlobalSettingsProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ctxValue = useMemo((): GlobalSettingsContextType => {
|
const ctxValue = useMemo((): GlobalSettingsContextType => {
|
||||||
|
const removeLocalStorage = async () => {
|
||||||
|
localStorage.removeItem('initialGameSettings');
|
||||||
|
localStorage.removeItem('players');
|
||||||
|
localStorage.removeItem('playing');
|
||||||
|
localStorage.removeItem('showPlay');
|
||||||
|
localStorage.removeItem('preStartComplete');
|
||||||
|
localStorage.removeItem('gameScore');
|
||||||
|
|
||||||
|
setPlaying(false);
|
||||||
|
setShowPlay(false);
|
||||||
|
setPreStartCompleted(false);
|
||||||
|
setSettings({ ...settings, useMonarch: false });
|
||||||
|
setGameScore({});
|
||||||
|
};
|
||||||
|
|
||||||
const goToStart = async () => {
|
const goToStart = async () => {
|
||||||
const currentPlayers = localStorage.getItem('players');
|
const currentPlayers = localStorage.getItem('players');
|
||||||
|
|
||||||
@@ -223,17 +210,20 @@ export const GlobalSettingsProvider = ({
|
|||||||
|
|
||||||
async function checkForNewVersion(source: 'settings' | 'start_menu') {
|
async function checkForNewVersion(source: 'settings' | 'start_menu') {
|
||||||
try {
|
try {
|
||||||
|
const token = import.meta.env.VITE_REPO_READ_ACCESS_TOKEN;
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
Accept: 'application/vnd.github+json',
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add authorization if token is available
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await fetch(
|
const result = await fetch(
|
||||||
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
|
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
|
||||||
{
|
{ headers }
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${
|
|
||||||
import.meta.env.VITE_REPO_READ_ACCESS_TOKEN
|
|
||||||
}`,
|
|
||||||
Accept: 'application/vnd.github+json',
|
|
||||||
'X-GitHub-Api-Version': '2022-11-28',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
|
|
||||||
@@ -282,7 +272,7 @@ export const GlobalSettingsProvider = ({
|
|||||||
playing,
|
playing,
|
||||||
setPlaying: setPlayingAndLocalStorage,
|
setPlaying: setPlayingAndLocalStorage,
|
||||||
initialGameSettings,
|
initialGameSettings,
|
||||||
setInitialGameSettings,
|
setInitialGameSettings: setInitialGameSettingsAndLocalStorage,
|
||||||
settings,
|
settings,
|
||||||
setSettings: setSettingsAndLocalStorage,
|
setSettings: setSettingsAndLocalStorage,
|
||||||
randomizingPlayer,
|
randomizingPlayer,
|
||||||
@@ -292,13 +282,15 @@ export const GlobalSettingsProvider = ({
|
|||||||
setPreStartCompleted: setPreStartCompletedAndLocalStorage,
|
setPreStartCompleted: setPreStartCompletedAndLocalStorage,
|
||||||
savedGame,
|
savedGame,
|
||||||
saveCurrentGame: setCurrentGameAndLocalStorage,
|
saveCurrentGame: setCurrentGameAndLocalStorage,
|
||||||
|
|
||||||
version: {
|
version: {
|
||||||
installedVersion: import.meta.env.VITE_APP_VERSION,
|
installedVersion: import.meta.env.VITE_APP_VERSION,
|
||||||
remoteVersion,
|
remoteVersion,
|
||||||
isLatest: isLatestVersion,
|
isLatest: isLatestVersion,
|
||||||
checkForNewVersion,
|
checkForNewVersion,
|
||||||
},
|
},
|
||||||
|
gameScore,
|
||||||
|
setGameScore: setGameScoreAndLocalStorage,
|
||||||
|
resetGameScore,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
isFullscreen,
|
isFullscreen,
|
||||||
@@ -312,11 +304,13 @@ export const GlobalSettingsProvider = ({
|
|||||||
initialGameSettings,
|
initialGameSettings,
|
||||||
settings,
|
settings,
|
||||||
randomizingPlayer,
|
randomizingPlayer,
|
||||||
|
setRandomizingPlayer,
|
||||||
preStartCompleted,
|
preStartCompleted,
|
||||||
savedGame,
|
savedGame,
|
||||||
remoteVersion,
|
remoteVersion,
|
||||||
isLatestVersion,
|
isLatestVersion,
|
||||||
analytics,
|
analytics,
|
||||||
|
gameScore,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ReactNode, useEffect } from 'react';
|
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Player } from '../Types/Player';
|
import { Player } from '../Types/Player';
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { PlayersContextType, PlayersContext } from '../Contexts/PlayersContext';
|
import { PlayersContextType, PlayersContext } from '../Contexts/PlayersContext';
|
||||||
import { InitialGameSettings } from '../Types/Settings';
|
import { InitialGameSettings } from '../Types/Settings';
|
||||||
|
|
||||||
@@ -13,10 +12,10 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
savedStartingPlayerIndex ? parseInt(savedStartingPlayerIndex) : -1
|
savedStartingPlayerIndex ? parseInt(savedStartingPlayerIndex) : -1
|
||||||
);
|
);
|
||||||
|
|
||||||
const setStartingPlayerIndexAndLocalStorage = (index: number) => {
|
const setStartingPlayerIndexAndLocalStorage = useCallback((index: number) => {
|
||||||
setStartingPlayerIndex(index);
|
setStartingPlayerIndex(index);
|
||||||
localStorage.setItem('startingPlayerIndex', String(index));
|
localStorage.setItem('startingPlayerIndex', String(index));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const [players, setPlayers] = useState<Player[]>(
|
const [players, setPlayers] = useState<Player[]>(
|
||||||
savedPlayers ? JSON.parse(savedPlayers) : []
|
savedPlayers ? JSON.parse(savedPlayers) : []
|
||||||
@@ -61,7 +60,11 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStartingPlayerIndex = Math.floor(Math.random() * players.length);
|
// Use the saved starting player index if available, otherwise random
|
||||||
|
const newStartingPlayerIndex =
|
||||||
|
startingPlayerIndex >= 0
|
||||||
|
? startingPlayerIndex
|
||||||
|
: Math.floor(Math.random() * players.length);
|
||||||
|
|
||||||
players.forEach((player: Player) => {
|
players.forEach((player: Player) => {
|
||||||
player.commanderDamage.map((damage) => {
|
player.commanderDamage.map((damage) => {
|
||||||
@@ -92,7 +95,7 @@ export const PlayersProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
startingPlayerIndex,
|
startingPlayerIndex,
|
||||||
setStartingPlayerIndex: setStartingPlayerIndexAndLocalStorage,
|
setStartingPlayerIndex: setStartingPlayerIndexAndLocalStorage,
|
||||||
};
|
};
|
||||||
}, [players, startingPlayerIndex]);
|
}, [players, startingPlayerIndex, setStartingPlayerIndexAndLocalStorage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlayersContext.Provider value={ctxValue}>
|
<PlayersContext.Provider value={ctxValue}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type Player = {
|
|||||||
commanderDamage: CommanderDamage[];
|
commanderDamage: CommanderDamage[];
|
||||||
extraCounters: ExtraCounter[];
|
extraCounters: ExtraCounter[];
|
||||||
isStartingPlayer: boolean;
|
isStartingPlayer: boolean;
|
||||||
|
isMonarch: boolean;
|
||||||
hasLost: boolean;
|
hasLost: boolean;
|
||||||
isSide: boolean;
|
isSide: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export type Settings = {
|
|||||||
goFullscreenOnStart: boolean;
|
goFullscreenOnStart: boolean;
|
||||||
preStartMode: PreStartMode;
|
preStartMode: PreStartMode;
|
||||||
showAnimations: boolean;
|
showAnimations: boolean;
|
||||||
|
useMonarch: boolean;
|
||||||
|
showMatchScore: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitialGameSettings = {
|
export type InitialGameSettings = {
|
||||||
@@ -59,6 +61,8 @@ export const settingsSchema = z.object({
|
|||||||
goFullscreenOnStart: z.boolean(),
|
goFullscreenOnStart: z.boolean(),
|
||||||
preStartMode: z.nativeEnum(PreStartMode),
|
preStartMode: z.nativeEnum(PreStartMode),
|
||||||
showAnimations: z.boolean(),
|
showAnimations: z.boolean(),
|
||||||
|
useMonarch: z.boolean().default(false),
|
||||||
|
showMatchScore: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultSettings: Settings = {
|
export const defaultSettings: Settings = {
|
||||||
@@ -68,4 +72,6 @@ export const defaultSettings: Settings = {
|
|||||||
showPlayerMenuCog: true,
|
showPlayerMenuCog: true,
|
||||||
preStartMode: PreStartMode.None,
|
preStartMode: PreStartMode.None,
|
||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
|
useMonarch: false,
|
||||||
|
showMatchScore: true,
|
||||||
};
|
};
|
||||||
|
|||||||
115
src/index.css
115
src/index.css
@@ -1,6 +1,32 @@
|
|||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
@theme {
|
||||||
|
--color-primary-main: #78A083;
|
||||||
|
--color-primary-dark: #608069;
|
||||||
|
--color-secondary-main: #5D7965;
|
||||||
|
--color-secondary-dark: #4a6151;
|
||||||
|
--color-background-default: #2E3041;
|
||||||
|
--color-background-spotlight: #777BA7;
|
||||||
|
--color-background-backdrop: rgba(0, 0, 0, 0.3);
|
||||||
|
--color-background-settings: rgba(0, 0, 0, 0.8);
|
||||||
|
--color-icons-dark: #000000;
|
||||||
|
--color-icons-light: #F9FFE3;
|
||||||
|
--color-icons-gold: #FFD700;
|
||||||
|
--color-text-primary: #F9FFE3;
|
||||||
|
--color-text-secondary: #c7ccb6;
|
||||||
|
--color-common-white: #F9FFE3;
|
||||||
|
--color-common-black: #000000;
|
||||||
|
--color-lifeCounter-text: rgba(0, 0, 0, 0.4);
|
||||||
|
--color-lifeCounter-lostWrapper: #000000;
|
||||||
|
--color-interface-loseButton-background: #43434380;
|
||||||
|
--color-interface-recentDifference-background: rgba(255, 255, 255, 0.6);
|
||||||
|
--color-interface-recentDifference-text: #333333;
|
||||||
|
|
||||||
|
--font-size-xxs: 0.625rem;
|
||||||
|
--line-height-xxs: 1rem;
|
||||||
|
|
||||||
|
--breakpoint-modalSm: 548px;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -14,7 +40,7 @@ html,
|
|||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: theme('colors.background.default');
|
background-color: var(--color-background-default);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
@@ -31,7 +57,7 @@ code {
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide scrollbar globally
|
/* hide scrollbar globally */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -43,18 +69,18 @@ code {
|
|||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background-color: theme('colors.background.default');
|
background-color: var(--color-background-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: theme('colors.primary.dark');
|
background: var(--color-primary-dark);
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: theme('colors.primary.main');
|
background: var(--color-primary-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -79,6 +105,67 @@ code {
|
|||||||
scrollbar-width: auto;
|
scrollbar-width: auto;
|
||||||
-ms-overflow-style: auto;
|
-ms-overflow-style: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Grid template areas */
|
||||||
|
.grid-areas-onePlayerLandscape {
|
||||||
|
grid-template-areas: 'player0 player0';
|
||||||
|
}
|
||||||
|
.grid-areas-onePlayerPortrait {
|
||||||
|
grid-template-areas: 'player0' 'player0';
|
||||||
|
}
|
||||||
|
.grid-areas-twoPlayersOppositeLandscape {
|
||||||
|
grid-template-areas: 'player0' 'player1';
|
||||||
|
}
|
||||||
|
.grid-areas-twoPlayersOppositePortrait {
|
||||||
|
grid-template-areas: 'player0 player1' 'player0 player1';
|
||||||
|
}
|
||||||
|
.grid-areas-twoPlayersSameSideLandscape {
|
||||||
|
grid-template-areas: 'player0 player1';
|
||||||
|
}
|
||||||
|
.grid-areas-threePlayers {
|
||||||
|
grid-template-areas: 'player0 player0' 'player1 player2';
|
||||||
|
}
|
||||||
|
.grid-areas-threePlayersSide {
|
||||||
|
grid-template-areas: 'player0 player0 player0 player2' 'player1 player1 player1 player2';
|
||||||
|
}
|
||||||
|
.grid-areas-fourPlayerPortrait {
|
||||||
|
grid-template-areas: 'player0 player1 player1 player1 player1 player3' 'player0 player2 player2 player2 player2 player3';
|
||||||
|
}
|
||||||
|
.grid-areas-fourPlayer {
|
||||||
|
grid-template-areas: 'player0 player1' 'player2 player3';
|
||||||
|
}
|
||||||
|
.grid-areas-fivePlayers {
|
||||||
|
grid-template-areas: 'player0 player0 player0 player1 player1 player1' 'player2 player2 player3 player3 player4 player4';
|
||||||
|
}
|
||||||
|
.grid-areas-fivePlayersSide {
|
||||||
|
grid-template-areas: 'player0 player0 player0 player0 player0 player1 player1 player1 player1 player1 player2' 'player3 player3 player3 player3 player3 player4 player4 player4 player4 player4 player2';
|
||||||
|
}
|
||||||
|
.grid-areas-sixPlayers {
|
||||||
|
grid-template-areas: 'player0 player1 player2' 'player3 player4 player5';
|
||||||
|
}
|
||||||
|
.grid-areas-sixPlayersSide {
|
||||||
|
grid-template-areas: '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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid area assignments */
|
||||||
|
.grid-in-player0 {
|
||||||
|
grid-area: player0;
|
||||||
|
}
|
||||||
|
.grid-in-player1 {
|
||||||
|
grid-area: player1;
|
||||||
|
}
|
||||||
|
.grid-in-player2 {
|
||||||
|
grid-area: player2;
|
||||||
|
}
|
||||||
|
.grid-in-player3 {
|
||||||
|
grid-area: player3;
|
||||||
|
}
|
||||||
|
.grid-in-player4 {
|
||||||
|
grid-area: player4;
|
||||||
|
}
|
||||||
|
.grid-in-player5 {
|
||||||
|
grid-area: player5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes background-orb {
|
@keyframes background-orb {
|
||||||
@@ -120,7 +207,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spotlight1 {
|
.spotlight1 {
|
||||||
background: theme('colors.background.default');
|
background: var(--color-background-default);
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 10vmax;
|
height: 10vmax;
|
||||||
@@ -142,7 +229,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spotlight2 {
|
.spotlight2 {
|
||||||
background: theme('colors.background.default');
|
background: var(--color-background-default);
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 30vmax;
|
height: 30vmax;
|
||||||
@@ -166,7 +253,7 @@ input[type='range'] {
|
|||||||
transition: background 0ms ease-in;
|
transition: background 0ms ease-in;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: theme('colors.secondary.main');
|
background: var(--color-secondary-main);
|
||||||
}
|
}
|
||||||
input[type='range']:focus {
|
input[type='range']:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -187,7 +274,7 @@ input[type='range']::-webkit-slider-thumb {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
background: theme('colors.primary.main');
|
background: var(--color-primary-main);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
@@ -207,7 +294,7 @@ input[type='range']::-moz-range-thumb {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
background: theme('colors.primary.main');
|
background: var(--color-primary-main);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +312,7 @@ input[type='range']::-ms-thumb {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
background: theme('colors.primary.main');
|
background: var(--color-primary-main);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
/* eslint-disable-next-line no-undef */
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
template: require('./template'),
|
template: require('./template'),
|
||||||
titleProp: true,
|
titleProp: true,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//@ts-expect-error - tailwindcss-grid-areas does not have typescript support
|
|
||||||
import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas';
|
|
||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
export const baseColors = {
|
export const baseColors = {
|
||||||
@@ -20,6 +18,7 @@ export const baseColors = {
|
|||||||
icons: {
|
icons: {
|
||||||
dark: '#000000',
|
dark: '#000000',
|
||||||
light: '#F9FFE3',
|
light: '#F9FFE3',
|
||||||
|
gold: '#FFD700',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#F9FFE3',
|
primary: '#F9FFE3',
|
||||||
@@ -106,6 +105,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [tailwindcssGridAreas],
|
plugins: [],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
// #98FF98
|
// #98FF98
|
||||||
|
|||||||
@@ -34,7 +34,5 @@ const propTypesTemplate = (
|
|||||||
export default ${title}`;
|
export default ${title}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
module.exports = propTypesTemplate;
|
module.exports = propTypesTemplate;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user