Add showMatchScore setting and forfeit button

Added two new features for better game control:

1. Show Match Score Setting:
   - Added showMatchScore boolean to Settings type (default: true)
   - Added toggle in SettingsDialog to control score visibility
   - Players.tsx now respects the setting when passing matchScore
   - Users can hide score badges in 1v1 games if desired

2. Forfeit Button in Player Menu:
   - Added Skull icon import to PlayerMenu
   - Added onForfeit optional prop to PlayerMenu
   - Added red skull button in player menu buttons section
   - LifeCounter passes forfeit handler (toggleGameLost) to menu
   - Only shows when player can lose (life ≤ 0 or poison/commander damage)
   - Allows players to forfeit game from their individual menu

Both features improve user control over the match scoring system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Viktor Rådberg
2025-11-16 18:33:38 +01:00
parent c71dbc2769
commit 309eb47f7a
5 changed files with 51 additions and 1 deletions

View File

@@ -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 in 1v1 games 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

View File

@@ -268,6 +268,7 @@ const LifeCounter = ({ player, opponents, matchScore }: LifeCounterProps) => {
isShown={showPlayerMenu} isShown={showPlayerMenu}
player={player} player={player}
setShowPlayerMenu={setShowPlayerMenu} setShowPlayerMenu={setShowPlayerMenu}
onForfeit={playerCanLose(player) ? toggleGameLost : undefined}
/> />
</LifeCounterWrapper> </LifeCounterWrapper>
</LifeCounterContentWrapper> </LifeCounterContentWrapper>

View File

@@ -16,6 +16,7 @@ import {
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';
@@ -90,12 +91,14 @@ type PlayerMenuProps = {
player: Player; player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void; setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean; isShown: boolean;
onForfeit?: () => void;
}; };
const PlayerMenu = ({ const PlayerMenu = ({
player, player,
setShowPlayerMenu, setShowPlayerMenu,
isShown, isShown,
onForfeit,
}: 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);
@@ -479,6 +482,28 @@ const PlayerMenu = ({
> >
<ResetGame size={iconSize} /> <ResetGame size={iconSize} />
</button> </button>
{onForfeit && (
<button
style={{
cursor: 'pointer',
userSelect: 'none',
fontSize: buttonFontSize,
padding: '2px',
}}
className="text-red-500"
onClick={() => {
analytics.trackEvent('forfeit_game', {
player: player.index,
});
onForfeit();
setShowPlayerMenu(false);
}}
aria-label="Forfeit Game"
>
<Skull size={iconSize} />
</button>
)}
</ButtonsSections> </ButtonsSections>
</BetterRowContainer> </BetterRowContainer>

View File

@@ -48,7 +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={players.length === 2 ? gameScore[player.index] : undefined} matchScore={
players.length === 2 && settings.showMatchScore
? gameScore[player.index]
: undefined
}
/> />
{settings.preStartMode === PreStartMode.RandomKing && {settings.preStartMode === PreStartMode.RandomKing &&

View File

@@ -27,6 +27,7 @@ export type Settings = {
preStartMode: PreStartMode; preStartMode: PreStartMode;
showAnimations: boolean; showAnimations: boolean;
useMonarch: boolean; useMonarch: boolean;
showMatchScore: boolean;
}; };
export type InitialGameSettings = { export type InitialGameSettings = {
@@ -61,6 +62,7 @@ export const settingsSchema = z.object({
preStartMode: z.nativeEnum(PreStartMode), preStartMode: z.nativeEnum(PreStartMode),
showAnimations: z.boolean(), showAnimations: z.boolean(),
useMonarch: z.boolean().default(false), useMonarch: z.boolean().default(false),
showMatchScore: z.boolean().default(true),
}); });
export const defaultSettings: Settings = { export const defaultSettings: Settings = {
@@ -71,4 +73,5 @@ export const defaultSettings: Settings = {
preStartMode: PreStartMode.None, preStartMode: PreStartMode.None,
showAnimations: true, showAnimations: true,
useMonarch: false, useMonarch: false,
showMatchScore: true,
}; };