Cockatrice/webclient/src/components/Game/PlayerList/PlayerList.tsx
2026-04-25 14:42:41 -05:00

99 lines
3.2 KiB
TypeScript

import { GameSelectors, useAppSelector } from '@app/store';
import { cx } from '@app/utils';
import './PlayerList.css';
export interface PlayerListProps {
gameId: number | undefined;
}
// HSV gradient from green (0s) to red (>=10s); black when disconnected
// (ping < 0). Mirrors desktop's PingPixmapGenerator in
// cockatrice/src/interface/pixel_map_generator.cpp.
function pingCssColor(pingSeconds: number | undefined): string {
if (pingSeconds == null || pingSeconds < 0) {
return '#000';
}
const max = 10;
const ratio = Math.min(pingSeconds, max) / max;
const hue = 120 * (1 - ratio);
return `hsl(${hue}, 100%, 50%)`;
}
function PlayerList({ gameId }: PlayerListProps) {
const players = useAppSelector((state) =>
gameId != null ? GameSelectors.getPlayers(state, gameId) : undefined,
);
const activePlayerId = useAppSelector((state) =>
gameId != null ? GameSelectors.getActivePlayerId(state, gameId) : undefined,
);
const hostId = useAppSelector((state) =>
gameId != null ? GameSelectors.getHostId(state, gameId) : undefined,
);
const entries = players ? Object.values(players) : [];
return (
<div className="player-list" data-testid="player-list">
<div className="player-list__heading">Players</div>
<ul className="player-list__items">
{entries.length === 0 && (
<li className="player-list__empty">no players</li>
)}
{entries.map((p) => {
const pid = p.properties.playerId;
const name = p.properties.userInfo?.name ?? '(unknown)';
const isActive = pid === activePlayerId;
const isHost = pid === hostId;
const sideboardLocked = p.properties.sideboardLocked ?? false;
const pingSeconds = p.properties.pingSeconds;
const pingLabel = `ping ${pingSeconds ?? '?'}s`;
return (
<li
key={pid}
className={cx('player-list__item', {
'player-list__item--active': isActive,
'player-list__item--conceded': p.properties.conceded,
})}
data-testid={`player-list-item-${pid}`}
>
<span
className={cx('player-list__indicator', {
'player-list__indicator--active': isActive,
})}
/>
{isHost && (
<span
className="player-list__host-badge"
aria-label="host"
title="Host"
>
</span>
)}
<span className="player-list__name">{name}</span>
{sideboardLocked && (
<span
className="player-list__sideboard-lock"
aria-label="sideboard locked"
title="Sideboard locked"
>
🔒
</span>
)}
<span
className="player-list__ping-dot"
style={{ background: pingCssColor(pingSeconds) }}
aria-label={pingLabel}
title={pingLabel}
data-testid={`ping-dot-${pid}`}
/>
</li>
);
})}
</ul>
</div>
);
}
export default PlayerList;