Added prettier
This commit is contained in:
parent
57c046fc77
commit
b0c6641ea2
@ -1,5 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
9
pac-man-board-game/ClientApp/.prettierrc.json
Normal file
9
pac-man-board-game/ClientApp/.prettierrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"printWidth": 120
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"happy-dom": "^12.10.3",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
@ -41,7 +42,8 @@
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"test": "cross-env CI=true vitest",
|
||||
"coverage": "vitest run --coverage"
|
||||
"coverage": "vitest run --coverage",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
10
pac-man-board-game/ClientApp/pnpm-lock.yaml
generated
10
pac-man-board-game/ClientApp/pnpm-lock.yaml
generated
@ -67,6 +67,9 @@ importers:
|
||||
postcss:
|
||||
specifier: ^8.4.31
|
||||
version: 8.4.31
|
||||
prettier:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
tailwindcss:
|
||||
specifier: ^3.3.5
|
||||
version: 3.3.5
|
||||
@ -2442,6 +2445,7 @@ packages:
|
||||
chalk: 3.0.0
|
||||
diff-match-patch: 1.0.5
|
||||
dev: false
|
||||
bundledDependencies: []
|
||||
|
||||
/lilconfig@2.1.0:
|
||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||
@ -2890,6 +2894,12 @@ packages:
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/prettier@3.0.3:
|
||||
resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/pretty-format@29.6.2:
|
||||
resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -1,21 +1,21 @@
|
||||
import React, {FC} from "react";
|
||||
import {Navigate, Route, Routes} from "react-router-dom";
|
||||
import Layout from "./components/layout";
|
||||
import AppRoutes from "./AppRoutes";
|
||||
import "./index.css";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {thisPlayerAtom} from "./utils/state";
|
||||
import React, { FC } from "react"
|
||||
import { Navigate, Route, Routes } from "react-router-dom"
|
||||
import Layout from "./components/layout"
|
||||
import AppRoutes from "./AppRoutes"
|
||||
import "./index.css"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { thisPlayerAtom } from "./utils/state"
|
||||
|
||||
export const App: FC = () => (
|
||||
<Layout>
|
||||
<Routes>
|
||||
{AppRoutes.map((route, index) => {
|
||||
const {element, secured = false, ...rest} = route;
|
||||
return <Route key={index} {...rest} element={<Secured secured={secured}>{element}</Secured>}/>;
|
||||
const { element, secured = false, ...rest } = route
|
||||
return <Route key={index} {...rest} element={<Secured secured={secured}>{element}</Secured>} />
|
||||
})}
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
)
|
||||
|
||||
/**
|
||||
* This component is used to redirect the user to the login page if they are not logged in and the page is secured.
|
||||
@ -23,13 +23,15 @@ export const App: FC = () => (
|
||||
* @param secured Whether or not the page is secured.
|
||||
* @constructor The Secured component.
|
||||
*/
|
||||
const Secured: FC<{
|
||||
secured: boolean
|
||||
} & ChildProps> = ({children, secured}) => {
|
||||
const player = useAtomValue(thisPlayerAtom);
|
||||
const Secured: FC<
|
||||
{
|
||||
secured: boolean
|
||||
} & ChildProps
|
||||
> = ({ children, secured }) => {
|
||||
const player = useAtomValue(thisPlayerAtom)
|
||||
|
||||
if (secured && player === undefined) {
|
||||
return <Navigate to={"/login"} replace/>
|
||||
return <Navigate to={"/login"} replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
|
@ -1,37 +1,37 @@
|
||||
import React from "react";
|
||||
import {Counter} from "./pages/counter";
|
||||
import GamePage from "./pages/game";
|
||||
import LobbyPage from "./pages/lobby";
|
||||
import LoginPage from "./pages/login";
|
||||
import HomePage from "./pages/home";
|
||||
import React from "react"
|
||||
import { Counter } from "./pages/counter"
|
||||
import GamePage from "./pages/game"
|
||||
import LobbyPage from "./pages/lobby"
|
||||
import LoginPage from "./pages/login"
|
||||
import HomePage from "./pages/home"
|
||||
|
||||
const AppRoutes = [
|
||||
{
|
||||
index: true,
|
||||
element: <HomePage/>
|
||||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
path: "/counter",
|
||||
element: <Counter/>
|
||||
element: <Counter />,
|
||||
},
|
||||
{
|
||||
path: "/game/:id",
|
||||
element: <GamePage/>,
|
||||
secured: true
|
||||
element: <GamePage />,
|
||||
secured: true,
|
||||
},
|
||||
{
|
||||
path: "/lobby",
|
||||
element: <LobbyPage/>,
|
||||
secured: true
|
||||
element: <LobbyPage />,
|
||||
secured: true,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage/>
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: <p>Page not found</p>
|
||||
}
|
||||
];
|
||||
element: <p>Page not found</p>,
|
||||
},
|
||||
]
|
||||
|
||||
export default AppRoutes;
|
||||
export default AppRoutes
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React, {FC} from "react";
|
||||
import React, { FC } from "react"
|
||||
|
||||
export const Button: FC<ButtonProps> = (
|
||||
{
|
||||
className,
|
||||
onClick,
|
||||
style,
|
||||
title,
|
||||
id,
|
||||
disabled = false,
|
||||
children,
|
||||
type = "button",
|
||||
}) => {
|
||||
export const Button: FC<ButtonProps> = ({
|
||||
className,
|
||||
onClick,
|
||||
style,
|
||||
title,
|
||||
id,
|
||||
disabled = false,
|
||||
children,
|
||||
type = "button",
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
id={id}
|
||||
@ -23,4 +22,4 @@ export const Button: FC<ButtonProps> = (
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,34 @@
|
||||
import React, {FC} from "react";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import {BugAntIcon} from "@heroicons/react/20/solid";
|
||||
import {selectedMapAtom} from "../utils/state";
|
||||
import {useAtom} from "jotai";
|
||||
import React, { FC } from "react"
|
||||
import useToggle from "../hooks/useToggle"
|
||||
import { BugAntIcon } from "@heroicons/react/20/solid"
|
||||
import { selectedMapAtom } from "../utils/state"
|
||||
import { useAtom } from "jotai"
|
||||
|
||||
const DebugMenu: FC = () => {
|
||||
|
||||
const [open, toggleOpen] = useToggle();
|
||||
const [open, toggleOpen] = useToggle()
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
return (
|
||||
<div>
|
||||
{open && <DebugOptions/>}
|
||||
<button className={"fixed bottom-2 right-2 bg-gray-800 text-white p-2 z-50 rounded-full"}
|
||||
title={"Debug menu"}
|
||||
onClick={() => toggleOpen()}>
|
||||
<BugAntIcon className={"w-8 m-1"}/>
|
||||
{open && <DebugOptions />}
|
||||
<button
|
||||
className={"fixed bottom-2 right-2 bg-gray-800 text-white p-2 z-50 rounded-full"}
|
||||
title={"Debug menu"}
|
||||
onClick={() => toggleOpen()}>
|
||||
<BugAntIcon className={"w-8 m-1"} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DebugMenu;
|
||||
export default DebugMenu
|
||||
|
||||
const DebugOptions: FC = () => {
|
||||
|
||||
const [map, setMap] = useAtom(selectedMapAtom);
|
||||
const [map, setMap] = useAtom(selectedMapAtom)
|
||||
|
||||
function clearSessionStorage(): void {
|
||||
sessionStorage.clear();
|
||||
sessionStorage.clear()
|
||||
}
|
||||
|
||||
function restartGame(): void {
|
||||
|
@ -1,56 +1,49 @@
|
||||
import React, {FC} from "react";
|
||||
import {useAtom, useAtomValue} from "jotai";
|
||||
import {selectedDiceAtom, thisPlayerAtom,} from "../utils/state";
|
||||
import {Button} from "./button";
|
||||
import React, { FC } from "react"
|
||||
import { useAtom, useAtomValue } from "jotai"
|
||||
import { selectedDiceAtom, thisPlayerAtom } from "../utils/state"
|
||||
import { Button } from "./button"
|
||||
|
||||
export const AllDice: FC<{ values?: number[] } & ComponentProps> = (
|
||||
{
|
||||
className,
|
||||
values,
|
||||
}) => {
|
||||
|
||||
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
|
||||
export const AllDice: FC<{ values?: number[] } & ComponentProps> = ({ className, values }) => {
|
||||
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom)
|
||||
|
||||
function handleClick(dice: SelectedDice): void {
|
||||
setSelectedDice(dice);
|
||||
setSelectedDice(dice)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"flex gap-5 justify-center"}>
|
||||
{values?.map((value, index) =>
|
||||
<Dice key={index}
|
||||
className={`${selectedDice?.index === index ? "border-2 border-black" : ""} ${className}`}
|
||||
value={value}
|
||||
onClick={(value) => handleClick({index, value})}/>)}
|
||||
{values?.map((value, index) => (
|
||||
<Dice
|
||||
key={index}
|
||||
className={`${selectedDice?.index === index ? "border-2 border-black" : ""} ${className}`}
|
||||
value={value}
|
||||
onClick={value => handleClick({ index, value })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DiceProps extends ComponentProps {
|
||||
value?: number,
|
||||
onClick?: (value: number) => void,
|
||||
)
|
||||
}
|
||||
|
||||
export const Dice: FC<DiceProps> = (
|
||||
{
|
||||
className,
|
||||
value,
|
||||
onClick,
|
||||
}) => {
|
||||
interface DiceProps extends ComponentProps {
|
||||
value?: number
|
||||
onClick?: (value: number) => void
|
||||
}
|
||||
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||
export const Dice: FC<DiceProps> = ({ className, value, onClick }) => {
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
||||
|
||||
function handleClick() {
|
||||
if (onClick && value) {
|
||||
onClick(value);
|
||||
onClick(value)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className={`text-2xl bg-gray-400 px-4 m-1 ${className}`}
|
||||
disabled={!thisPlayer?.isTurn()}
|
||||
onClick={handleClick}>
|
||||
<Button
|
||||
className={`text-2xl bg-gray-400 px-4 m-1 ${className}`}
|
||||
disabled={!thisPlayer?.isTurn()}
|
||||
onClick={handleClick}>
|
||||
{value?.toString()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,24 +1,17 @@
|
||||
import React, {forwardRef, ReactEventHandler} from "react";
|
||||
import React, { forwardRef, ReactEventHandler } from "react"
|
||||
|
||||
export interface DropdownProps extends ComponentProps {
|
||||
options?: string[],
|
||||
onSelect?: ReactEventHandler<HTMLSelectElement>,
|
||||
options?: string[]
|
||||
onSelect?: ReactEventHandler<HTMLSelectElement>
|
||||
}
|
||||
|
||||
const Dropdown: FRComponent<DropdownProps, HTMLSelectElement> = forwardRef((
|
||||
{
|
||||
className,
|
||||
options,
|
||||
onSelect
|
||||
}, ref) => (
|
||||
<select ref={ref} className={"border-2 border-gray-300 rounded-md py-1 px-2 bg-white " + className}
|
||||
onSelect={onSelect}>
|
||||
{
|
||||
options?.map((option, index) => (
|
||||
<option key={index}>{option}</option>
|
||||
))
|
||||
}
|
||||
const Dropdown: FRComponent<DropdownProps, HTMLSelectElement> = forwardRef(({ className, options, onSelect }, ref) => (
|
||||
<select
|
||||
ref={ref}
|
||||
className={"border-2 border-gray-300 rounded-md py-1 px-2 bg-white " + className}
|
||||
onSelect={onSelect}>
|
||||
{options?.map((option, index) => <option key={index}>{option}</option>)}
|
||||
</select>
|
||||
));
|
||||
))
|
||||
|
||||
export default Dropdown;
|
||||
export default Dropdown
|
||||
|
@ -1,147 +1,138 @@
|
||||
import React, {FC, Fragment, useEffect, useState} from "react";
|
||||
import {Character} from "../game/character";
|
||||
import findPossiblePositions from "../game/possibleMovesAlgorithm";
|
||||
import {GameTile} from "./gameTile";
|
||||
import {TileType} from "../game/tileType";
|
||||
import {atom, useAtom, useAtomValue, useSetAtom} from "jotai";
|
||||
import {allCharactersAtom, currentPlayerAtom, playersAtom, selectedDiceAtom} from "../utils/state";
|
||||
import {Dialog, Transition} from "@headlessui/react";
|
||||
import React, { FC, Fragment, useEffect, useState } from "react"
|
||||
import { Character } from "../game/character"
|
||||
import findPossiblePositions from "../game/possibleMovesAlgorithm"
|
||||
import { GameTile } from "./gameTile"
|
||||
import { TileType } from "../game/tileType"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { allCharactersAtom, currentPlayerAtom, playersAtom, selectedDiceAtom } from "../utils/state"
|
||||
import { Dialog, Transition } from "@headlessui/react"
|
||||
|
||||
interface BoardProps extends ComponentProps {
|
||||
onMove?: Action<Position[]>,
|
||||
onMove?: Action<Position[]>
|
||||
map: GameMap
|
||||
}
|
||||
|
||||
const modalOpenAtom = atom(false);
|
||||
const modalOpenAtom = atom(false)
|
||||
|
||||
const Board: FC<BoardProps> = (
|
||||
{
|
||||
className,
|
||||
onMove,
|
||||
map
|
||||
}) => {
|
||||
|
||||
const currentPlayer = useAtomValue(currentPlayerAtom);
|
||||
const characters = useAtomValue(allCharactersAtom);
|
||||
const selectedDice = useAtomValue(selectedDiceAtom);
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<Character>();
|
||||
const [possiblePositions, setPossiblePositions] = useState<Path[]>([]);
|
||||
const [hoveredPosition, setHoveredPosition] = useState<Path>();
|
||||
const setModalOpen = useSetAtom(modalOpenAtom);
|
||||
const Board: FC<BoardProps> = ({ className, onMove, map }) => {
|
||||
const currentPlayer = useAtomValue(currentPlayerAtom)
|
||||
const characters = useAtomValue(allCharactersAtom)
|
||||
const selectedDice = useAtomValue(selectedDiceAtom)
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<Character>()
|
||||
const [possiblePositions, setPossiblePositions] = useState<Path[]>([])
|
||||
const [hoveredPosition, setHoveredPosition] = useState<Path>()
|
||||
const setModalOpen = useSetAtom(modalOpenAtom)
|
||||
|
||||
function handleSelectCharacter(character: Character): void {
|
||||
if (character.isPacMan() && currentPlayer?.pacMan.colour !== character.colour) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
setSelectedCharacter(character);
|
||||
setSelectedCharacter(character)
|
||||
}
|
||||
|
||||
function handleShowPath(path: Path): void {
|
||||
setHoveredPosition(path);
|
||||
setHoveredPosition(path)
|
||||
}
|
||||
|
||||
function handleMoveCharacter(destination: Path): void {
|
||||
if (selectedCharacter) {
|
||||
setHoveredPosition(undefined);
|
||||
setHoveredPosition(undefined)
|
||||
|
||||
if (selectedCharacter.isGhost()) {
|
||||
tryMovePacManToSpawn(destination);
|
||||
tryMovePacManToSpawn(destination)
|
||||
}
|
||||
|
||||
selectedCharacter.follow(destination);
|
||||
selectedCharacter.follow(destination)
|
||||
|
||||
const positions = pickUpPellets(destination);
|
||||
onMove?.(positions);
|
||||
setSelectedCharacter(undefined);
|
||||
const positions = pickUpPellets(destination)
|
||||
onMove?.(positions)
|
||||
setSelectedCharacter(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function tryMovePacManToSpawn(destination: Path): void {
|
||||
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.end));
|
||||
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.end))
|
||||
if (takenChar) {
|
||||
takenChar.moveToSpawn();
|
||||
stealFromPlayer();
|
||||
takenChar.moveToSpawn()
|
||||
stealFromPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
function stealFromPlayer(): void {
|
||||
setModalOpen(true);
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
function pickUpPellets(destination: Path): Position[] {
|
||||
const positions: Position[] = [];
|
||||
const positions: Position[] = []
|
||||
if (selectedCharacter?.isPacMan()) {
|
||||
|
||||
for (const tile of [...destination.path ?? [], destination.end]) {
|
||||
const currentTile = map[tile.y][tile.x];
|
||||
for (const tile of [...(destination.path ?? []), destination.end]) {
|
||||
const currentTile = map[tile.y][tile.x]
|
||||
|
||||
function updateTileAndPlayerBox(isPowerPellet = false): void {
|
||||
if (isPowerPellet) {
|
||||
currentPlayer?.addPowerPellet();
|
||||
currentPlayer?.addPowerPellet()
|
||||
} else {
|
||||
currentPlayer?.addPellet();
|
||||
currentPlayer?.addPellet()
|
||||
}
|
||||
map[tile.y][tile.x] = TileType.empty;
|
||||
positions.push(tile);
|
||||
map[tile.y][tile.x] = TileType.empty
|
||||
positions.push(tile)
|
||||
}
|
||||
|
||||
if (currentTile === TileType.pellet) {
|
||||
updateTileAndPlayerBox();
|
||||
updateTileAndPlayerBox()
|
||||
} else if (currentTile === TileType.powerPellet) {
|
||||
updateTileAndPlayerBox(true);
|
||||
updateTileAndPlayerBox(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
return positions
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCharacter && selectedDice) {
|
||||
const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value, characters);
|
||||
setPossiblePositions(possiblePaths);
|
||||
const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value, characters)
|
||||
setPossiblePositions(possiblePaths)
|
||||
} else {
|
||||
setPossiblePositions([]);
|
||||
setPossiblePositions([])
|
||||
}
|
||||
}, [selectedCharacter, selectedDice]);
|
||||
}, [selectedCharacter, selectedDice])
|
||||
|
||||
return (
|
||||
<div className={`w-fit ${className}`}>
|
||||
<SelectPlayerModal/>
|
||||
{
|
||||
map.map((row, rowIndex) =>
|
||||
<div key={rowIndex} className={"flex"}>
|
||||
{
|
||||
row.map((tile, colIndex) =>
|
||||
<GameTile
|
||||
key={colIndex + rowIndex * colIndex}
|
||||
type={tile}
|
||||
possiblePath={possiblePositions.find(p => p.end.x === colIndex && p.end.y === rowIndex)}
|
||||
character={characters.find(c => c.isAt({x: colIndex, y: rowIndex}))}
|
||||
isSelected={selectedCharacter?.isAt({x: colIndex, y: rowIndex})}
|
||||
showPath={hoveredPosition?.path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined}
|
||||
handleMoveCharacter={handleMoveCharacter}
|
||||
handleSelectCharacter={handleSelectCharacter}
|
||||
handleStartShowPath={handleShowPath}
|
||||
handleStopShowPath={() => setHoveredPosition(undefined)}/>
|
||||
)
|
||||
}
|
||||
</div>)
|
||||
}
|
||||
<SelectPlayerModal />
|
||||
{map.map((row, rowIndex) => (
|
||||
<div key={rowIndex} className={"flex"}>
|
||||
{row.map((tile, colIndex) => (
|
||||
<GameTile
|
||||
key={colIndex + rowIndex * colIndex}
|
||||
type={tile}
|
||||
possiblePath={possiblePositions.find(p => p.end.x === colIndex && p.end.y === rowIndex)}
|
||||
character={characters.find(c => c.isAt({ x: colIndex, y: rowIndex }))}
|
||||
isSelected={selectedCharacter?.isAt({ x: colIndex, y: rowIndex })}
|
||||
showPath={hoveredPosition?.path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined}
|
||||
handleMoveCharacter={handleMoveCharacter}
|
||||
handleSelectCharacter={handleSelectCharacter}
|
||||
handleStartShowPath={handleShowPath}
|
||||
handleStopShowPath={() => setHoveredPosition(undefined)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Board;
|
||||
export default Board
|
||||
|
||||
const SelectPlayerModal: FC = () => {
|
||||
const [isOpen, setIsOpen] = useAtom(modalOpenAtom);
|
||||
const currentPlayer = useAtomValue(currentPlayerAtom);
|
||||
const allPlayers = useAtomValue(playersAtom).filter(p => p !== currentPlayer);
|
||||
const [isOpen, setIsOpen] = useAtom(modalOpenAtom)
|
||||
const currentPlayer = useAtomValue(currentPlayerAtom)
|
||||
const allPlayers = useAtomValue(playersAtom).filter(p => p !== currentPlayer)
|
||||
|
||||
if (currentPlayer === undefined) return null;
|
||||
if (currentPlayer === undefined) return null
|
||||
|
||||
function close(): void {
|
||||
setIsOpen(false);
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -155,9 +146,8 @@ const SelectPlayerModal: FC = () => {
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25"/>
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
@ -169,10 +159,8 @@ const SelectPlayerModal: FC = () => {
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
Steal from player
|
||||
</Dialog.Title>
|
||||
@ -182,29 +170,29 @@ const SelectPlayerModal: FC = () => {
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
|
||||
{
|
||||
allPlayers.map(player =>
|
||||
<div key={player.username} className={"border-b pb-1"}>
|
||||
<span className={"mx-2"}>{player.username} has {player.box.pellets} pellets</span>
|
||||
<button className={"text-blue-500 enabled:cursor-pointer disabled:text-gray-500"}
|
||||
style={{background: "none"}}
|
||||
disabled={player.box.pellets === 0}
|
||||
onClick={() => {
|
||||
currentPlayer?.stealFrom(player);
|
||||
close();
|
||||
}}>
|
||||
Steal
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{allPlayers.map(player => (
|
||||
<div key={player.username} className={"border-b pb-1"}>
|
||||
<span className={"mx-2"}>
|
||||
{player.username} has {player.box.pellets} pellets
|
||||
</span>
|
||||
<button
|
||||
className={"text-blue-500 enabled:cursor-pointer disabled:text-gray-500"}
|
||||
style={{ background: "none" }}
|
||||
disabled={player.box.pellets === 0}
|
||||
onClick={() => {
|
||||
currentPlayer?.stealFrom(player)
|
||||
close()
|
||||
}}>
|
||||
Steal
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={close}
|
||||
>
|
||||
onClick={close}>
|
||||
Don't steal from anyone
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,33 +1,36 @@
|
||||
import React, {FC, MouseEventHandler} from "react";
|
||||
import {State} from "../game/player";
|
||||
import {currentPlayerAtom, playersAtom, rollDiceButtonAtom, thisPlayerAtom} from "../utils/state";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {Button} from "./button";
|
||||
import rules from "../game/rules";
|
||||
import React, { FC, MouseEventHandler } from "react"
|
||||
import { State } from "../game/player"
|
||||
import { currentPlayerAtom, playersAtom, rollDiceButtonAtom, thisPlayerAtom } from "../utils/state"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { Button } from "./button"
|
||||
import rules from "../game/rules"
|
||||
|
||||
interface GameButtonProps extends ComponentProps {
|
||||
onReadyClick?: MouseEventHandler,
|
||||
onReadyClick?: MouseEventHandler
|
||||
onRollDiceClick?: MouseEventHandler
|
||||
}
|
||||
|
||||
const GameButton: FC<GameButtonProps> = (
|
||||
{
|
||||
onReadyClick,
|
||||
onRollDiceClick,
|
||||
}) => {
|
||||
const GameButton: FC<GameButtonProps> = ({ onReadyClick, onRollDiceClick }) => {
|
||||
const currentPlayer = useAtomValue(currentPlayerAtom)
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
||||
const players = useAtomValue(playersAtom)
|
||||
const activeRollDiceButton = useAtomValue(rollDiceButtonAtom)
|
||||
|
||||
const currentPlayer = useAtomValue(currentPlayerAtom);
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||
const players = useAtomValue(playersAtom);
|
||||
const activeRollDiceButton = useAtomValue(rollDiceButtonAtom);
|
||||
|
||||
if (players.length >= rules.minPlayers && (currentPlayer === undefined || currentPlayer.state === State.waitingForPlayers)) {
|
||||
return <Button onClick={onReadyClick}>Ready</Button>;
|
||||
if (
|
||||
players.length >= rules.minPlayers &&
|
||||
(currentPlayer === undefined || currentPlayer.state === State.waitingForPlayers)
|
||||
) {
|
||||
return <Button onClick={onReadyClick}>Ready</Button>
|
||||
}
|
||||
if (!thisPlayer?.isTurn()) { // TODO also show when waiting for other players
|
||||
return <Button disabled>Please wait</Button>;
|
||||
if (!thisPlayer?.isTurn()) {
|
||||
// TODO also show when waiting for other players
|
||||
return <Button disabled>Please wait</Button>
|
||||
}
|
||||
return <Button onClick={onRollDiceClick} disabled={!activeRollDiceButton}>Roll dice</Button>;
|
||||
};
|
||||
return (
|
||||
<Button onClick={onRollDiceClick} disabled={!activeRollDiceButton}>
|
||||
Roll dice
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default GameButton;
|
||||
export default GameButton
|
||||
|
@ -1,39 +1,38 @@
|
||||
import React, {FC, useEffect} from "react";
|
||||
import {AllDice} from "./dice";
|
||||
import {doAction, GameAction} from "../utils/actions";
|
||||
import GameBoard from "./gameBoard";
|
||||
import WebSocketService from "../websockets/WebSocketService";
|
||||
import Player from "../game/player";
|
||||
import PlayerStats from "../components/playerStats";
|
||||
import {useAtom, useAtomValue, useSetAtom} from "jotai";
|
||||
import {diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom} from "../utils/state";
|
||||
import GameButton from "./gameButton";
|
||||
import {Button} from "./button";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
import {getData} from "../utils/api";
|
||||
import React, { FC, useEffect } from "react"
|
||||
import { AllDice } from "./dice"
|
||||
import { doAction, GameAction } from "../utils/actions"
|
||||
import GameBoard from "./gameBoard"
|
||||
import WebSocketService from "../websockets/WebSocketService"
|
||||
import Player from "../game/player"
|
||||
import PlayerStats from "../components/playerStats"
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom } from "../utils/state"
|
||||
import GameButton from "./gameButton"
|
||||
import { Button } from "./button"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
import { getData } from "../utils/api"
|
||||
|
||||
const wsService = new WebSocketService(import.meta.env.VITE_API_WS);
|
||||
const wsService = new WebSocketService(import.meta.env.VITE_API_WS)
|
||||
|
||||
export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map}) => {
|
||||
export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, map }) => {
|
||||
const players = useAtomValue(playersAtom)
|
||||
const dice = useAtomValue(diceAtom)
|
||||
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom)
|
||||
const setActiveRollDiceButton = useSetAtom(rollDiceButtonAtom)
|
||||
const ghosts = useAtomValue(ghostsAtom)
|
||||
|
||||
const players = useAtomValue(playersAtom);
|
||||
const dice = useAtomValue(diceAtom);
|
||||
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
|
||||
const setActiveRollDiceButton = useSetAtom(rollDiceButtonAtom);
|
||||
const ghosts = useAtomValue(ghostsAtom);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {id} = useParams();
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams()
|
||||
|
||||
/**
|
||||
* Rolls the dice for the current player's turn.
|
||||
*/
|
||||
function rollDice(): void {
|
||||
if (!player.isTurn()) return;
|
||||
if (!player.isTurn()) return
|
||||
|
||||
setSelectedDice(undefined);
|
||||
wsService.send({action: GameAction.rollDice});
|
||||
setActiveRollDiceButton(false);
|
||||
setSelectedDice(undefined)
|
||||
wsService.send({ action: GameAction.rollDice })
|
||||
setActiveRollDiceButton(false)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,22 +41,22 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map
|
||||
*/
|
||||
function onCharacterMove(eatenPellets: Position[]): void {
|
||||
if (dice && selectedDice) {
|
||||
dice.splice(selectedDice.index, 1);
|
||||
dice.splice(selectedDice.index, 1)
|
||||
}
|
||||
setSelectedDice(undefined);
|
||||
setSelectedDice(undefined)
|
||||
const data: ActionMessage = {
|
||||
action: GameAction.moveCharacter,
|
||||
data: {
|
||||
dice: dice?.length ?? 0 > 0 ? dice : null,
|
||||
players: players,
|
||||
ghosts: ghosts,
|
||||
eatenPellets: eatenPellets
|
||||
}
|
||||
};
|
||||
wsService.send(data);
|
||||
eatenPellets: eatenPellets,
|
||||
},
|
||||
}
|
||||
wsService.send(data)
|
||||
|
||||
if (dice?.length === 0) {
|
||||
endTurn();
|
||||
endTurn()
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,15 +69,15 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map
|
||||
data: {
|
||||
username: player.username,
|
||||
gameId: id,
|
||||
} as JoinGameData
|
||||
});
|
||||
} as JoinGameData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ready action to the WebSocket service.
|
||||
*/
|
||||
function sendReady(): void {
|
||||
wsService.send({action: GameAction.ready});
|
||||
wsService.send({ action: GameAction.ready })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,44 +85,40 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map
|
||||
* to advance to the next player in the game.
|
||||
*/
|
||||
function endTurn(): void {
|
||||
wsService.send({action: GameAction.nextPlayer});
|
||||
wsService.send({ action: GameAction.nextPlayer })
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the current game and navigates to the lobby.
|
||||
*/
|
||||
function leaveGame(): void {
|
||||
wsService.send({action: GameAction.disconnect});
|
||||
navigate("/lobby");
|
||||
wsService.send({ action: GameAction.disconnect })
|
||||
navigate("/lobby")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getData(`/game/exists/${id}`).then(res => {
|
||||
if (!res.ok) {
|
||||
return navigate("/lobby")
|
||||
}
|
||||
wsService.onReceive = doAction
|
||||
wsService.open()
|
||||
|
||||
getData(`/game/exists/${id}`)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
return navigate("/lobby");
|
||||
}
|
||||
wsService.onReceive = doAction;
|
||||
wsService.open();
|
||||
wsService.waitForOpen().then(() => joinGame())
|
||||
})
|
||||
|
||||
wsService.waitForOpen().then(() => joinGame());
|
||||
})
|
||||
|
||||
return () => wsService.close();
|
||||
}, []);
|
||||
return () => wsService.close()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={leaveGame}>Leave game</Button>
|
||||
<div className={"flex justify-center"}>
|
||||
{players?.map(p => <PlayerStats key={p.username} player={p}/>)}
|
||||
</div>
|
||||
<div className={"flex justify-center"}>{players?.map(p => <PlayerStats key={p.username} player={p} />)}</div>
|
||||
<div className={"flex-center"}>
|
||||
<GameButton onReadyClick={sendReady} onRollDiceClick={rollDice}/>
|
||||
<GameButton onReadyClick={sendReady} onRollDiceClick={rollDice} />
|
||||
</div>
|
||||
<AllDice values={dice}/>
|
||||
<GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={map}/>
|
||||
<AllDice values={dice} />
|
||||
<GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={map} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,165 +1,151 @@
|
||||
import React, {FC, useEffect, useState} from "react";
|
||||
import {TileType} from "../game/tileType";
|
||||
import {Character, Dummy} from "../game/character";
|
||||
import {Direction} from "../game/direction";
|
||||
import {Colour} from "../game/colour";
|
||||
import React, { FC, useEffect, useState } from "react"
|
||||
import { TileType } from "../game/tileType"
|
||||
import { Character, Dummy } from "../game/character"
|
||||
import { Direction } from "../game/direction"
|
||||
import { Colour } from "../game/colour"
|
||||
|
||||
interface TileWithCharacterProps extends ComponentProps {
|
||||
possiblePath?: Path,
|
||||
character?: Character,
|
||||
type?: TileType,
|
||||
handleMoveCharacter?: Action<Path>,
|
||||
handleSelectCharacter?: Action<Character>,
|
||||
handleStartShowPath?: Action<Path>,
|
||||
handleStopShowPath?: VoidFunction,
|
||||
isSelected?: boolean,
|
||||
possiblePath?: Path
|
||||
character?: Character
|
||||
type?: TileType
|
||||
handleMoveCharacter?: Action<Path>
|
||||
handleSelectCharacter?: Action<Character>
|
||||
handleStartShowPath?: Action<Path>
|
||||
handleStopShowPath?: VoidFunction
|
||||
isSelected?: boolean
|
||||
showPath?: boolean
|
||||
}
|
||||
|
||||
export const GameTile: FC<TileWithCharacterProps> = (
|
||||
{
|
||||
possiblePath,
|
||||
character,
|
||||
type,
|
||||
handleMoveCharacter,
|
||||
handleSelectCharacter,
|
||||
handleStartShowPath,
|
||||
handleStopShowPath,
|
||||
isSelected = false,
|
||||
showPath = false
|
||||
}) => (
|
||||
<Tile className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
|
||||
type={type}
|
||||
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
|
||||
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
|
||||
onMouseLeave={handleStopShowPath}>
|
||||
export const GameTile: FC<TileWithCharacterProps> = ({
|
||||
possiblePath,
|
||||
character,
|
||||
type,
|
||||
handleMoveCharacter,
|
||||
handleSelectCharacter,
|
||||
handleStartShowPath,
|
||||
handleStopShowPath,
|
||||
isSelected = false,
|
||||
showPath = false,
|
||||
}) => (
|
||||
<Tile
|
||||
className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
|
||||
type={type}
|
||||
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
|
||||
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
|
||||
onMouseLeave={handleStopShowPath}>
|
||||
<>
|
||||
{character &&
|
||||
<div className={"flex-center wh-full"}>
|
||||
<CharacterComponent
|
||||
character={character}
|
||||
onClick={handleSelectCharacter}
|
||||
className={isSelected ? "animate-bounce" : ""}/>
|
||||
</div>
|
||||
}
|
||||
{showPath && <Circle/>}
|
||||
<AddDummy path={possiblePath}/>
|
||||
{character && (
|
||||
<div className={"flex-center wh-full"}>
|
||||
<CharacterComponent
|
||||
character={character}
|
||||
onClick={handleSelectCharacter}
|
||||
className={isSelected ? "animate-bounce" : ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showPath && <Circle />}
|
||||
<AddDummy path={possiblePath} />
|
||||
</>
|
||||
</Tile>
|
||||
);
|
||||
)
|
||||
|
||||
const Circle: FC<{ colour?: Colour } & ComponentProps> = ({colour = Colour.white, className}) => (
|
||||
const Circle: FC<{ colour?: Colour } & ComponentProps> = ({ colour = Colour.white, className }) => (
|
||||
<div className={`flex-center w-full h-full ${className}`}>
|
||||
<div className={`w-1/2 h-1/2 rounded-full`}
|
||||
style={{backgroundColor: colour}}/>
|
||||
<div className={`w-1/2 h-1/2 rounded-full`} style={{ backgroundColor: colour }} />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
|
||||
interface TileProps extends ChildProps {
|
||||
type?: TileType,
|
||||
onClick?: VoidFunction,
|
||||
onMouseEnter?: VoidFunction,
|
||||
onMouseLeave?: VoidFunction,
|
||||
character?: Character,
|
||||
onCharacterClick?: Action<Character>,
|
||||
characterClass?: string,
|
||||
type?: TileType
|
||||
onClick?: VoidFunction
|
||||
onMouseEnter?: VoidFunction
|
||||
onMouseLeave?: VoidFunction
|
||||
character?: Character
|
||||
onCharacterClick?: Action<Character>
|
||||
characterClass?: string
|
||||
}
|
||||
|
||||
const Tile: FC<TileProps> = (
|
||||
{
|
||||
type = TileType.empty,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
className,
|
||||
children
|
||||
}) => {
|
||||
|
||||
const [tileSize, setTileSize] = useState(2);
|
||||
const Tile: FC<TileProps> = ({ type = TileType.empty, onClick, onMouseEnter, onMouseLeave, className, children }) => {
|
||||
const [tileSize, setTileSize] = useState(2)
|
||||
|
||||
function setColor(): string {
|
||||
switch (type) {
|
||||
case TileType.wall:
|
||||
return "bg-blue-500";
|
||||
return "bg-blue-500"
|
||||
case TileType.ghostSpawn:
|
||||
return "bg-red-500";
|
||||
return "bg-red-500"
|
||||
case TileType.pacmanSpawn:
|
||||
return "bg-green-500"; // TODO should be the colour of the player
|
||||
return "bg-green-500" // TODO should be the colour of the player
|
||||
default:
|
||||
return "bg-black";
|
||||
return "bg-black"
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
function handleResize(): void {
|
||||
const newSize = Math.floor(window.innerWidth / 16);
|
||||
setTileSize(newSize);
|
||||
const newSize = Math.floor(window.innerWidth / 16)
|
||||
setTileSize(newSize)
|
||||
}
|
||||
|
||||
handleResize();
|
||||
handleResize()
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
window.addEventListener("resize", handleResize)
|
||||
return () => window.removeEventListener("resize", handleResize)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`${setColor()} hover:border relative max-w-[75px] max-h-[75px] ${className}`}
|
||||
style={{width: `${tileSize}px`, height: `${tileSize}px`}}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
<div
|
||||
className={`${setColor()} hover:border relative max-w-[75px] max-h-[75px] ${className}`}
|
||||
style={{ width: `${tileSize}px`, height: `${tileSize}px` }}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
{children}
|
||||
{type === TileType.pellet && <Circle colour={Colour.yellow}/>}
|
||||
{type === TileType.powerPellet && <Circle colour={Colour.red}/>}
|
||||
{type === TileType.pellet && <Circle colour={Colour.yellow} />}
|
||||
{type === TileType.powerPellet && <Circle colour={Colour.red} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AddDummy: FC<{ path?: Path } & ComponentProps> = ({path}) => (
|
||||
<>
|
||||
{path &&
|
||||
<div className={"flex-center wh-full"}>
|
||||
<CharacterComponent character={new Dummy(path)}/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
interface CharacterComponentProps extends ComponentProps {
|
||||
character?: Character,
|
||||
onClick?: Action<Character>,
|
||||
)
|
||||
}
|
||||
|
||||
const CharacterComponent: FC<CharacterComponentProps> = (
|
||||
{
|
||||
character,
|
||||
onClick,
|
||||
className
|
||||
}) => {
|
||||
const AddDummy: FC<{ path?: Path } & ComponentProps> = ({ path }) => (
|
||||
<>
|
||||
{path && (
|
||||
<div className={"flex-center wh-full"}>
|
||||
<CharacterComponent character={new Dummy(path)} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
interface CharacterComponentProps extends ComponentProps {
|
||||
character?: Character
|
||||
onClick?: Action<Character>
|
||||
}
|
||||
|
||||
const CharacterComponent: FC<CharacterComponentProps> = ({ character, onClick, className }) => {
|
||||
function getSide() {
|
||||
switch (character?.position?.direction) {
|
||||
case Direction.up:
|
||||
return "right-1/4 top-0";
|
||||
return "right-1/4 top-0"
|
||||
case Direction.down:
|
||||
return "right-1/4 bottom-0";
|
||||
return "right-1/4 bottom-0"
|
||||
case Direction.left:
|
||||
return "left-0 top-1/4";
|
||||
return "left-0 top-1/4"
|
||||
case Direction.right:
|
||||
return "right-0 top-1/4";
|
||||
return "right-0 top-1/4"
|
||||
}
|
||||
}
|
||||
|
||||
if (character === undefined) return null;
|
||||
if (character === undefined) return null
|
||||
|
||||
return (
|
||||
<div className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`}
|
||||
style={{backgroundColor: `${character.colour}`}}
|
||||
onClick={() => onClick?.(character)}>
|
||||
<div
|
||||
className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`}
|
||||
style={{ backgroundColor: `${character.colour}` }}
|
||||
onClick={() => onClick?.(character)}>
|
||||
<div>
|
||||
<div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`}/>
|
||||
<div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,23 +1,18 @@
|
||||
import React, {forwardRef} from "react";
|
||||
import React, { forwardRef } from "react"
|
||||
|
||||
const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef((
|
||||
{
|
||||
type = "text",
|
||||
className,
|
||||
id,
|
||||
placeholder,
|
||||
required = false,
|
||||
name,
|
||||
autoComplete = "off",
|
||||
}, ref) => (
|
||||
<input type={type}
|
||||
autoComplete={autoComplete}
|
||||
ref={ref}
|
||||
id={id}
|
||||
name={name}
|
||||
className={"border-2 border-gray-300 rounded-md p-1 " + className}
|
||||
placeholder={placeholder}
|
||||
required={required}/>
|
||||
));
|
||||
const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef(
|
||||
({ type = "text", className, id, placeholder, required = false, name, autoComplete = "off" }, ref) => (
|
||||
<input
|
||||
type={type}
|
||||
autoComplete={autoComplete}
|
||||
ref={ref}
|
||||
id={id}
|
||||
name={name}
|
||||
className={"border-2 border-gray-300 rounded-md p-1 " + className}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
/>
|
||||
),
|
||||
)
|
||||
|
||||
export default Input;
|
||||
export default Input
|
||||
|
@ -1,13 +1,11 @@
|
||||
import React, {FC} from "react";
|
||||
import NavMenu from "./navMenu";
|
||||
import React, { FC } from "react"
|
||||
import NavMenu from "./navMenu"
|
||||
|
||||
const Layout: FC<ChildProps> = ({children}) => (
|
||||
const Layout: FC<ChildProps> = ({ children }) => (
|
||||
<div>
|
||||
<NavMenu/>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<NavMenu />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
|
||||
export default Layout;
|
||||
export default Layout
|
||||
|
@ -1,84 +1,89 @@
|
||||
import React, {FC, useEffect} from "react";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {useAtom, useAtomValue} from "jotai";
|
||||
import {thisPlayerAtom} from "../utils/state";
|
||||
import {UserCircleIcon} from "@heroicons/react/24/outline";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import React, { FC, useEffect } from "react"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { useAtom, useAtomValue } from "jotai"
|
||||
import { thisPlayerAtom } from "../utils/state"
|
||||
import { UserCircleIcon } from "@heroicons/react/24/outline"
|
||||
import useToggle from "../hooks/useToggle"
|
||||
|
||||
const NavMenu: FC = () => {
|
||||
const player = useAtomValue(thisPlayerAtom);
|
||||
const player = useAtomValue(thisPlayerAtom)
|
||||
|
||||
return (
|
||||
<header className={"z-10"}>
|
||||
<nav className="mb-3 flex justify-between border-b-2">
|
||||
<Link to="/"><h2 className={"m-1"}>Pac-Man Board Game</h2></Link>
|
||||
<Link to="/">
|
||||
<h2 className={"m-1"}>Pac-Man Board Game</h2>
|
||||
</Link>
|
||||
|
||||
<ul className="inline-flex gap-2 items-center mr-5 relative">
|
||||
<NavItem to="/">Home</NavItem>
|
||||
{
|
||||
player === undefined ?
|
||||
<NavItem className={"mx-2"} to={"/login"}>Login</NavItem>
|
||||
:
|
||||
<>
|
||||
<NavItem to={"/lobby"}>Lobby</NavItem>
|
||||
<ProfileDropdown className={"mx-2"}/>
|
||||
</>
|
||||
}
|
||||
{player === undefined ? (
|
||||
<NavItem className={"mx-2"} to={"/login"}>
|
||||
Login
|
||||
</NavItem>
|
||||
) : (
|
||||
<>
|
||||
<NavItem to={"/lobby"}>Lobby</NavItem>
|
||||
<ProfileDropdown className={"mx-2"} />
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default NavMenu;
|
||||
export default NavMenu
|
||||
|
||||
const NavItem: FC<LinkProps> = ({to, children, className}) => (
|
||||
const NavItem: FC<LinkProps> = ({ to, children, className }) => (
|
||||
<li>
|
||||
<Link className={`hover:underline ${className}`} to={to}>{children}</Link>
|
||||
<Link className={`hover:underline ${className}`} to={to}>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
)
|
||||
|
||||
const ProfileDropdown: FC<ComponentProps> = ({className}) => {
|
||||
const [player, setPlayer] = useAtom(thisPlayerAtom);
|
||||
const [isOpened, toggleOpen] = useToggle();
|
||||
const navigate = useNavigate();
|
||||
const ProfileDropdown: FC<ComponentProps> = ({ className }) => {
|
||||
const [player, setPlayer] = useAtom(thisPlayerAtom)
|
||||
const [isOpened, toggleOpen] = useToggle()
|
||||
const navigate = useNavigate()
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
setPlayer(undefined);
|
||||
navigate("/login");
|
||||
setPlayer(undefined)
|
||||
navigate("/login")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (isOpened) {
|
||||
function closeIfOutsideButton(e: MouseEvent): void {
|
||||
if (isOpened && e.target instanceof HTMLElement) {
|
||||
if (e.target.closest("#profile-dropdown") === null) {
|
||||
toggleOpen(false);
|
||||
toggleOpen(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", closeIfOutsideButton);
|
||||
return () => document.removeEventListener("click", closeIfOutsideButton);
|
||||
document.addEventListener("click", closeIfOutsideButton)
|
||||
return () => document.removeEventListener("click", closeIfOutsideButton)
|
||||
}
|
||||
|
||||
}, [isOpened]);
|
||||
}, [isOpened])
|
||||
|
||||
return (
|
||||
<>
|
||||
<li id={"profile-dropdown"}
|
||||
className={`inline-flex-center cursor-pointer hover:bg-gray-100 h-full px-2 ${className}`}
|
||||
onClick={() => toggleOpen()}>
|
||||
<UserCircleIcon className={"w-7"}/>
|
||||
<li
|
||||
id={"profile-dropdown"}
|
||||
className={`inline-flex-center cursor-pointer hover:bg-gray-100 h-full px-2 ${className}`}
|
||||
onClick={() => toggleOpen()}>
|
||||
<UserCircleIcon className={"w-7"} />
|
||||
<span>{player?.username}</span>
|
||||
</li>
|
||||
{
|
||||
isOpened &&
|
||||
<div className={"absolute right-2 border rounded-b -bottom-9 px-5"}>
|
||||
<button onClick={logout} className={"hover:underline py-1"}>Logout</button>
|
||||
</div>
|
||||
}
|
||||
{isOpened && (
|
||||
<div className={"absolute right-2 border rounded-b -bottom-9 px-5"}>
|
||||
<button onClick={logout} className={"hover:underline py-1"}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,29 +1,27 @@
|
||||
import React, {FC} from "react";
|
||||
import Player, {State} from "../game/player";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {currentPlayerNameAtom} from "../utils/state";
|
||||
import React, { FC } from "react"
|
||||
import Player, { State } from "../game/player"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { currentPlayerNameAtom } from "../utils/state"
|
||||
|
||||
const PlayerStats: FC<{ player: Player } & ComponentProps> = (
|
||||
{
|
||||
player,
|
||||
className,
|
||||
id
|
||||
}) => {
|
||||
const currentPlayerName = useAtomValue(currentPlayerNameAtom);
|
||||
const PlayerStats: FC<{ player: Player } & ComponentProps> = ({ player, className, id }) => {
|
||||
const currentPlayerName = useAtomValue(currentPlayerNameAtom)
|
||||
return (
|
||||
<div key={player.colour}
|
||||
className={`w-fit m-2 ${player.state === State.disconnected ? "text-gray-500" : ""} ${className}`} id={id}>
|
||||
<div
|
||||
key={player.colour}
|
||||
className={`w-fit m-2 ${player.state === State.disconnected ? "text-gray-500" : ""} ${className}`}
|
||||
id={id}>
|
||||
<p className={player.username === currentPlayerName ? "underline" : ""}>Player: {player.username}</p>
|
||||
<p>Colour: {player.colour}</p>
|
||||
{player.state === State.inGame || player.state === State.disconnected ?
|
||||
{player.state === State.inGame || player.state === State.disconnected ? (
|
||||
<>
|
||||
<p>Pellets: {player.box.pellets}</p>
|
||||
<p>PowerPellets: {player.box.powerPellets}</p>
|
||||
</>
|
||||
:
|
||||
<p>{player.state === State.waitingForPlayers ? "Waiting" : "Ready"}</p>}
|
||||
) : (
|
||||
<p>{player.state === State.waitingForPlayers ? "Waiting" : "Ready"}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerStats;
|
||||
export default PlayerStats
|
||||
|
@ -1,32 +1,31 @@
|
||||
export default class Box implements BoxProps {
|
||||
public readonly colour;
|
||||
public pellets;
|
||||
public powerPellets;
|
||||
public readonly colour
|
||||
public pellets
|
||||
public powerPellets
|
||||
|
||||
public constructor({colour, pellets = 0, powerPellets = 0}: BoxProps) {
|
||||
this.colour = colour;
|
||||
this.pellets = pellets;
|
||||
this.powerPellets = powerPellets;
|
||||
public constructor({ colour, pellets = 0, powerPellets = 0 }: BoxProps) {
|
||||
this.colour = colour
|
||||
this.pellets = pellets
|
||||
this.powerPellets = powerPellets
|
||||
}
|
||||
|
||||
public addPellet(): void {
|
||||
this.pellets++;
|
||||
this.pellets++
|
||||
}
|
||||
|
||||
public removePellet(): boolean {
|
||||
if (this.pellets <= 0) return false;
|
||||
this.pellets--;
|
||||
return true;
|
||||
if (this.pellets <= 0) return false
|
||||
this.pellets--
|
||||
return true
|
||||
}
|
||||
|
||||
public addPowerPellet(): void {
|
||||
this.powerPellets++;
|
||||
this.powerPellets++
|
||||
}
|
||||
|
||||
public removePowerPellet(): boolean {
|
||||
if (this.powerPellets <= 0) return false;
|
||||
this.powerPellets--;
|
||||
return true;
|
||||
if (this.powerPellets <= 0) return false
|
||||
this.powerPellets--
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Direction} from "./direction";
|
||||
import {Colour} from "./colour";
|
||||
import { Direction } from "./direction"
|
||||
import { Colour } from "./colour"
|
||||
|
||||
export enum CharacterType {
|
||||
pacMan,
|
||||
@ -8,89 +8,106 @@ export enum CharacterType {
|
||||
}
|
||||
|
||||
export class Character implements CharacterProps {
|
||||
public readonly colour;
|
||||
public position;
|
||||
public isEatable;
|
||||
public readonly spawnPosition;
|
||||
public readonly type;
|
||||
public readonly colour
|
||||
public position
|
||||
public isEatable
|
||||
public readonly spawnPosition
|
||||
public readonly type
|
||||
|
||||
public constructor(
|
||||
{
|
||||
colour,
|
||||
position = null,
|
||||
type = CharacterType.dummy,
|
||||
isEatable = type === CharacterType.pacMan,
|
||||
spawnPosition = null
|
||||
}: CharacterProps) {
|
||||
this.colour = colour;
|
||||
this.isEatable = isEatable;
|
||||
this.spawnPosition = spawnPosition;
|
||||
public constructor({
|
||||
colour,
|
||||
position = null,
|
||||
type = CharacterType.dummy,
|
||||
isEatable = type === CharacterType.pacMan,
|
||||
spawnPosition = null,
|
||||
}: CharacterProps) {
|
||||
this.colour = colour
|
||||
this.isEatable = isEatable
|
||||
this.spawnPosition = spawnPosition
|
||||
|
||||
if (position) {
|
||||
this.position = position;
|
||||
this.position = position
|
||||
} else {
|
||||
this.position = spawnPosition ? {
|
||||
end: spawnPosition!.at,
|
||||
direction: spawnPosition!.direction
|
||||
} : null;
|
||||
this.position = spawnPosition
|
||||
? {
|
||||
end: spawnPosition!.at,
|
||||
direction: spawnPosition!.direction,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
this.type = type;
|
||||
this.type = type
|
||||
}
|
||||
|
||||
public follow(path: Path): void {
|
||||
if (!this.position) {
|
||||
this.position = path;
|
||||
this.position = path
|
||||
} else {
|
||||
this.position.end = path.end;
|
||||
this.position.direction = path.direction;
|
||||
this.position.path = undefined;
|
||||
this.position.end = path.end
|
||||
this.position.direction = path.direction
|
||||
this.position.path = undefined
|
||||
}
|
||||
}
|
||||
|
||||
public isPacMan(): boolean {
|
||||
return this.type === CharacterType.pacMan;
|
||||
return this.type === CharacterType.pacMan
|
||||
}
|
||||
|
||||
public isGhost(): boolean {
|
||||
return this.type === CharacterType.ghost;
|
||||
return this.type === CharacterType.ghost
|
||||
}
|
||||
|
||||
public moveToSpawn(): void {
|
||||
if (!this.spawnPosition) return;
|
||||
this.follow({end: this.spawnPosition.at, direction: this.spawnPosition.direction});
|
||||
if (!this.spawnPosition) return
|
||||
this.follow({
|
||||
end: this.spawnPosition.at,
|
||||
direction: this.spawnPosition.direction,
|
||||
})
|
||||
}
|
||||
|
||||
public isAt(position: Position): boolean {
|
||||
return this.position !== null && this.position.end.x === position.x && this.position.end.y === position.y;
|
||||
return this.position !== null && this.position.end.x === position.x && this.position.end.y === position.y
|
||||
}
|
||||
}
|
||||
|
||||
export class PacMan extends Character implements CharacterProps {
|
||||
|
||||
public constructor({colour, position, isEatable = true, spawnPosition, type = CharacterType.pacMan}: CharacterProps) {
|
||||
super({colour: colour, position: position, isEatable: isEatable, spawnPosition: spawnPosition, type: type});
|
||||
public constructor({
|
||||
colour,
|
||||
position,
|
||||
isEatable = true,
|
||||
spawnPosition,
|
||||
type = CharacterType.pacMan,
|
||||
}: CharacterProps) {
|
||||
super({
|
||||
colour: colour,
|
||||
position: position,
|
||||
isEatable: isEatable,
|
||||
spawnPosition: spawnPosition,
|
||||
type: type,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Ghost extends Character implements CharacterProps {
|
||||
|
||||
public constructor({colour, position, isEatable, spawnPosition, type = CharacterType.ghost}: CharacterProps) {
|
||||
super({colour: colour, position: position, isEatable: isEatable, spawnPosition: spawnPosition, type: type});
|
||||
public constructor({ colour, position, isEatable, spawnPosition, type = CharacterType.ghost }: CharacterProps) {
|
||||
super({
|
||||
colour: colour,
|
||||
position: position,
|
||||
isEatable: isEatable,
|
||||
spawnPosition: spawnPosition,
|
||||
type: type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Dummy extends Character implements CharacterProps {
|
||||
|
||||
public constructor(position: Path) {
|
||||
super({
|
||||
colour: Colour.grey,
|
||||
position: position,
|
||||
isEatable: false,
|
||||
spawnPosition: {at: {x: 0, y: 0}, direction: Direction.up},
|
||||
spawnPosition: { at: { x: 0, y: 0 }, direction: Direction.up },
|
||||
type: CharacterType.dummy,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,4 +8,4 @@ export enum Colour {
|
||||
grey = "grey",
|
||||
}
|
||||
|
||||
export const getColours = (): Colour[] => Object.values(Colour);
|
||||
export const getColours = (): Colour[] => Object.values(Colour)
|
||||
|
@ -2,8 +2,7 @@ export enum Direction {
|
||||
left,
|
||||
up,
|
||||
right,
|
||||
down
|
||||
down,
|
||||
}
|
||||
|
||||
export const getDirections = () => Object.values(Direction)
|
||||
.filter(d => !isNaN(Number(d))) as Direction[];
|
||||
export const getDirections = () => Object.values(Direction).filter(d => !isNaN(Number(d))) as Direction[]
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {CharacterType} from "./character";
|
||||
import {Direction} from "./direction";
|
||||
import { CharacterType } from "./character"
|
||||
import { Direction } from "./direction"
|
||||
|
||||
/**
|
||||
* 0 = empty
|
||||
@ -25,29 +25,32 @@ export const customMap: GameMap = [
|
||||
[1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1],
|
||||
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
|
||||
];
|
||||
]
|
||||
|
||||
export function getCharacterSpawns(map: GameMap): { type: CharacterType, position: DirectionalPosition }[] {
|
||||
|
||||
const result: { type: CharacterType, position: DirectionalPosition }[] = [];
|
||||
export function getCharacterSpawns(map: GameMap): { type: CharacterType; position: DirectionalPosition }[] {
|
||||
const result: { type: CharacterType; position: DirectionalPosition }[] = []
|
||||
for (let row = 0; row < map.length; row++) {
|
||||
for (let col = 0; col < map.length; col++) {
|
||||
// TODO find direction
|
||||
if (map[row][col] === 4) {
|
||||
result.push({type: CharacterType.ghost, position: {at: {x: col, y: row}, direction: Direction.up}});
|
||||
result.push({
|
||||
type: CharacterType.ghost,
|
||||
position: { at: { x: col, y: row }, direction: Direction.up },
|
||||
})
|
||||
} else if (map[row][col] === 5) {
|
||||
result.push({
|
||||
type: CharacterType.pacMan, position: {at: {x: col, y: row}, direction: Direction.up}
|
||||
});
|
||||
type: CharacterType.pacMan,
|
||||
position: { at: { x: col, y: row }, direction: Direction.up },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
export function getPacManSpawns(map: GameMap): DirectionalPosition[] {
|
||||
return getCharacterSpawns(map)
|
||||
.filter(s => s.type === CharacterType.pacMan)
|
||||
.map(s => s.position)
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +1,57 @@
|
||||
import {Character, CharacterType} from "./character";
|
||||
import Box from "./box";
|
||||
import {getDefaultStore} from "jotai";
|
||||
import {currentPlayerNameAtom, playersAtom} from "../utils/state";
|
||||
import rules from "./rules";
|
||||
import { Character, CharacterType } from "./character"
|
||||
import Box from "./box"
|
||||
import { getDefaultStore } from "jotai"
|
||||
import { currentPlayerNameAtom, playersAtom } from "../utils/state"
|
||||
import rules from "./rules"
|
||||
|
||||
export enum State {
|
||||
waitingForPlayers,
|
||||
ready,
|
||||
inGame,
|
||||
disconnected
|
||||
disconnected,
|
||||
}
|
||||
|
||||
export default class Player implements PlayerProps {
|
||||
private static store = getDefaultStore();
|
||||
public readonly username;
|
||||
public readonly pacMan;
|
||||
public readonly colour;
|
||||
public readonly box;
|
||||
public state;
|
||||
private static store = getDefaultStore()
|
||||
public readonly username
|
||||
public readonly pacMan
|
||||
public readonly colour
|
||||
public readonly box
|
||||
public state
|
||||
|
||||
constructor(props: PlayerProps) {
|
||||
this.username = props.username;
|
||||
this.colour = props.colour;
|
||||
this.box = new Box(props.box ?? {colour: props.colour});
|
||||
this.pacMan = new Character(props.pacMan ?? {
|
||||
colour: props.colour,
|
||||
type: CharacterType.pacMan
|
||||
});
|
||||
this.state = props.state ?? State.waitingForPlayers;
|
||||
this.username = props.username
|
||||
this.colour = props.colour
|
||||
this.box = new Box(props.box ?? { colour: props.colour })
|
||||
this.pacMan = new Character(
|
||||
props.pacMan ?? {
|
||||
colour: props.colour,
|
||||
type: CharacterType.pacMan,
|
||||
},
|
||||
)
|
||||
this.state = props.state ?? State.waitingForPlayers
|
||||
}
|
||||
|
||||
public isTurn(): boolean {
|
||||
return Player.store.get(currentPlayerNameAtom) === this.username;
|
||||
return Player.store.get(currentPlayerNameAtom) === this.username
|
||||
}
|
||||
|
||||
public addPellet(): void {
|
||||
this.box.addPellet();
|
||||
this.box.addPellet()
|
||||
}
|
||||
|
||||
public addPowerPellet(): void {
|
||||
this.box.addPowerPellet();
|
||||
this.box.addPowerPellet()
|
||||
}
|
||||
|
||||
public stealFrom(other: Player): void {
|
||||
for (let i = 0; i < rules.maxStealPellets; i++) {
|
||||
const removed = other.box.removePellet();
|
||||
if (removed)
|
||||
this.box.addPellet();
|
||||
const removed = other.box.removePellet()
|
||||
if (removed) this.box.addPellet()
|
||||
}
|
||||
Player.store.set(playersAtom, Player.store.get(playersAtom).map(player => player));
|
||||
Player.store.set(
|
||||
playersAtom,
|
||||
Player.store.get(playersAtom).map(player => player),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {TileType} from "./tileType";
|
||||
import {Character} from "./character";
|
||||
import {Direction, getDirections} from "./direction";
|
||||
import { TileType } from "./tileType"
|
||||
import { Character } from "./character"
|
||||
import { Direction, getDirections } from "./direction"
|
||||
|
||||
/**
|
||||
* Finds all the possible positions for the character to move to
|
||||
@ -10,10 +10,15 @@ import {Direction, getDirections} from "./direction";
|
||||
* @param characters All the characters on the map
|
||||
* @returns An array of paths the character can move to
|
||||
*/
|
||||
export default function findPossiblePositions(board: GameMap, character: Character, steps: number, characters: Character[]): Path[] {
|
||||
if (!character.position || !character.spawnPosition) throw new Error("Character has no position or spawn position");
|
||||
return findPossibleRecursive(board, character.position, steps, character, characters);
|
||||
};
|
||||
export default function findPossiblePositions(
|
||||
board: GameMap,
|
||||
character: Character,
|
||||
steps: number,
|
||||
characters: Character[],
|
||||
): Path[] {
|
||||
if (!character.position || !character.spawnPosition) throw new Error("Character has no position or spawn position")
|
||||
return findPossibleRecursive(board, character.position, steps, character, characters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses recursion to move through the map and find all the possible positions
|
||||
@ -24,38 +29,40 @@ export default function findPossiblePositions(board: GameMap, character: Charact
|
||||
* @param characters All the characters on the map
|
||||
* @returns {Path[]} An array of paths the character can move to
|
||||
*/
|
||||
function findPossibleRecursive(map: GameMap, currentPath: Path, steps: number, character: Character, characters: Character[]): Path[] {
|
||||
|
||||
const paths: Path[] = [];
|
||||
function findPossibleRecursive(
|
||||
map: GameMap,
|
||||
currentPath: Path,
|
||||
steps: number,
|
||||
character: Character,
|
||||
characters: Character[],
|
||||
): Path[] {
|
||||
const paths: Path[] = []
|
||||
if (isOutsideBoard(currentPath, map.length)) {
|
||||
if (character.isPacMan()) {
|
||||
return addTeleportationTiles(map, currentPath, steps, character, characters);
|
||||
return addTeleportationTiles(map, currentPath, steps, character, characters)
|
||||
}
|
||||
} else if (!isWall(map, currentPath)) {
|
||||
|
||||
if (!characterHitsAnotherCharacter(character, currentPath, characters)) {
|
||||
if (steps <= 0) {
|
||||
if (!(isSpawn(map, currentPath) && !isOwnSpawn(currentPath, character))) {
|
||||
paths.push(currentPath);
|
||||
paths.push(currentPath)
|
||||
}
|
||||
|
||||
} else {
|
||||
tryAddToPath(currentPath)
|
||||
|
||||
tryAddToPath(currentPath);
|
||||
|
||||
steps--;
|
||||
steps--
|
||||
for (const direction of getDirections()) {
|
||||
paths.push(...tryMove(map, currentPath, direction, steps, character, characters));
|
||||
paths.push(...tryMove(map, currentPath, direction, steps, character, characters))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pacMan = ghostHitsPacMan(character, currentPath, characters);
|
||||
const pacMan = ghostHitsPacMan(character, currentPath, characters)
|
||||
if (pacMan instanceof Character && !isCharactersSpawn(currentPath, pacMan)) {
|
||||
paths.push(currentPath);
|
||||
paths.push(currentPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,7 +72,7 @@ function findPossibleRecursive(map: GameMap, currentPath: Path, steps: number, c
|
||||
* @returns {boolean} True if the character is on its spawn, otherwise false
|
||||
*/
|
||||
function isCharactersSpawn(currentPath: Path, character: Character): boolean {
|
||||
return character.spawnPosition?.at.x === currentPath.end.x && character.spawnPosition.at.y === currentPath.end.y;
|
||||
return character.spawnPosition?.at.x === currentPath.end.x && character.spawnPosition.at.y === currentPath.end.y
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,8 +82,12 @@ function isCharactersSpawn(currentPath: Path, character: Character): boolean {
|
||||
* @param characters All the characters on the board
|
||||
* @returns {boolean} True if the character is a ghost and hits Pac-Man, otherwise false
|
||||
*/
|
||||
function ghostHitsPacMan(character: Character, currentPath: Path, characters: Character[]): Character | undefined | false {
|
||||
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end));
|
||||
function ghostHitsPacMan(
|
||||
character: Character,
|
||||
currentPath: Path,
|
||||
characters: Character[],
|
||||
): Character | undefined | false {
|
||||
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,7 +98,7 @@ function ghostHitsPacMan(character: Character, currentPath: Path, characters: Ch
|
||||
* @returns {boolean} True if the character hits another character, otherwise false
|
||||
*/
|
||||
function characterHitsAnotherCharacter(character: Character, currentPath: Path, characters: Character[]): boolean {
|
||||
return characters.find(c => c !== character && c.isAt(currentPath.end)) !== undefined;
|
||||
return characters.find(c => c !== character && c.isAt(currentPath.end)) !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,9 +107,9 @@ function characterHitsAnotherCharacter(character: Character, currentPath: Path,
|
||||
*/
|
||||
function tryAddToPath(currentPos: Path): void {
|
||||
if (!currentPos.path) {
|
||||
currentPos.path = [];
|
||||
currentPos.path = []
|
||||
} else if (!currentPos.path.includes(currentPos.end)) {
|
||||
currentPos.path = [...currentPos.path, currentPos.end];
|
||||
currentPos.path = [...currentPos.path, currentPos.end]
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,39 +124,53 @@ function tryAddToPath(currentPos: Path): void {
|
||||
* @param characters All the characters on the board
|
||||
* @returns An array of paths the character can move to
|
||||
*/
|
||||
function tryMove(board: GameMap, path: Path, direction: Direction, steps: number, character: Character, characters: Character[]): Path[] {
|
||||
|
||||
function tryMove(
|
||||
board: GameMap,
|
||||
path: Path,
|
||||
direction: Direction,
|
||||
steps: number,
|
||||
character: Character,
|
||||
characters: Character[],
|
||||
): Path[] {
|
||||
function getNewPosition(): Position {
|
||||
switch (direction) {
|
||||
case Direction.left:
|
||||
return {
|
||||
x: path.end.x - 1,
|
||||
y: path.end.y
|
||||
};
|
||||
y: path.end.y,
|
||||
}
|
||||
case Direction.up:
|
||||
return {
|
||||
x: path.end.x,
|
||||
y: path.end.y - 1
|
||||
};
|
||||
y: path.end.y - 1,
|
||||
}
|
||||
case Direction.right:
|
||||
return {
|
||||
x: path.end.x + 1,
|
||||
y: path.end.y
|
||||
};
|
||||
y: path.end.y,
|
||||
}
|
||||
case Direction.down:
|
||||
return {
|
||||
x: path.end.x,
|
||||
y: path.end.y + 1
|
||||
};
|
||||
y: path.end.y + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.direction !== (direction + 2) % 4) {
|
||||
return findPossibleRecursive(board, {
|
||||
end: getNewPosition(), direction: direction, path: path.path
|
||||
}, steps, character, characters);
|
||||
return findPossibleRecursive(
|
||||
board,
|
||||
{
|
||||
end: getNewPosition(),
|
||||
direction: direction,
|
||||
path: path.path,
|
||||
},
|
||||
steps,
|
||||
character,
|
||||
characters,
|
||||
)
|
||||
}
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,22 +182,26 @@ function tryMove(board: GameMap, path: Path, direction: Direction, steps: number
|
||||
* @param characters All the characters on the map
|
||||
* @returns {Path[]} An array of paths the character can move to
|
||||
*/
|
||||
function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number, character: Character, characters: Character[]): Path[] {
|
||||
const possiblePositions = findTeleportationTiles(board);
|
||||
const paths: Path[] = [];
|
||||
function addTeleportationTiles(
|
||||
board: GameMap,
|
||||
currentPath: Path,
|
||||
steps: number,
|
||||
character: Character,
|
||||
characters: Character[],
|
||||
): Path[] {
|
||||
const possiblePositions = findTeleportationTiles(board)
|
||||
const paths: Path[] = []
|
||||
for (const pos of possiblePositions) {
|
||||
|
||||
function inInterval(coordinate: "x" | "y"): boolean {
|
||||
return pos.end[coordinate] !== interval(0, board.length - 1, currentPath.end[coordinate])
|
||||
}
|
||||
|
||||
if (inInterval("x") || inInterval("y")) {
|
||||
|
||||
pos.path = currentPath.path;
|
||||
paths.push(...findPossibleRecursive(board, pos, steps, character, characters));
|
||||
pos.path = currentPath.path
|
||||
paths.push(...findPossibleRecursive(board, pos, steps, character, characters))
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,7 +212,7 @@ function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number,
|
||||
* @returns {number} The value if it's between the lower and upper bounds, otherwise it returns the lower or upper bound
|
||||
*/
|
||||
function interval(lower: number, upper: number, value: number): number {
|
||||
return Math.max(Math.min(value, upper), lower);
|
||||
return Math.max(Math.min(value, upper), lower)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,18 +221,17 @@ function interval(lower: number, upper: number, value: number): number {
|
||||
* @returns An array of paths containing the teleportation tiles
|
||||
*/
|
||||
function findTeleportationTiles(map: GameMap): Path[] {
|
||||
const possiblePositions: Path[] = [];
|
||||
const edge = [0, map.length - 1];
|
||||
const possiblePositions: Path[] = []
|
||||
const edge = [0, map.length - 1]
|
||||
|
||||
for (const e of edge) {
|
||||
for (let i = 0; i < map[e].length; i++) {
|
||||
|
||||
pushPath(map, possiblePositions, i, e);
|
||||
pushPath(map, possiblePositions, e, i);
|
||||
pushPath(map, possiblePositions, i, e)
|
||||
pushPath(map, possiblePositions, e, i)
|
||||
}
|
||||
}
|
||||
|
||||
return possiblePositions;
|
||||
return possiblePositions
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,7 +243,10 @@ function findTeleportationTiles(map: GameMap): Path[] {
|
||||
*/
|
||||
function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: number): void {
|
||||
if (board[y] && board[y][x] !== TileType.wall) {
|
||||
possiblePositions.push({end: {x: x, y: y}, direction: findDirection(x, y, board.length)});
|
||||
possiblePositions.push({
|
||||
end: { x: x, y: y },
|
||||
direction: findDirection(x, y, board.length),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,17 +257,17 @@ function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: numbe
|
||||
* @param boardSize The length of the board
|
||||
*/
|
||||
function findDirection(x: number, y: number, boardSize: number): Direction {
|
||||
let direction: Direction;
|
||||
let direction: Direction
|
||||
if (x === 0) {
|
||||
direction = Direction.right;
|
||||
direction = Direction.right
|
||||
} else if (y === 0) {
|
||||
direction = Direction.down;
|
||||
direction = Direction.down
|
||||
} else if (x === boardSize - 1) {
|
||||
direction = Direction.left;
|
||||
direction = Direction.left
|
||||
} else {
|
||||
direction = Direction.up;
|
||||
direction = Direction.up
|
||||
}
|
||||
return direction;
|
||||
return direction
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,8 +276,8 @@ function findDirection(x: number, y: number, boardSize: number): Direction {
|
||||
* @param boardSize The size of the board
|
||||
*/
|
||||
function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
|
||||
const pos = currentPos.end;
|
||||
return pos.x < 0 || pos.x >= boardSize || pos.y < 0 || pos.y >= boardSize;
|
||||
const pos = currentPos.end
|
||||
return pos.x < 0 || pos.x >= boardSize || pos.y < 0 || pos.y >= boardSize
|
||||
}
|
||||
|
||||
/**
|
||||
@ -255,8 +286,8 @@ function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
|
||||
* @param currentPos The current position of the character
|
||||
*/
|
||||
function isWall(board: GameMap, currentPos: Path): boolean {
|
||||
const pos = currentPos.end;
|
||||
return board[pos.y][pos.x] === TileType.wall; // Shouldn't work, but it does
|
||||
const pos = currentPos.end
|
||||
return board[pos.y][pos.x] === TileType.wall // Shouldn't work, but it does
|
||||
}
|
||||
|
||||
/**
|
||||
@ -265,8 +296,8 @@ function isWall(board: GameMap, currentPos: Path): boolean {
|
||||
* @param currentPos The current position of the character
|
||||
*/
|
||||
function isSpawn(board: GameMap, currentPos: Path) {
|
||||
const pos = currentPos.end;
|
||||
return board[pos.y][pos.x] === TileType.pacmanSpawn || board[pos.y][pos.x] === TileType.ghostSpawn;
|
||||
const pos = currentPos.end
|
||||
return board[pos.y][pos.x] === TileType.pacmanSpawn || board[pos.y][pos.x] === TileType.ghostSpawn
|
||||
}
|
||||
|
||||
/**
|
||||
@ -275,8 +306,7 @@ function isSpawn(board: GameMap, currentPos: Path) {
|
||||
* @param character The current character
|
||||
*/
|
||||
function isOwnSpawn(currentPos: Path, character: Character): boolean {
|
||||
const pos = currentPos.end;
|
||||
const charPos = character.spawnPosition!.at;
|
||||
return charPos.x === pos.x && charPos.y === pos.y;
|
||||
const pos = currentPos.end
|
||||
const charPos = character.spawnPosition!.at
|
||||
return charPos.x === pos.x && charPos.y === pos.y
|
||||
}
|
||||
|
||||
|
@ -4,4 +4,4 @@ const rules = {
|
||||
maxStealPellets: 2,
|
||||
}
|
||||
|
||||
export default rules;
|
||||
export default rules
|
||||
|
@ -5,4 +5,4 @@ export enum TileType {
|
||||
powerPellet,
|
||||
ghostSpawn,
|
||||
pacmanSpawn,
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {useState} from "react";
|
||||
import { useState } from "react"
|
||||
|
||||
/**
|
||||
* A hook that returns a boolean value and a function to toggle it. The function can optionally be passed a boolean
|
||||
@ -6,7 +6,7 @@ import {useState} from "react";
|
||||
* @returns A tuple containing the boolean value and a function to toggle it.
|
||||
*/
|
||||
export default function useToggle(defaultValue = false): [boolean, (value?: boolean) => void] {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const toggleValue = (newValue?: boolean) => newValue ? setValue(newValue) : setValue(!value);
|
||||
return [value, toggleValue];
|
||||
}
|
||||
const [value, setValue] = useState(defaultValue)
|
||||
const toggleValue = (newValue?: boolean) => (newValue ? setValue(newValue) : setValue(!value))
|
||||
return [value, toggleValue]
|
||||
}
|
||||
|
@ -3,35 +3,37 @@
|
||||
@tailwind utilities;
|
||||
|
||||
.debug {
|
||||
@apply border border-red-500;
|
||||
@apply after:content-['debug'] after:absolute;
|
||||
@apply border border-red-500;
|
||||
@apply after:content-['debug'] after:absolute;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
@apply flex justify-center items-center;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
||||
.inline-flex-center {
|
||||
@apply inline-flex justify-center items-center;
|
||||
@apply inline-flex justify-center items-center;
|
||||
}
|
||||
|
||||
.wh-full {
|
||||
@apply w-full h-full;
|
||||
@apply w-full h-full;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl;
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl;
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
br {
|
||||
@apply my-2;
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
.button-default, button[type=submit], input[type=submit] {
|
||||
@apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded;
|
||||
@apply disabled:bg-gray-500;
|
||||
.button-default,
|
||||
button[type="submit"],
|
||||
input[type="submit"] {
|
||||
@apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded;
|
||||
@apply disabled:bg-gray-500;
|
||||
}
|
||||
|
@ -1,25 +1,26 @@
|
||||
import React from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import {App} from './App';
|
||||
import React from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { App } from "./App"
|
||||
// @ts-ignore
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import {DevTools} from "jotai-devtools";
|
||||
import DebugMenu from "./components/debugMenu";
|
||||
import reportWebVitals from "./reportWebVitals"
|
||||
import { DevTools } from "jotai-devtools"
|
||||
import DebugMenu from "./components/debugMenu"
|
||||
|
||||
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement === null) throw new Error("Root element is null");
|
||||
const root = createRoot(rootElement);
|
||||
const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")
|
||||
const rootElement = document.getElementById("root")
|
||||
if (rootElement === null) throw new Error("Root element is null")
|
||||
const root = createRoot(rootElement)
|
||||
|
||||
root.render(
|
||||
<BrowserRouter basename={baseUrl ?? undefined}>
|
||||
<DevTools/>
|
||||
<DebugMenu/>
|
||||
<App/>
|
||||
</BrowserRouter>);
|
||||
<DevTools />
|
||||
<DebugMenu />
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
)
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
reportWebVitals()
|
||||
|
@ -1,29 +1,27 @@
|
||||
import React, {FC} from "react";
|
||||
import WebSocketService from "../websockets/WebSocketService";
|
||||
import React, { FC } from "react"
|
||||
import WebSocketService from "../websockets/WebSocketService"
|
||||
|
||||
const ws = new WebSocketService("wss://localhost:3000/api/ws");
|
||||
const ws = new WebSocketService("wss://localhost:3000/api/ws")
|
||||
|
||||
export const Counter: FC = () => {
|
||||
const [currentCount, setCurrentCount] = React.useState(0)
|
||||
|
||||
const [currentCount, setCurrentCount] = React.useState(0);
|
||||
|
||||
async function incrementCounterAndSend() {
|
||||
function incrementCounterAndSend() {
|
||||
if (ws.isOpen()) {
|
||||
await ws.send((currentCount + 1).toString());
|
||||
ws.send((currentCount + 1).toString())
|
||||
}
|
||||
}
|
||||
|
||||
function receiveMessage(data: MessageEvent<string>) {
|
||||
const count = parseInt(data.data);
|
||||
if (!isNaN(count))
|
||||
setCurrentCount(count);
|
||||
const count = parseInt(data.data)
|
||||
if (!isNaN(count)) setCurrentCount(count)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
ws.onReceive = receiveMessage;
|
||||
ws.open();
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
ws.onReceive = receiveMessage
|
||||
ws.open()
|
||||
return () => ws.close()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -31,9 +29,13 @@ export const Counter: FC = () => {
|
||||
|
||||
<p>This is a simple example of a React component.</p>
|
||||
|
||||
<p aria-live="polite">Current count: <strong>{currentCount}</strong></p>
|
||||
<p aria-live="polite">
|
||||
Current count: <strong>{currentCount}</strong>
|
||||
</p>
|
||||
|
||||
<button className="btn btn-primary" onClick={incrementCounterAndSend}>Increment</button>
|
||||
<button className="btn btn-primary" onClick={incrementCounterAndSend}>
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,17 +1,16 @@
|
||||
import React, {FC} from "react";
|
||||
import {GameComponent} from "../components/gameComponent";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {selectedMapAtom, thisPlayerAtom} from "../utils/state";
|
||||
import React, { FC } from "react"
|
||||
import { GameComponent } from "../components/gameComponent"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { selectedMapAtom, thisPlayerAtom } from "../utils/state"
|
||||
|
||||
const GamePage: FC = () => {
|
||||
const player = useAtomValue(thisPlayerAtom);
|
||||
const map = useAtomValue(selectedMapAtom);
|
||||
const player = useAtomValue(thisPlayerAtom)
|
||||
const map = useAtomValue(selectedMapAtom)
|
||||
|
||||
if (player && map) {
|
||||
return <GameComponent player={player} map={map}/>;
|
||||
} else {
|
||||
return null;
|
||||
return <GameComponent player={player} map={map} />
|
||||
}
|
||||
};
|
||||
return null
|
||||
}
|
||||
|
||||
export default GamePage;
|
||||
export default GamePage
|
||||
|
@ -1,27 +1,35 @@
|
||||
import React, {FC} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {thisPlayerAtom} from "../utils/state";
|
||||
import React, { FC } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { thisPlayerAtom } from "../utils/state"
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const player = useAtomValue(thisPlayerAtom);
|
||||
const player = useAtomValue(thisPlayerAtom)
|
||||
|
||||
return (
|
||||
<div className={"container max-w-[800px] mx-auto px-2"}>
|
||||
<h1 className={"w-fit mx-auto"}>Hello {player?.username ?? "Player"}. Do you want to play a game?</h1>
|
||||
<p className={"text-center mt-5"}>
|
||||
{!player ?
|
||||
<>Start by {" "}
|
||||
<Link to={"/login"} className={"text-blue-600"}>logging in</Link>.
|
||||
{!player ? (
|
||||
<>
|
||||
Start by{" "}
|
||||
<Link to={"/login"} className={"text-blue-600"}>
|
||||
logging in
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
:
|
||||
<>Go to the {" "}
|
||||
<Link to={"/lobby"} className={"text-blue-600"}>lobby</Link> to select a game.
|
||||
) : (
|
||||
<>
|
||||
Go to the{" "}
|
||||
<Link to={"/lobby"} className={"text-blue-600"}>
|
||||
lobby
|
||||
</Link>{" "}
|
||||
to select a game.
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
export default HomePage
|
||||
|
@ -1,66 +1,65 @@
|
||||
import React, {FC, Suspense} from "react";
|
||||
import {atom, useAtomValue} from "jotai";
|
||||
import {Button} from "../components/button";
|
||||
import {selectedMapAtom, thisPlayerAtom} from "../utils/state";
|
||||
import {getData, postData} from "../utils/api";
|
||||
import {getPacManSpawns} from "../game/map";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import React, { FC, Suspense } from "react"
|
||||
import { atom, useAtomValue } from "jotai"
|
||||
import { Button } from "../components/button"
|
||||
import { selectedMapAtom, thisPlayerAtom } from "../utils/state"
|
||||
import { getData, postData } from "../utils/api"
|
||||
import { getPacManSpawns } from "../game/map"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
const fetchAtom = atom(async () => {
|
||||
const response = await getData("/game/all");
|
||||
return await response.json() as Game[];
|
||||
});
|
||||
const response = await getData("/game/all")
|
||||
return (await response.json()) as Game[]
|
||||
})
|
||||
|
||||
const LobbyPage: FC = () => {
|
||||
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||
const navigate = useNavigate();
|
||||
const map = useAtomValue(selectedMapAtom);
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
||||
const navigate = useNavigate()
|
||||
const map = useAtomValue(selectedMapAtom)
|
||||
|
||||
async function createGame(): Promise<void> {
|
||||
|
||||
const response = await postData("/game/create", {
|
||||
body: {player: thisPlayer, spawns: getPacManSpawns(map)} as CreateGameData
|
||||
});
|
||||
body: {
|
||||
player: thisPlayer,
|
||||
spawns: getPacManSpawns(map),
|
||||
} as CreateGameData,
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
navigate("/game/" + data.id)
|
||||
} else {
|
||||
const data = await response.text();
|
||||
console.error("Error: ", data);
|
||||
const data = await response.text()
|
||||
console.error("Error: ", data)
|
||||
// TODO display error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={createGame}>New game</Button>
|
||||
<Suspense fallback={"Please wait"}>
|
||||
<GameTable className={"mx-auto"}/>
|
||||
<GameTable className={"mx-auto"} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default LobbyPage;
|
||||
export default LobbyPage
|
||||
|
||||
const GameTable: FC<ComponentProps> = ({className}) => {
|
||||
|
||||
const data = useAtomValue(fetchAtom);
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom);
|
||||
const navigate = useNavigate();
|
||||
const GameTable: FC<ComponentProps> = ({ className }) => {
|
||||
const data = useAtomValue(fetchAtom)
|
||||
const thisPlayer = useAtomValue(thisPlayerAtom)
|
||||
const navigate = useNavigate()
|
||||
|
||||
async function joinGame(gameId: string): Promise<void> {
|
||||
if (thisPlayer === undefined) throw new Error("Player is undefined");
|
||||
if (thisPlayer === undefined) throw new Error("Player is undefined")
|
||||
|
||||
const result = await postData("/game/join/" + gameId, {body: thisPlayer});
|
||||
const result = await postData("/game/join/" + gameId, { body: thisPlayer })
|
||||
|
||||
if (result.ok) {
|
||||
navigate("/game/" + gameId);
|
||||
navigate("/game/" + gameId)
|
||||
} else {
|
||||
console.error("Failed to join game " + gameId, await result.text());
|
||||
console.error("Failed to join game " + gameId, await result.text())
|
||||
// TODO show error message
|
||||
}
|
||||
}
|
||||
@ -68,33 +67,34 @@ const GameTable: FC<ComponentProps> = ({className}) => {
|
||||
return (
|
||||
<table className={`rounded overflow-hidden ${className}`}>
|
||||
<thead className={"bg-gray-500 text-white"}>
|
||||
<tr className={"my-5"}>
|
||||
<th className={"p-2"}>Id</th>
|
||||
<th className={"p-2"}>Count</th>
|
||||
<th className={"p-2"}>State</th>
|
||||
<th className={"p-2"}>Join</th>
|
||||
</tr>
|
||||
<tr className={"my-5"}>
|
||||
<th className={"p-2"}>Id</th>
|
||||
<th className={"p-2"}>Count</th>
|
||||
<th className={"p-2"}>State</th>
|
||||
<th className={"p-2"}>Join</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map(game =>
|
||||
<tr key={game.id} className={"even:bg-gray-200"}>
|
||||
<td className={"p-2"}>{game.id}</td>
|
||||
<td className={"text-center"}>{game.count}</td>
|
||||
<td>{game.isGameStarted ? "Closed" : "Open"}</td>
|
||||
<td className={"p-2"}>
|
||||
<Button disabled={game.isGameStarted} onClick={() => joinGame(game.id)}>
|
||||
Join
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{
|
||||
data?.length === 0 &&
|
||||
<tr>
|
||||
<td colSpan={4} className={"text-center"}>No games found</td>
|
||||
{data?.map(game => (
|
||||
<tr key={game.id} className={"even:bg-gray-200"}>
|
||||
<td className={"p-2"}>{game.id}</td>
|
||||
<td className={"text-center"}>{game.count}</td>
|
||||
<td>{game.isGameStarted ? "Closed" : "Open"}</td>
|
||||
<td className={"p-2"}>
|
||||
<Button disabled={game.isGameStarted} onClick={() => joinGame(game.id)}>
|
||||
Join
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
))}
|
||||
{data?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className={"text-center"}>
|
||||
No games found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,23 +1,22 @@
|
||||
import React, {FC, FormEvent, useState} from "react";
|
||||
import {Button} from "../components/button";
|
||||
import Input from "../components/input";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {thisPlayerAtom} from "../utils/state";
|
||||
import Player from "../game/player";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {postData} from "../utils/api";
|
||||
import React, { FC, FormEvent, useState } from "react"
|
||||
import { Button } from "../components/button"
|
||||
import Input from "../components/input"
|
||||
import { useSetAtom } from "jotai"
|
||||
import { thisPlayerAtom } from "../utils/state"
|
||||
import Player from "../game/player"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { postData } from "../utils/api"
|
||||
|
||||
const LoginPage: FC = () => {
|
||||
|
||||
const setThisPlayer = useSetAtom(thisPlayerAtom);
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const setThisPlayer = useSetAtom(thisPlayerAtom)
|
||||
const navigate = useNavigate()
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
|
||||
async function handleLogin(e: FormEvent<HTMLFormElement>): Promise<void> {
|
||||
e.preventDefault();
|
||||
const fields = e.currentTarget.querySelectorAll("input");
|
||||
e.preventDefault()
|
||||
const fields = e.currentTarget.querySelectorAll("input")
|
||||
|
||||
let user: User = {username: "", password: ""};
|
||||
let user: User = { username: "", password: "" }
|
||||
for (const field of fields) {
|
||||
user = {
|
||||
...user,
|
||||
@ -26,36 +25,41 @@ const LoginPage: FC = () => {
|
||||
}
|
||||
|
||||
const response = await postData("/player/login", {
|
||||
body: {username: user.username, password: user.password} as User
|
||||
body: { username: user.username, password: user.password } as User,
|
||||
})
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json() as PlayerProps;
|
||||
setThisPlayer(new Player(data));
|
||||
navigate("/lobby");
|
||||
const data = (await response.json()) as PlayerProps
|
||||
setThisPlayer(new Player(data))
|
||||
navigate("/lobby")
|
||||
} else {
|
||||
const data = await response.text();
|
||||
console.error(data);
|
||||
setError(data);
|
||||
const data = await response.text()
|
||||
console.error(data)
|
||||
setError(data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const username = "username", password = "password";
|
||||
const username = "username",
|
||||
password = "password"
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin} className={"container w-fit mx-auto flex flex-col gap-2"}>
|
||||
<h1 className={"my-5"}>Login</h1>
|
||||
{error && <p className={"text-red-500"}>{error}</p>}
|
||||
<label htmlFor={username}>Username:</label>
|
||||
<Input id={username} name={username} placeholder={"Username"} autoComplete={"username"} required/>
|
||||
<Input id={username} name={username} placeholder={"Username"} autoComplete={"username"} required />
|
||||
<label htmlFor={password}>Password:</label>
|
||||
<Input id={password} name={password} type={"password"} placeholder={"Password"}
|
||||
autoComplete={"current-password"} required/>
|
||||
<Input
|
||||
id={password}
|
||||
name={password}
|
||||
type={"password"}
|
||||
placeholder={"Password"}
|
||||
autoComplete={"current-password"}
|
||||
required
|
||||
/>
|
||||
<Button type={"submit"}>Login</Button>
|
||||
</form>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
export default LoginPage
|
||||
|
@ -1,13 +1,13 @@
|
||||
const reportWebVitals = (onPerfEntry) => {
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default reportWebVitals;
|
||||
export default reportWebVitals
|
||||
|
@ -1,25 +1,27 @@
|
||||
type FRComponent<T = ComponentProps, HTML extends HTMLElement = HTMLElement> = React.ForwardRefExoticComponent<React.PropsWithoutRef<T> & React.RefAttributes<HTML>>;
|
||||
type FRComponent<T = ComponentProps, HTML extends HTMLElement = HTMLElement> = React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<T> & React.RefAttributes<HTML>
|
||||
>
|
||||
|
||||
interface ComponentProps {
|
||||
className?: string,
|
||||
style?: React.CSSProperties,
|
||||
id?: string,
|
||||
title?: string,
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
id?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
interface ChildProps extends ComponentProps {
|
||||
children?: React.JSX.Element | string,
|
||||
children?: React.JSX.Element | string
|
||||
}
|
||||
|
||||
interface LinkProps extends ChildProps {
|
||||
to: string,
|
||||
newTab?: boolean,
|
||||
to: string
|
||||
newTab?: boolean
|
||||
}
|
||||
|
||||
interface ButtonProps extends ChildProps {
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void,
|
||||
disabled?: boolean,
|
||||
type?: "button" | "submit" | "reset",
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
disabled?: boolean
|
||||
type?: "button" | "submit" | "reset"
|
||||
}
|
||||
|
||||
interface InputProps extends ComponentProps {
|
||||
@ -31,23 +33,23 @@ interface InputProps extends ComponentProps {
|
||||
}
|
||||
|
||||
interface CharacterProps {
|
||||
colour: import("../game/colour").Colour,
|
||||
position?: Path | null,
|
||||
isEatable?: boolean,
|
||||
spawnPosition?: DirectionalPosition | null,
|
||||
type?: import("../game/character").CharacterType,
|
||||
colour: import("../game/colour").Colour
|
||||
position?: Path | null
|
||||
isEatable?: boolean
|
||||
spawnPosition?: DirectionalPosition | null
|
||||
type?: import("../game/character").CharacterType
|
||||
}
|
||||
|
||||
interface BoxProps {
|
||||
pellets?: number,
|
||||
powerPellets?: number,
|
||||
readonly colour: import("../game/colour").Colour,
|
||||
pellets?: number
|
||||
powerPellets?: number
|
||||
readonly colour: import("../game/colour").Colour
|
||||
}
|
||||
|
||||
interface PlayerProps {
|
||||
readonly username: string,
|
||||
readonly pacMan?: CharacterProps,
|
||||
readonly colour: import("../game/colour").Colour,
|
||||
readonly box?: BoxProps,
|
||||
state?: import("../game/player").State,
|
||||
readonly username: string
|
||||
readonly pacMan?: CharacterProps
|
||||
readonly colour: import("../game/colour").Colour
|
||||
readonly box?: BoxProps
|
||||
state?: import("../game/player").State
|
||||
}
|
||||
|
@ -1,68 +1,68 @@
|
||||
type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void;
|
||||
type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void
|
||||
|
||||
type Setter<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||
type Setter<T> = React.Dispatch<React.SetStateAction<T>>
|
||||
|
||||
type GUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||
type GUID = `${string}-${string}-${string}-${string}-${string}`
|
||||
|
||||
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView;
|
||||
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView
|
||||
|
||||
type ActionMessage<T = any> = {
|
||||
readonly action: import("../utils/actions").GameAction,
|
||||
readonly action: import("../utils/actions").GameAction
|
||||
readonly data?: T
|
||||
}
|
||||
|
||||
type Action<T> = (obj: T) => void;
|
||||
type Action<T> = (obj: T) => void
|
||||
|
||||
type BiAction<T1, T2> = (obj1: T1, obj2: T2) => void;
|
||||
type BiAction<T1, T2> = (obj1: T1, obj2: T2) => void
|
||||
|
||||
type Predicate<T> = (obj: T) => boolean;
|
||||
type Predicate<T> = (obj: T) => boolean
|
||||
|
||||
type SelectedDice = {
|
||||
value: number,
|
||||
value: number
|
||||
index: number
|
||||
};
|
||||
}
|
||||
|
||||
type Position = { x: number, y: number };
|
||||
type Position = { x: number; y: number }
|
||||
|
||||
type GameMap = number[][];
|
||||
type GameMap = number[][]
|
||||
|
||||
type DirectionalPosition = {
|
||||
at: Position,
|
||||
at: Position
|
||||
direction: import("../game/direction").Direction
|
||||
}
|
||||
|
||||
type Path = {
|
||||
path?: Position[] | null,
|
||||
path?: Position[] | null
|
||||
// TODO replace with DirectionalPosition
|
||||
end: Position,
|
||||
end: Position
|
||||
direction: import("../game/direction").Direction
|
||||
}
|
||||
|
||||
type Game = {
|
||||
readonly id: string,
|
||||
readonly count: number,
|
||||
readonly isGameStarted: boolean,
|
||||
readonly id: string
|
||||
readonly count: number
|
||||
readonly isGameStarted: boolean
|
||||
}
|
||||
|
||||
type User = {
|
||||
readonly username: string,
|
||||
readonly password: string,
|
||||
readonly username: string
|
||||
readonly password: string
|
||||
readonly colour?: import("../game/colour").Colour
|
||||
}
|
||||
|
||||
type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>;
|
||||
type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>
|
||||
|
||||
type ApiRequest = {
|
||||
headers?: HeadersInit,
|
||||
headers?: HeadersInit
|
||||
body?: any
|
||||
}
|
||||
|
||||
type JoinGameData = {
|
||||
readonly username: string,
|
||||
readonly gameId: GUID,
|
||||
readonly username: string
|
||||
readonly gameId: GUID
|
||||
}
|
||||
|
||||
type CreateGameData = {
|
||||
readonly player: PlayerProps,
|
||||
readonly spawns: DirectionalPosition[],
|
||||
readonly player: PlayerProps
|
||||
readonly spawns: DirectionalPosition[]
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import Player from "../game/player";
|
||||
import {CharacterType, Ghost} from "../game/character";
|
||||
import {getCharacterSpawns} from "../game/map";
|
||||
import {TileType} from "../game/tileType";
|
||||
import {getDefaultStore} from "jotai";
|
||||
import {currentPlayerNameAtom, diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedMapAtom} from "./state";
|
||||
import {Colour} from "../game/colour";
|
||||
import Player from "../game/player"
|
||||
import { CharacterType, Ghost } from "../game/character"
|
||||
import { getCharacterSpawns } from "../game/map"
|
||||
import { TileType } from "../game/tileType"
|
||||
import { getDefaultStore } from "jotai"
|
||||
import { currentPlayerNameAtom, diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedMapAtom } from "./state"
|
||||
import { Colour } from "../game/colour"
|
||||
|
||||
export enum GameAction {
|
||||
error,
|
||||
@ -17,107 +17,112 @@ export enum GameAction {
|
||||
// TODO add updatePellets
|
||||
}
|
||||
|
||||
const store = getDefaultStore();
|
||||
const map = store.get(selectedMapAtom);
|
||||
const store = getDefaultStore()
|
||||
const map = store.get(selectedMapAtom)
|
||||
|
||||
const ghostsProps: CharacterProps[] = [
|
||||
{colour: Colour.purple},
|
||||
{colour: Colour.purple},
|
||||
];
|
||||
let spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.ghost);
|
||||
const ghostsProps: CharacterProps[] = [{ colour: Colour.purple }, { colour: Colour.purple }]
|
||||
let spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.ghost)
|
||||
ghostsProps.forEach(ghost => {
|
||||
ghost.spawnPosition = spawns.pop()?.position;
|
||||
ghost.spawnPosition = spawns.pop()?.position
|
||||
})
|
||||
const ghosts = ghostsProps.map(props => new Ghost(props))
|
||||
|
||||
});
|
||||
const ghosts = ghostsProps.map(props => new Ghost(props));
|
||||
|
||||
store.set(ghostsAtom, ghosts);
|
||||
store.set(ghostsAtom, ghosts)
|
||||
|
||||
export const doAction: MessageEventFunction<string> = (event): void => {
|
||||
const message: ActionMessage = JSON.parse(event.data);
|
||||
console.debug("Received message:", message);
|
||||
const message: ActionMessage = JSON.parse(event.data)
|
||||
console.debug("Received message:", message)
|
||||
|
||||
switch (message.action as GameAction) {
|
||||
case GameAction.error:
|
||||
console.error("Error:", message.data);
|
||||
break;
|
||||
console.error("Error:", message.data)
|
||||
break
|
||||
case GameAction.rollDice:
|
||||
setDice(message.data);
|
||||
break;
|
||||
setDice(message.data)
|
||||
break
|
||||
case GameAction.moveCharacter:
|
||||
moveCharacter(message.data);
|
||||
break;
|
||||
moveCharacter(message.data)
|
||||
break
|
||||
case GameAction.joinGame:
|
||||
joinGame(message.data);
|
||||
break;
|
||||
joinGame(message.data)
|
||||
break
|
||||
case GameAction.ready:
|
||||
ready(message.data);
|
||||
break;
|
||||
ready(message.data)
|
||||
break
|
||||
case GameAction.nextPlayer:
|
||||
nextPlayer(message.data);
|
||||
break;
|
||||
nextPlayer(message.data)
|
||||
break
|
||||
case GameAction.disconnect:
|
||||
updatePlayers(message.data);
|
||||
break;
|
||||
updatePlayers(message.data)
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
function setDice(data?: number[]): void {
|
||||
store.set(diceAtom, data);
|
||||
}
|
||||
|
||||
type MoveCharacterData = { dice: number[], players: PlayerProps[], ghosts: CharacterProps[], eatenPellets: Position[] };
|
||||
function setDice(data?: number[]): void {
|
||||
store.set(diceAtom, data)
|
||||
}
|
||||
|
||||
type MoveCharacterData = {
|
||||
dice: number[]
|
||||
players: PlayerProps[]
|
||||
ghosts: CharacterProps[]
|
||||
eatenPellets: Position[]
|
||||
}
|
||||
|
||||
function moveCharacter(data?: MoveCharacterData): void {
|
||||
store.set(diceAtom, data?.dice);
|
||||
updatePlayers(data?.players);
|
||||
updateGhosts(data);
|
||||
removeEatenPellets(data);
|
||||
store.set(diceAtom, data?.dice)
|
||||
updatePlayers(data?.players)
|
||||
updateGhosts(data)
|
||||
removeEatenPellets(data)
|
||||
}
|
||||
|
||||
function updatePlayers(updatedPlayers?: PlayerProps[]): void {
|
||||
if (updatedPlayers) {
|
||||
const newList: Player[] = updatedPlayers.map(p => new Player(p));
|
||||
store.set(playersAtom, newList);
|
||||
const newList: Player[] = updatedPlayers.map(p => new Player(p))
|
||||
store.set(playersAtom, newList)
|
||||
}
|
||||
}
|
||||
|
||||
function updateGhosts(data?: MoveCharacterData): void {
|
||||
const updatedGhosts = data?.ghosts;
|
||||
const updatedGhosts = data?.ghosts
|
||||
|
||||
if (updatedGhosts) {
|
||||
const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g));
|
||||
store.set(ghostsAtom, newList);
|
||||
const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g))
|
||||
store.set(ghostsAtom, newList)
|
||||
}
|
||||
}
|
||||
|
||||
function removeEatenPellets(data?: MoveCharacterData): void {
|
||||
const pellets = data?.eatenPellets;
|
||||
const pellets = data?.eatenPellets
|
||||
|
||||
for (const pellet of pellets ?? []) {
|
||||
map[pellet.y][pellet.x] = TileType.empty;
|
||||
map[pellet.y][pellet.x] = TileType.empty
|
||||
}
|
||||
}
|
||||
|
||||
function joinGame(data?: PlayerProps[]): void { // TODO missing data when refreshing page
|
||||
const playerProps = data ?? [];
|
||||
spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan);
|
||||
store.set(playersAtom, playerProps.map(p => new Player(p)));
|
||||
function joinGame(data?: PlayerProps[]): void {
|
||||
// TODO missing data when refreshing page
|
||||
const playerProps = data ?? []
|
||||
spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan)
|
||||
store.set(
|
||||
playersAtom,
|
||||
playerProps.map(p => new Player(p)),
|
||||
)
|
||||
}
|
||||
|
||||
type ReadyData = { allReady: boolean, players: PlayerProps[] };
|
||||
type ReadyData = { allReady: boolean; players: PlayerProps[] }
|
||||
|
||||
function ready(data?: ReadyData): void {
|
||||
if (data) {
|
||||
const players = data.players.map(p => new Player(p));
|
||||
store.set(playersAtom, players);
|
||||
const players = data.players.map(p => new Player(p))
|
||||
store.set(playersAtom, players)
|
||||
if (data.allReady) {
|
||||
store.set(currentPlayerNameAtom, data.players[0].username);
|
||||
store.set(currentPlayerNameAtom, data.players[0].username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nextPlayer(currentPlayerName?: string): void {
|
||||
store.set(currentPlayerNameAtom, currentPlayerName);
|
||||
store.set(rollDiceButtonAtom, true);
|
||||
store.set(currentPlayerNameAtom, currentPlayerName)
|
||||
store.set(rollDiceButtonAtom, true)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
export const getData: Api = async (path, {headers} = {}) => {
|
||||
if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([])));
|
||||
export const getData: Api = async (path, { headers } = {}) => {
|
||||
if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([])))
|
||||
return await fetch(import.meta.env.VITE_API_HTTP + path, {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
});
|
||||
headers: headers,
|
||||
})
|
||||
}
|
||||
|
||||
export const postData: Api = async (path, {body, headers} = {}) => {
|
||||
export const postData: Api = async (path, { body, headers } = {}) => {
|
||||
return await fetch(import.meta.env.VITE_API_HTTP + path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
@ -1,74 +1,75 @@
|
||||
import Player from "../game/player";
|
||||
import {atom} from "jotai";
|
||||
import {Ghost} from "../game/character";
|
||||
import {customMap} from "../game/map";
|
||||
import Player from "../game/player"
|
||||
import { atom } from "jotai"
|
||||
import { Ghost } from "../game/character"
|
||||
import { customMap } from "../game/map"
|
||||
|
||||
const playerStorage = "player";
|
||||
const playerStorage = "player"
|
||||
/**
|
||||
* All players in the game.
|
||||
*/
|
||||
export const playersAtom = atom<Player[]>([]);
|
||||
export const playersAtom = atom<Player[]>([])
|
||||
/**
|
||||
* All player characters (Pac-Man) in the game.
|
||||
*/
|
||||
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.pacMan));
|
||||
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.pacMan))
|
||||
/**
|
||||
* All ghosts in the game.
|
||||
*/
|
||||
export const ghostsAtom = atom<Ghost[]>([]);
|
||||
export const ghostsAtom = atom<Ghost[]>([])
|
||||
/**
|
||||
* All characters in the game.
|
||||
*/
|
||||
export const allCharactersAtom = atom(get => [...get(playerCharactersAtom), ...get(ghostsAtom)]);
|
||||
export const allCharactersAtom = atom(get => [...get(playerCharactersAtom), ...get(ghostsAtom)])
|
||||
/**
|
||||
* The player that is currently logged in.
|
||||
*/
|
||||
const playerAtom = atom<Player | undefined>(undefined);
|
||||
const playerAtom = atom<Player | undefined>(undefined)
|
||||
/**
|
||||
* Gets a getter and setter to get or set the player that is currently logged in.
|
||||
* Returns A tuple containing a getter and setter to get or set the player that is currently logged in.
|
||||
*/
|
||||
export const thisPlayerAtom = atom(get => {
|
||||
const atomValue = get(playerAtom);
|
||||
if (!atomValue) {
|
||||
const item = sessionStorage.getItem(playerStorage);
|
||||
if (item) {
|
||||
const playerProps = JSON.parse(item) as PlayerProps;
|
||||
return new Player(playerProps);
|
||||
export const thisPlayerAtom = atom(
|
||||
get => {
|
||||
const atomValue = get(playerAtom)
|
||||
if (!atomValue) {
|
||||
const item = sessionStorage.getItem(playerStorage)
|
||||
if (item) {
|
||||
const playerProps = JSON.parse(item) as PlayerProps
|
||||
return new Player(playerProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
return atomValue;
|
||||
}, (_get, set, player: Player | undefined) => {
|
||||
if (player)
|
||||
sessionStorage.setItem(playerStorage, JSON.stringify(player));
|
||||
else
|
||||
sessionStorage.removeItem(playerStorage);
|
||||
set(playerAtom, player);
|
||||
});
|
||||
return atomValue
|
||||
},
|
||||
(_get, set, player: Player | undefined) => {
|
||||
if (player) sessionStorage.setItem(playerStorage, JSON.stringify(player))
|
||||
else sessionStorage.removeItem(playerStorage)
|
||||
set(playerAtom, player)
|
||||
},
|
||||
)
|
||||
/**
|
||||
* All dice that have been rolled.
|
||||
*/
|
||||
export const diceAtom = atom<number[] | undefined>(undefined);
|
||||
export const diceAtom = atom<number[] | undefined>(undefined)
|
||||
/**
|
||||
* The dice that have been selected by the player.
|
||||
*/
|
||||
export const selectedDiceAtom = atom<SelectedDice | undefined>(undefined);
|
||||
export const selectedDiceAtom = atom<SelectedDice | undefined>(undefined)
|
||||
/**
|
||||
* The name of the player whose turn it is.
|
||||
*/
|
||||
export const currentPlayerNameAtom = atom<string | undefined>(undefined);
|
||||
export const currentPlayerNameAtom = atom<string | undefined>(undefined)
|
||||
/**
|
||||
* The player whose turn it is.
|
||||
*/
|
||||
export const currentPlayerAtom = atom<Player | undefined>(get => {
|
||||
const currentPlayerName = get(currentPlayerNameAtom);
|
||||
return get(playersAtom).find(player => player.username === currentPlayerName);
|
||||
});
|
||||
const currentPlayerName = get(currentPlayerNameAtom)
|
||||
return get(playersAtom).find(player => player.username === currentPlayerName)
|
||||
})
|
||||
/**
|
||||
* Whether the roll dice button should be enabled.
|
||||
*/
|
||||
export const rollDiceButtonAtom = atom(true);
|
||||
export const rollDiceButtonAtom = atom(true)
|
||||
/**
|
||||
* The map that is currently selected.
|
||||
*/
|
||||
export const selectedMapAtom = atom(customMap);
|
||||
export const selectedMapAtom = atom(customMap)
|
||||
|
@ -5,14 +5,14 @@
|
||||
* @returns A promise that resolves when the predicate is true.
|
||||
*/
|
||||
export function wait(predicate: Predicate<void>, timeout: number = 50): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
return new Promise<void>(resolve => {
|
||||
const f = () => {
|
||||
if (predicate()) {
|
||||
return resolve();
|
||||
return resolve()
|
||||
}
|
||||
setTimeout(f, timeout);
|
||||
};
|
||||
setTimeout(f, timeout)
|
||||
}
|
||||
|
||||
f();
|
||||
});
|
||||
f()
|
||||
})
|
||||
}
|
||||
|
10
pac-man-board-game/ClientApp/src/vite-env.d.ts
vendored
10
pac-man-board-game/ClientApp/src/vite-env.d.ts
vendored
@ -1,11 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URI: string,
|
||||
readonly VITE_API_HTTP: string,
|
||||
readonly VITE_API_WS: string,
|
||||
readonly VITE_API_URI: string
|
||||
readonly VITE_API_HTTP: string
|
||||
readonly VITE_API_WS: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {wait} from "../utils/utils";
|
||||
import { wait } from "../utils/utils"
|
||||
|
||||
interface IWebSocket {
|
||||
onOpen?: VoidFunction,
|
||||
onReceive?: MessageEventFunction,
|
||||
onClose?: VoidFunction,
|
||||
onOpen?: VoidFunction
|
||||
onReceive?: MessageEventFunction
|
||||
onClose?: VoidFunction
|
||||
onError?: VoidFunction
|
||||
}
|
||||
|
||||
@ -11,59 +11,59 @@ interface IWebSocket {
|
||||
* WebSocketService class provides a WebSocket client interface for easy communication with a WebSocket server.
|
||||
*/
|
||||
export default class WebSocketService {
|
||||
private ws?: WebSocket;
|
||||
private readonly _url: string;
|
||||
private ws?: WebSocket
|
||||
private readonly _url: string
|
||||
|
||||
constructor(url: string, {onOpen, onReceive, onClose, onError}: IWebSocket = {}) {
|
||||
this._url = url;
|
||||
this._onOpen = onOpen;
|
||||
this._onReceive = onReceive;
|
||||
this._onClose = onClose;
|
||||
this._onError = onError;
|
||||
constructor(url: string, { onOpen, onReceive, onClose, onError }: IWebSocket = {}) {
|
||||
this._url = url
|
||||
this._onOpen = onOpen
|
||||
this._onReceive = onReceive
|
||||
this._onClose = onClose
|
||||
this._onError = onError
|
||||
}
|
||||
|
||||
private _onOpen?: VoidFunction;
|
||||
private _onOpen?: VoidFunction
|
||||
|
||||
set onOpen(onOpen: VoidFunction) {
|
||||
this._onOpen = onOpen;
|
||||
if (!this.ws) return;
|
||||
this.ws.onopen = onOpen;
|
||||
this._onOpen = onOpen
|
||||
if (!this.ws) return
|
||||
this.ws.onopen = onOpen
|
||||
}
|
||||
|
||||
private _onReceive?: MessageEventFunction;
|
||||
private _onReceive?: MessageEventFunction
|
||||
|
||||
set onReceive(onReceive: MessageEventFunction) {
|
||||
this._onReceive = onReceive;
|
||||
if (!this.ws) return;
|
||||
this.ws.onmessage = onReceive;
|
||||
this._onReceive = onReceive
|
||||
if (!this.ws) return
|
||||
this.ws.onmessage = onReceive
|
||||
}
|
||||
|
||||
private _onClose?: VoidFunction;
|
||||
private _onClose?: VoidFunction
|
||||
|
||||
set onClose(onClose: VoidFunction) {
|
||||
this._onClose = onClose;
|
||||
if (!this.ws) return;
|
||||
this.ws.onclose = onClose;
|
||||
this._onClose = onClose
|
||||
if (!this.ws) return
|
||||
this.ws.onclose = onClose
|
||||
}
|
||||
|
||||
private _onError?: VoidFunction;
|
||||
private _onError?: VoidFunction
|
||||
|
||||
set onError(onError: VoidFunction) {
|
||||
this._onError = onError;
|
||||
if (!this.ws) return;
|
||||
this.ws.onerror = onError;
|
||||
this._onError = onError
|
||||
if (!this.ws) return
|
||||
this.ws.onerror = onError
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a WebSocket connection with the specified URL and sets the event callbacks.
|
||||
*/
|
||||
public open(): void {
|
||||
if (typeof WebSocket === "undefined" || this.isConnecting()) return;
|
||||
this.ws = new WebSocket(this._url);
|
||||
if (this._onOpen) this.ws.onopen = this._onOpen;
|
||||
if (this._onReceive) this.ws.onmessage = this._onReceive;
|
||||
if (this._onClose) this.ws.onclose = this._onClose;
|
||||
if (this._onError) this.ws.onerror = this._onError;
|
||||
if (typeof WebSocket === "undefined" || this.isConnecting()) return
|
||||
this.ws = new WebSocket(this._url)
|
||||
if (this._onOpen) this.ws.onopen = this._onOpen
|
||||
if (this._onReceive) this.ws.onmessage = this._onReceive
|
||||
if (this._onClose) this.ws.onclose = this._onClose
|
||||
if (this._onError) this.ws.onerror = this._onError
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,8 +72,8 @@ export default class WebSocketService {
|
||||
* @returns {Promise<void>} - A promise that resolves when the "isOpen" condition is met.
|
||||
*/
|
||||
public async waitForOpen(): Promise<void> {
|
||||
await wait(() => this.isOpen());
|
||||
if (this._onOpen) this.onOpen = this._onOpen;
|
||||
await wait(() => this.isOpen())
|
||||
if (this._onOpen) this.onOpen = this._onOpen
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,16 +83,16 @@ export default class WebSocketService {
|
||||
*/
|
||||
public send(data: ActionMessage | string): void {
|
||||
if (typeof data !== "string") {
|
||||
data = JSON.stringify(data);
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
this.ws?.send(data);
|
||||
this.ws?.send(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the WebSocket connection.
|
||||
*/
|
||||
public close(): void {
|
||||
this.ws?.close();
|
||||
this.ws?.close()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,7 +100,7 @@ export default class WebSocketService {
|
||||
* @returns {boolean} Returns true if the WebSocket is open, otherwise false.
|
||||
*/
|
||||
public isOpen(): boolean {
|
||||
return this.ws?.readyState === WebSocket?.OPEN;
|
||||
return this.ws?.readyState === WebSocket?.OPEN
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,7 +109,7 @@ export default class WebSocketService {
|
||||
* @returns {boolean} - Returns 'true' if the WebSocket is connecting, otherwise 'false'.
|
||||
*/
|
||||
public isConnecting(): boolean {
|
||||
return this.ws?.readyState === WebSocket?.CONNECTING;
|
||||
return this.ws?.readyState === WebSocket?.CONNECTING
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,6 +118,6 @@ export default class WebSocketService {
|
||||
* @returns {boolean} Returns true if the WebSocket connection is closed, false otherwise.
|
||||
*/
|
||||
public isClosed(): boolean {
|
||||
return this.ws?.readyState === WebSocket?.CLOSED;
|
||||
return this.ws?.readyState === WebSocket?.CLOSED
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ public class GameController : GenericController
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(e.Message); // TODO not necessary?
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user