score badge

This commit is contained in:
Viktor Rådberg
2025-11-16 21:49:25 +01:00
parent e0f50ac984
commit 60371264b1
6 changed files with 107 additions and 22 deletions

View File

@@ -256,7 +256,7 @@ export const SettingsDialog = ({
/>
</ToggleContainer>
<Description>
Shows a score badge on each player's card in 1v1 games to track wins across multiple games.
Shows a score badge on each player's card to track wins across multiple games.
</Description>
</SettingContainer>
<Separator height="1px" />

View File

@@ -32,23 +32,39 @@ const WinnerName = twc.div`
mb-2
`;
const StartButton = twc.button`
const PrimaryButton = twc.button`
py-4 px-6 rounded-xl
text-xl font-semibold
bg-interface-primary
text-white
transition-all duration-200
hover:scale-105 active:scale-95
border-2 border-transparent
hover:border-white/30
border-3 border-white/50
shadow-lg shadow-interface-primary/50
`;
const SecondaryButton = twc.button`
py-4 px-6 rounded-xl
text-xl 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 }: GameOverProps) => {
export const GameOver = ({
winner,
onStartNextGame,
onStay,
}: GameOverProps) => {
return (
<Overlay>
<Modal>
@@ -60,9 +76,10 @@ export const GameOver = ({ winner, onStartNextGame }: GameOverProps) => {
Won the game!
</p>
<ButtonContainer>
<StartButton onClick={onStartNextGame}>
<PrimaryButton onClick={onStartNextGame}>
Start Next Game
</StartButton>
</PrimaryButton>
<SecondaryButton onClick={onStay}>Close</SecondaryButton>
</ButtonContainer>
</Modal>
</Overlay>

View File

@@ -226,11 +226,8 @@ const LifeCounter = ({ player, opponents, matchScore }: LifeCounterProps) => {
<PlayerLostWrapper $rotation={player.settings.rotation} />
)}
{matchScore !== undefined && matchScore > 0 && (
<MatchScoreBadge
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
>
{matchScore}
<MatchScoreBadge $rotation={player.settings.rotation}>
<div style={{ rotate: `${-calcRotation}deg` }}>{matchScore}</div>
</MatchScoreBadge>
)}
<CommanderDamageBar
@@ -269,6 +266,7 @@ const LifeCounter = ({ player, opponents, matchScore }: LifeCounterProps) => {
player={player}
setShowPlayerMenu={setShowPlayerMenu}
onForfeit={toggleGameLost}
totalPlayers={opponents.length + 1}
/>
</LifeCounterWrapper>
</LifeCounterContentWrapper>

View File

@@ -92,6 +92,7 @@ type PlayerMenuProps = {
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean;
onForfeit?: () => void;
totalPlayers: number;
};
const PlayerMenu = ({
@@ -99,10 +100,12 @@ const PlayerMenu = ({
setShowPlayerMenu,
isShown,
onForfeit,
totalPlayers,
}: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const resetGameDialogRef = useRef<HTMLDialogElement | null>(null);
const endGameDialogRef = useRef<HTMLDialogElement | null>(null);
const forfeitGameDialogRef = useRef<HTMLDialogElement | null>(null);
const { isSide } = useSafeRotate({
rotation: player.settings.rotation,
@@ -492,6 +495,9 @@ const PlayerMenu = ({
}}
className="text-red-500"
onClick={() => {
if (totalPlayers === 2) {
forfeitGameDialogRef.current?.show();
} else {
if (onForfeit) {
analytics.trackEvent('forfeit_game', {
player: player.index,
@@ -499,6 +505,7 @@ const PlayerMenu = ({
onForfeit();
setShowPlayerMenu(false);
}
}
}}
aria-label="Forfeit Game"
>
@@ -584,6 +591,48 @@ const PlayerMenu = ({
</div>
</div>
</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>
</PlayerMenuWrapper>
);

View File

@@ -49,7 +49,7 @@ export const Players = ({ gridLayout }: { gridLayout: GridLayout }) => {
(opponent) => opponent.index !== player.index
)}
matchScore={
players.length === 2 && settings.showMatchScore
settings.showMatchScore
? gameScore[player.index]
: undefined
}

View File

@@ -96,9 +96,9 @@ export const Play = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Check for game over in 1v1 games
// Check for game over when only one player remains
useEffect(() => {
if (players.length !== 2 || winner !== null) {
if (players.length < 2 || winner !== null || !settings.showMatchScore) {
return;
}
@@ -108,7 +108,7 @@ export const Play = () => {
if (activePlayers.length === 1) {
setWinner(activePlayers[0].index);
}
}, [players, winner]);
}, [players, winner, settings.showMatchScore]);
const handleStartNextGame = () => {
if (winner === null) return;
@@ -127,6 +127,26 @@ export const Play = () => {
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 (
<MainWrapper>
{players.length > 1 &&
@@ -141,6 +161,7 @@ export const Play = () => {
<GameOver
winner={players[winner]}
onStartNextGame={handleStartNextGame}
onStay={handleStay}
/>
)}
</MainWrapper>