From 1ab0294d483ade79808b96e881b09ab71aa53426 Mon Sep 17 00:00:00 2001 From: Oliveriver Date: Thu, 1 Aug 2024 15:31:14 +0100 Subject: [PATCH] Update player list to reflect new victory condition --- client/src/components/interface/OrderList.tsx | 4 +- .../src/components/interface/PlayerList.tsx | 94 ++++++++++--------- .../components/interface/PlayerListItem.tsx | 49 ++++++++++ .../interface/common/ExpandButton.tsx | 28 ++++++ .../interface/common/RemoveButton.tsx | 2 +- client/src/hooks/useRegionSvg.tsx | 1 - .../src/hooks/useSetAvailableInputModes.tsx | 16 +--- client/src/types/board.ts | 16 ++++ client/src/utils/constants.ts | 9 +- server/Repositories/WorldRepository.cs | 1 + 10 files changed, 154 insertions(+), 66 deletions(-) create mode 100644 client/src/components/interface/PlayerListItem.tsx create mode 100644 client/src/components/interface/common/ExpandButton.tsx diff --git a/client/src/components/interface/OrderList.tsx b/client/src/components/interface/OrderList.tsx index bb5a563..c4cc74c 100644 --- a/client/src/components/interface/OrderList.tsx +++ b/client/src/components/interface/OrderList.tsx @@ -14,8 +14,6 @@ const OrderList = () => { const timelines = filterUnique(orders.map(({ location }) => location.timeline)).sort(); const maxHeight = window.innerHeight - 168; - const scrollBehaviour = - scrollRef.current && scrollRef.current.scrollHeight > maxHeight ? 'scroll' : 'hidden'; return (
{ className="absolute right-10 bottom-32 flex flex-col gap-4 items-end" style={{ maxHeight, - overflowY: scrollBehaviour, + overflowY: 'auto', }} > {timelines.map((timeline) => ( diff --git a/client/src/components/interface/PlayerList.tsx b/client/src/components/interface/PlayerList.tsx index a3ecbee..ffa185b 100644 --- a/client/src/components/interface/PlayerList.tsx +++ b/client/src/components/interface/PlayerList.tsx @@ -1,65 +1,67 @@ -import { useContext } from 'react'; -import regions from '../../data/regions'; -import Nation, { getNationColour } from '../../types/enums/nation'; -import { getLatestPhase } from '../../types/enums/phase'; +import { useContext, useRef, useState } from 'react'; +import Nation from '../../types/enums/nation'; import colours from '../../utils/colours'; -import { filterUnique } from '../../utils/listUtils'; import WorldContext from '../context/WorldContext'; import GameDetails from './GameDetails'; +import { getActiveBoards } from '../../types/board'; +import { filterUnique } from '../../utils/listUtils'; +import PlayerListItem from './PlayerListItem'; const PlayerList = () => { const { world } = useContext(WorldContext); - if (!world) return null; - - const timelines = filterUnique(world.boards.map(({ timeline }) => timeline)); - const activeBoards = timelines.map((timeline) => - world.boards - .filter((board) => board.timeline === timeline) - .reduce((board1, board2) => { - if (board1.year > board2.year) return board1; - if (board2.year > board1.year) return board2; + const scrollRef = useRef(null); + const [expandedPlayers, setExpandedPlayers] = useState([]); - return getLatestPhase(board1.phase, board2.phase) === board1.phase ? board1 : board2; - }), - ); - - // TODO sort out new win condition - const totalCentres = - activeBoards.length * Object.values(regions).filter((region) => region.isSupplyCentre).length; + if (!world) return null; + const { winner } = world; - const winPercentages = Object.values(Nation) - .map((nation) => { - const centreCount = activeBoards.flatMap((board) => - Object.values(board.centres).filter((owner) => owner === nation), - ).length; + const activeBoards = getActiveBoards(world.boards); + const playerCentres = Object.values(Nation) + .map((nation) => ({ + player: nation, + centres: filterUnique( + activeBoards.flatMap((board) => + Object.keys(board.centres) + .filter((region) => board.centres[region] === nation) + .sort(), + ), + ), + })) + .sort((player1, player2) => player2.centres.length - player1.centres.length); - return { - nation, - percentage: 100 * (centreCount / totalCentres), - }; - }) - .sort((player1, player2) => player2.percentage - player1.percentage); + const maxHeight = window.innerHeight - 272; return (
- {winPercentages.map(({ nation, percentage }) => ( -
0 ? 1 : 0.3, - color: getNationColour(nation), - }} - > -

{`${Math.round(percentage)}%`}

-

{nation}

-
- ))} + {playerCentres.map(({ player, centres }) => { + const isExpanded = expandedPlayers.includes(player); + return ( + + setExpandedPlayers( + isExpanded + ? expandedPlayers.filter((nation) => nation !== player) + : [...expandedPlayers, player], + ) + } + /> + ); + })}
); diff --git a/client/src/components/interface/PlayerListItem.tsx b/client/src/components/interface/PlayerListItem.tsx new file mode 100644 index 0000000..6288585 --- /dev/null +++ b/client/src/components/interface/PlayerListItem.tsx @@ -0,0 +1,49 @@ +import Nation, { getNationColour } from '../../types/enums/nation'; +import { victoryRequiredCentreCount } from '../../utils/constants'; +import ExpandButton from './common/ExpandButton'; + +type PlayerListItemProps = { + player: Nation; + centres: string[]; + winner: Nation | null; + isExpanded: boolean; + toggleExpand: () => void; +}; + +const PlayerListItem = ({ + player, + centres, + winner, + isExpanded, + toggleExpand, +}: PlayerListItemProps) => { + const centreCount = centres.length; + const isEliminated = centreCount === 0 || (winner && player !== winner); + const colour = getNationColour(player); + + return ( + <> +
+

{player}

+

+ {`${centreCount}/${victoryRequiredCentreCount}`} +

+ +
+ {isExpanded && + centres.map((centre) => ( +

+ {centre} +

+ ))} + + ); +}; + +export default PlayerListItem; diff --git a/client/src/components/interface/common/ExpandButton.tsx b/client/src/components/interface/common/ExpandButton.tsx new file mode 100644 index 0000000..aa1a9c5 --- /dev/null +++ b/client/src/components/interface/common/ExpandButton.tsx @@ -0,0 +1,28 @@ +import colours from '../../../utils/colours'; + +type ExpandButtonProps = { + colour: string; + isExpanded: boolean; + toggleExpand: () => void; +}; + +const ExpandButton = ({ colour, isExpanded, toggleExpand }: ExpandButtonProps) => ( + +); + +export default ExpandButton; diff --git a/client/src/components/interface/common/RemoveButton.tsx b/client/src/components/interface/common/RemoveButton.tsx index 6ee3ff6..2ccef65 100644 --- a/client/src/components/interface/common/RemoveButton.tsx +++ b/client/src/components/interface/common/RemoveButton.tsx @@ -11,7 +11,7 @@ const RemoveButton = ({ isDisabled, remove }: RemoveButtonProps) => ( onClick={remove} className="relative w-3.5 h-3.5 rounded-full ml-3 opacity-30 hover:opacity-100 cursor-pointer" style={{ backgroundColor: colours.iconDelete, pointerEvents: isDisabled ? 'none' : 'all' }} - aria-label="Delete order" + aria-label="Delete item" disabled={isDisabled} >
({ ADR, diff --git a/client/src/hooks/useSetAvailableInputModes.tsx b/client/src/hooks/useSetAvailableInputModes.tsx index 8973939..10cbf16 100644 --- a/client/src/hooks/useSetAvailableInputModes.tsx +++ b/client/src/hooks/useSetAvailableInputModes.tsx @@ -1,10 +1,10 @@ import { useContext, useEffect } from 'react'; -import Phase, { getLatestPhase } from '../types/enums/phase'; -import { filterUnique } from '../utils/listUtils'; +import Phase from '../types/enums/phase'; import InputMode from '../types/enums/inputMode'; import { OrderEntryActionType } from '../types/context/orderEntryAction'; import OrderEntryContext from '../components/context/OrderEntryContext'; import WorldContext from '../components/context/WorldContext'; +import { getActiveBoards } from '../types/board'; const useSetAvailableInputModes = () => { const { world, isLoading, error } = useContext(WorldContext); @@ -13,17 +13,7 @@ const useSetAvailableInputModes = () => { const boards = world && !isLoading && !error ? world.boards : []; const winner = world?.winner; - const timelines = filterUnique(boards.map(({ timeline }) => timeline)); - const activeBoards = timelines.map((timeline) => - boards - .filter((board) => board.timeline === timeline) - .reduce((board1, board2) => { - if (board1.year > board2.year) return board1; - if (board2.year > board1.year) return board2; - - return getLatestPhase(board1.phase, board2.phase) === board1.phase ? board1 : board2; - }), - ); + const activeBoards = getActiveBoards(boards); const hasMajorBoard = !winner && activeBoards.some((board) => board.phase !== Phase.Winter); const hasMinorBoard = !winner && activeBoards.some((board) => board.phase === Phase.Winter); diff --git a/client/src/types/board.ts b/client/src/types/board.ts index 33f569a..04a719f 100644 --- a/client/src/types/board.ts +++ b/client/src/types/board.ts @@ -1,6 +1,8 @@ import Nation from './enums/nation'; import Unit from './unit'; import Location, { getLocationKey } from './location'; +import { filterUnique } from '../utils/listUtils'; +import { getLatestPhase } from './enums/phase'; type Board = Omit & { childTimelines: number[]; @@ -13,4 +15,18 @@ export const getBoardKey = (board: Board | Omit) => { return getLocationKey({ timeline, year, phase, region: '' }); }; +export const getActiveBoards = (boards: Board[]) => { + const timelines = filterUnique(boards.map(({ timeline }) => timeline)); + return timelines.map((timeline) => + boards + .filter((board) => board.timeline === timeline) + .reduce((board1, board2) => { + if (board1.year > board2.year) return board1; + if (board2.year > board1.year) return board2; + + return getLatestPhase(board1.phase, board2.phase) === board1.phase ? board1 : board2; + }), + ); +}; + export default Board; diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts index d9e9d2c..937c37d 100644 --- a/client/src/utils/constants.ts +++ b/client/src/utils/constants.ts @@ -1,18 +1,23 @@ +// Gameplay + export const startYear = 1901; +export const victoryRequiredCentreCount = 18; + +// UI export const initialScale = 0.8; export const orderFocusScale = 1.5; - export const majorBoardWidth = 1000; // TODO stop everything breaking if this changes export const minorBoardWidth = 650; export const boardSeparation = 400; export const boardBorderWidth = 32; // TODO stop everything breaking if this changes - export const unitWidth = 28; export const boardArrowWidth = 200; export const orderArrowStartSeparation = 20; export const orderArrowEndSeparation = 10; +// API + // TODO change both of these for production export const refetchInterval = 2000; export const refetchAttempts = 3; diff --git a/server/Repositories/WorldRepository.cs b/server/Repositories/WorldRepository.cs index 90bc295..15b7240 100644 --- a/server/Repositories/WorldRepository.cs +++ b/server/Repositories/WorldRepository.cs @@ -39,6 +39,7 @@ public async Task AddOrders(int gameId, Nation[] players, IEnumerable ord var newPlayersSubmitted = players.Where(p => !game.PlayersSubmitted.Contains(p)); game.PlayersSubmitted = [.. game.PlayersSubmitted, .. newPlayersSubmitted]; + // TODO figure out how to deal with player elimination (and resurrection?) if (game.PlayersSubmitted.Count == Constants.Nations.Count) { logger.LogInformation("Adjudicating game {GameId}", gameId);