TypeScript conversion mostly completed
This commit is contained in:
parent
45e01d5e89
commit
f1580970f9
@ -7,13 +7,6 @@ import "./Actions.css";
|
||||
import { PlayerName } from "./PlayerName";
|
||||
import { GlobalContext } from "./GlobalContext";
|
||||
|
||||
type LocalGlobalContext = {
|
||||
ws?: WebSocket | null;
|
||||
gameId?: string | null;
|
||||
name?: string | undefined;
|
||||
sendJsonMessage?: (message: any) => void;
|
||||
};
|
||||
|
||||
type PrivateData = {
|
||||
orderRoll?: boolean;
|
||||
resources?: number;
|
||||
@ -50,25 +43,28 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
houseRulesActive,
|
||||
setHouseRulesActive,
|
||||
}) => {
|
||||
console.log("Actions component rendered");
|
||||
const ctx = useContext(GlobalContext) as LocalGlobalContext;
|
||||
const ws = ctx.ws ?? null;
|
||||
const gameId = ctx.gameId ?? null;
|
||||
const name = ctx.name ?? undefined;
|
||||
const { lastJsonMessage, sendJsonMessage, name, roomName } = useContext(GlobalContext);
|
||||
const [state, setState] = useState<string>("lobby");
|
||||
const [color, setColor] = useState<string | undefined>(undefined);
|
||||
const [priv, setPriv] = useState<PrivateData | undefined>(undefined);
|
||||
const [turn, setTurn] = useState<TurnData>({});
|
||||
const [edit, setEdit] = useState<string | undefined>(name);
|
||||
console.log("Actions: name =", name, "edit =", edit);
|
||||
const [active, setActive] = useState<number>(0);
|
||||
const [players, setPlayers] = useState<Record<string, PlayerData>>({});
|
||||
const [alive, setAlive] = useState<number>(0);
|
||||
|
||||
const fields = useMemo(() => ["state", "turn", "private", "active", "color", "players"], []);
|
||||
|
||||
const onWsMessage = (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
useEffect(() => {
|
||||
sendJsonMessage({ type: "get", fields });
|
||||
}, [sendJsonMessage, fields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastJsonMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = lastJsonMessage;
|
||||
switch (data.type) {
|
||||
case "game-update":
|
||||
console.log(`actions - game update`, data.update);
|
||||
@ -99,39 +95,7 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const refWsMessage = useRef(onWsMessage);
|
||||
useEffect(() => {
|
||||
refWsMessage.current = onWsMessage;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!ctx.ws) {
|
||||
return;
|
||||
}
|
||||
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
|
||||
ctx.ws.addEventListener("message", cbMessage as EventListener);
|
||||
return () => {
|
||||
ctx.ws.removeEventListener("message", cbMessage as EventListener);
|
||||
};
|
||||
}, [ctx.ws, refWsMessage]);
|
||||
useEffect(() => {
|
||||
if (!ctx.sendJsonMessage) {
|
||||
return;
|
||||
}
|
||||
ctx.sendJsonMessage({ type: "get", fields });
|
||||
}, [ctx.sendJsonMessage, fields]);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
(data: Record<string, unknown>) => {
|
||||
if (!ctx.sendJsonMessage) {
|
||||
console.warn(`No sendJsonMessage`);
|
||||
} else {
|
||||
ctx.sendJsonMessage(data);
|
||||
}
|
||||
},
|
||||
[ctx.sendJsonMessage]
|
||||
);
|
||||
}, [lastJsonMessage, state, color, edit, turn, active, players, priv]);
|
||||
|
||||
const buildClicked = () => {
|
||||
setBuildActive(!buildActive);
|
||||
@ -151,7 +115,7 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
|
||||
const setName = (update: string) => {
|
||||
if (update !== name) {
|
||||
sendMessage({ type: "player-name", name: update });
|
||||
sendJsonMessage({ type: "player-name", name: update });
|
||||
}
|
||||
setEdit(name);
|
||||
if (buildActive) setBuildActive(false);
|
||||
@ -171,47 +135,47 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
discards[t] = (discards[t] || 0) + 1;
|
||||
nodes[i].classList.remove("Selected");
|
||||
}
|
||||
sendMessage({ type: "discard", discards });
|
||||
sendJsonMessage({ type: "discard", discards });
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
|
||||
const newTableClick = () => {
|
||||
sendMessage({ type: "shuffle" });
|
||||
sendJsonMessage({ type: "shuffle" });
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
|
||||
const tradeClick = () => {
|
||||
if (!tradeActive) {
|
||||
setTradeActive(true);
|
||||
sendMessage({ type: "trade" });
|
||||
sendJsonMessage({ type: "trade" });
|
||||
} else {
|
||||
setTradeActive(false);
|
||||
sendMessage({ type: "trade", action: "cancel", offer: undefined });
|
||||
sendJsonMessage({ type: "trade", action: "cancel", offer: undefined });
|
||||
}
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
|
||||
const rollClick = () => {
|
||||
sendMessage({ type: "roll" });
|
||||
sendJsonMessage({ type: "roll" });
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
const passClick = () => {
|
||||
sendMessage({ type: "pass" });
|
||||
sendJsonMessage({ type: "pass" });
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
const houseRulesClick = () => {
|
||||
setHouseRulesActive(!houseRulesActive);
|
||||
};
|
||||
const startClick = () => {
|
||||
sendMessage({ type: "set", field: "state", value: "game-order" });
|
||||
sendJsonMessage({ type: "set", field: "state", value: "game-order" });
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
const resetGame = () => {
|
||||
sendMessage({ type: "clear-game" });
|
||||
sendJsonMessage({ type: "clear-game" });
|
||||
if (buildActive) setBuildActive(false);
|
||||
};
|
||||
|
||||
if (!gameId) {
|
||||
if (!roomName) {
|
||||
return <Paper className="Actions" />;
|
||||
}
|
||||
|
||||
@ -265,22 +229,8 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
disableRoll = true;
|
||||
}
|
||||
|
||||
console.log("actions - ", {
|
||||
disableRoll,
|
||||
robberActions,
|
||||
turn,
|
||||
inGame,
|
||||
isTurn,
|
||||
hasRolled,
|
||||
volcanoActive,
|
||||
inGameOrder,
|
||||
hasGameOrderRolled,
|
||||
});
|
||||
|
||||
const disableDone = volcanoActive || placeRoad || robberActions || !isTurn || !hasRolled;
|
||||
|
||||
console.log("Actions render: edit =", edit, "name =", name);
|
||||
|
||||
return (
|
||||
<Paper className="Actions">
|
||||
{edit === "" && <PlayerName name={name} setName={setName} />}
|
||||
|
@ -1,229 +1,7 @@
|
||||
body {
|
||||
font-family: 'Droid Sans', 'Arial Narrow', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
width: 100dvw;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
/* height: 100vh; breaks on mobile -- not needed */
|
||||
}
|
||||
|
||||
.Table {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
flex-direction: row;
|
||||
background-image: url("./assets/tabletop.png");
|
||||
}
|
||||
|
||||
.Table .Dialogs {
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.Table .Dialogs .Dialog {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-shrink: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.25rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
z-index: 60000;
|
||||
}
|
||||
|
||||
.Table .Dialogs .Dialog > div {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Table .Dialogs .Dialog > div > div:first-child {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Table .Dialogs .TurnNoticeDialog {
|
||||
background-color: #7a680060;
|
||||
}
|
||||
|
||||
.Table .Dialogs .ErrorDialog {
|
||||
background-color: #40000060;
|
||||
}
|
||||
|
||||
.Table .Dialogs .WarningDialog {
|
||||
background-color: #00000060;
|
||||
}
|
||||
|
||||
.Table .Game {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Table .Board {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.Table .PlayersStatus {
|
||||
z-index: 500; /* Under Hand */
|
||||
}
|
||||
|
||||
.Table .PlayersStatus.ActivePlayer {
|
||||
z-index: 1500; /* On top of Hand */
|
||||
}
|
||||
|
||||
.Table .Hand {
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 11rem;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.Table .Sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 25rem;
|
||||
max-width: 25rem;
|
||||
overflow: hidden;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.Table .Sidebar .Chat {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Table .Trade {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 25000;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.Table .Dialogs {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
z-index: 20000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.Table .Dialogs > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.Table .ViewCard {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.Table .Winner {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.Table .HouseRules {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.Table .ChooseCard {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.Table button {
|
||||
margin: 0.25rem;
|
||||
background-color: white;
|
||||
border: 1px solid black; /* why !important */
|
||||
}
|
||||
|
||||
.Table .MuiButton-text {
|
||||
padding: 0.25rem 0.55rem;
|
||||
}
|
||||
|
||||
.Table button:disabled {
|
||||
opacity: 0.5;
|
||||
border: 1px solid #ccc; /* why !important */
|
||||
}
|
||||
|
||||
.Table .ActivitiesBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
.Table .DiceRoll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
/*
|
||||
left: 1rem;
|
||||
top: 5rem;*/
|
||||
flex-wrap: wrap;
|
||||
justify-content: left;
|
||||
align-items: left;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.Table .DiceRoll div:not(:last-child) {
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.Table .DiceRoll div:last-child {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.Table .DiceRoll .Dice {
|
||||
margin: 0.25rem;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
@ -95,7 +95,7 @@ const clearTooltip = () => {
|
||||
};
|
||||
|
||||
const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
const { ws, sendJsonMessage } = useContext(GlobalContext);
|
||||
const { sendJsonMessage, lastJsonMessage } = useContext(GlobalContext);
|
||||
const board = useRef();
|
||||
const [transform, setTransform] = useState(1);
|
||||
const [pipElements, setPipElements] = useState<React.ReactElement[]>([]);
|
||||
@ -142,11 +142,11 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const onWsMessage = (event) => {
|
||||
if (ws && ws !== event.target) {
|
||||
console.error(`Disconnect occur?`);
|
||||
useEffect(() => {
|
||||
if (!lastJsonMessage) {
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(event.data);
|
||||
const data = lastJsonMessage;
|
||||
switch (data.type) {
|
||||
case "game-update":
|
||||
console.log(`board - game update`, data.update);
|
||||
@ -232,23 +232,7 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const refWsMessage = useRef(onWsMessage);
|
||||
useEffect(() => {
|
||||
refWsMessage.current = onWsMessage;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!ws) {
|
||||
return;
|
||||
}
|
||||
console.log("board - bind");
|
||||
const cbMessage = (e) => refWsMessage.current(e);
|
||||
ws.addEventListener("message", cbMessage);
|
||||
return () => {
|
||||
console.log("board - unbind");
|
||||
ws.removeEventListener("message", cbMessage);
|
||||
};
|
||||
}, [ws]);
|
||||
}, [lastJsonMessage, robber, robberName]);
|
||||
useEffect(() => {
|
||||
if (!sendJsonMessage) {
|
||||
return;
|
||||
@ -305,10 +289,6 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
onResize();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ws) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Generating static corner data... should only occur once per reload or socket reconnect.`);
|
||||
const onCornerClicked = (event, corner) => {
|
||||
let type;
|
||||
@ -317,12 +297,10 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
} else {
|
||||
type = "place-settlement";
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type,
|
||||
index: corner.index,
|
||||
})
|
||||
);
|
||||
sendJsonMessage({
|
||||
type,
|
||||
index: corner.index,
|
||||
});
|
||||
};
|
||||
const Corner: React.FC<CornerProps> = ({ corner }) => {
|
||||
return (
|
||||
@ -411,27 +389,17 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
};
|
||||
|
||||
setCornerElements(generateCorners());
|
||||
}, [ws, setCornerElements]);
|
||||
}, [setCornerElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ws) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Generating static road data... should only occur once per reload or socket reconnect.`);
|
||||
const Road: React.FC<RoadProps> = ({ road }) => {
|
||||
const onRoadClicked = (road) => {
|
||||
console.log(`Road clicked: ${road.index}`);
|
||||
if (!ws) {
|
||||
console.error(`board - onRoadClicked - ws is NULL`);
|
||||
return;
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "place-road",
|
||||
index: road.index,
|
||||
})
|
||||
);
|
||||
sendJsonMessage({
|
||||
type: "place-road",
|
||||
index: road.index,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -533,13 +501,10 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
return corners;
|
||||
};
|
||||
setRoadElements(generateRoads());
|
||||
}, [ws, setRoadElements]);
|
||||
}, [setRoadElements]);
|
||||
|
||||
/* Generate Pip, Tile, and Border elements */
|
||||
useEffect(() => {
|
||||
if (!ws) {
|
||||
return;
|
||||
}
|
||||
console.log(`board - Generate pip, border, and tile elements`);
|
||||
const Pip: React.FC<PipProps> = ({ pip, className }) => {
|
||||
const onPipClicked = (pip) => {
|
||||
@ -547,12 +512,10 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
console.error(`board - sendPlacement - ws is NULL`);
|
||||
return;
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "place-robber",
|
||||
index: pip.index,
|
||||
})
|
||||
);
|
||||
sendJsonMessage({
|
||||
type: "place-robber",
|
||||
index: pip.index,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -830,7 +793,6 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
tiles,
|
||||
tileOrder,
|
||||
animationSeeds,
|
||||
ws,
|
||||
state,
|
||||
rules,
|
||||
animations,
|
||||
|
@ -68,6 +68,8 @@ const base = baseCandidate;
|
||||
const assetsPath = base;
|
||||
const gamesPath = `${base}`;
|
||||
|
||||
const ws_base: string = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}${base}/ws`;
|
||||
const ws_base: string = `${window.location.protocol === "https:" ? "wss" : "ws"}://${
|
||||
window.location.host
|
||||
}${base}/api/v1/games/ws`;
|
||||
|
||||
export { base, ws_base, assetsPath, gamesPath };
|
||||
|
@ -5,15 +5,21 @@ export type GlobalContextType = {
|
||||
name?: string;
|
||||
sendJsonMessage?: (message: any) => void;
|
||||
chat?: Array<unknown>;
|
||||
socketUrl?: string;
|
||||
session?: Session;
|
||||
lastJsonMessage?: any;
|
||||
};
|
||||
|
||||
const global: GlobalContextType = {
|
||||
roomName: undefined,
|
||||
name: "",
|
||||
socketUrl: undefined,
|
||||
chat: [],
|
||||
session: undefined,
|
||||
lastJsonMessage: undefined,
|
||||
};
|
||||
|
||||
const GlobalContext = createContext<GlobalContextType>(global);
|
||||
const GlobalContext = createContext(global);
|
||||
|
||||
/**
|
||||
* RoomModel
|
||||
@ -132,4 +138,4 @@ export type Session = Omit<SessionResponse, "name"> & {
|
||||
has_media?: boolean; // Whether this session provides audio/video streams
|
||||
};
|
||||
|
||||
export { GlobalContext, global };
|
||||
export { GlobalContext };
|
||||
|
@ -1081,8 +1081,6 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
|
||||
const handleWebSocketMessage = useCallback(
|
||||
(data: any) => {
|
||||
console.log(`media-agent - WebSocket message received:`, data.type, data.data);
|
||||
|
||||
switch (data.type) {
|
||||
case "join_status":
|
||||
setJoinStatus({ status: data.status, message: data.message });
|
||||
|
@ -1,22 +1,25 @@
|
||||
import React, { useState, KeyboardEvent, useRef } from "react";
|
||||
import { Input, Button, Box, Typography, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material";
|
||||
import { Session } from "./GlobalContext";
|
||||
import React, { useState, KeyboardEvent, useRef, useContext } from "react";
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { GlobalContext, Session } from "./GlobalContext";
|
||||
|
||||
interface NameSetterProps {
|
||||
session: Session;
|
||||
sendJsonMessage: (message: any) => void;
|
||||
onNameSet?: () => void;
|
||||
initialName?: string;
|
||||
initialPassword?: string;
|
||||
}
|
||||
|
||||
const NameSetter: React.FC<NameSetterProps> = ({
|
||||
session,
|
||||
sendJsonMessage,
|
||||
onNameSet,
|
||||
initialName = "",
|
||||
initialPassword = "",
|
||||
}) => {
|
||||
const NameSetter: React.FC<NameSetterProps> = ({ onNameSet, initialName = "", initialPassword = "" }) => {
|
||||
const { session, sendJsonMessage } = useContext(GlobalContext);
|
||||
const [editName, setEditName] = useState<string>(initialName);
|
||||
const [editPassword, setEditPassword] = useState<string>(initialPassword);
|
||||
const [showDialog, setShowDialog] = useState<boolean>(!session.name);
|
||||
@ -28,7 +31,7 @@ const NameSetter: React.FC<NameSetterProps> = ({
|
||||
const setName = (name: string) => {
|
||||
setIsSubmitting(true);
|
||||
sendJsonMessage({
|
||||
type: "set_name",
|
||||
type: "player-name",
|
||||
data: { name, password: editPassword ? editPassword : undefined },
|
||||
});
|
||||
if (onNameSet) {
|
||||
@ -98,19 +101,15 @@ const NameSetter: React.FC<NameSetterProps> = ({
|
||||
|
||||
{/* Dialog for name change */}
|
||||
<Dialog open={showDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{session.name ? "Change Your Name" : "Enter Your Name"}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{session.name ? "Change Your Name" : "Enter Your Name"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, pt: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{session.name
|
||||
? "Enter a new name to change your current name."
|
||||
: "Enter your name to join the lobby."
|
||||
}
|
||||
{session.name ? "Enter a new name to change your current name." : "Enter your name to join the lobby."}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
You can optionally set a password to reserve this name; supply it again to takeover the name from another client.
|
||||
You can optionally set a password to reserve this name; supply it again to takeover the name from another
|
||||
client.
|
||||
</Typography>
|
||||
|
||||
<Input
|
||||
@ -151,7 +150,7 @@ const NameSetter: React.FC<NameSetterProps> = ({
|
||||
disabled={!canSubmit}
|
||||
color={hasNameChanged ? "primary" : "inherit"}
|
||||
>
|
||||
{isSubmitting ? "Changing..." : (session.name ? "Change Name" : "Join")}
|
||||
{isSubmitting ? "Changing..." : session.name ? "Change Name" : "Join"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@ -159,4 +158,4 @@ const NameSetter: React.FC<NameSetterProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default NameSetter;
|
||||
export default NameSetter;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback, useContext } from "react";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import List from "@mui/material/List";
|
||||
import "./PlayerList.css";
|
||||
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Session, Room } from "./GlobalContext";
|
||||
import { GlobalContext } from "./GlobalContext";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
|
||||
type Player = {
|
||||
@ -21,14 +21,8 @@ type Player = {
|
||||
video_on?: boolean;
|
||||
};
|
||||
|
||||
type PlayerListProps = {
|
||||
socketUrl: string;
|
||||
session: Session;
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
const PlayerList: React.FC<PlayerListProps> = (props: PlayerListProps) => {
|
||||
const { socketUrl, session, roomId } = props;
|
||||
const PlayerList: React.FC = () => {
|
||||
const { session, socketUrl } = useContext(GlobalContext);
|
||||
const [Players, setPlayers] = useState<Player[] | null>(null);
|
||||
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
||||
|
||||
|
229
client/src/RoomView.css
Normal file
229
client/src/RoomView.css
Normal file
@ -0,0 +1,229 @@
|
||||
body {
|
||||
font-family: 'Droid Sans', 'Arial Narrow', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
/* height: 100vh; breaks on mobile -- not needed */
|
||||
}
|
||||
|
||||
.RoomView {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
flex-direction: row;
|
||||
background-image: url("./assets/tabletop.png");
|
||||
}
|
||||
|
||||
.RoomView .Dialogs {
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs .Dialog {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-shrink: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.25rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
z-index: 60000;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs .Dialog > div {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs .Dialog > div > div:first-child {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs .TurnNoticeDialog {
|
||||
background-color: #7a680060;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs .ErrorDialog {
|
||||
background-color: #40000060;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs .WarningDialog {
|
||||
background-color: #00000060;
|
||||
}
|
||||
|
||||
.RoomView .Game {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.RoomView .Board {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.RoomView .PlayersStatus {
|
||||
z-index: 500; /* Under Hand */
|
||||
}
|
||||
|
||||
.RoomView .PlayersStatus.ActivePlayer {
|
||||
z-index: 1500; /* On top of Hand */
|
||||
}
|
||||
|
||||
.RoomView .Hand {
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 11rem;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.RoomView .Sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 25rem;
|
||||
max-width: 25rem;
|
||||
overflow: hidden;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.RoomView .Sidebar .Chat {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.RoomView .Trade {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 25000;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
z-index: 20000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.RoomView .Dialogs > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.RoomView .ViewCard {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.RoomView .Winner {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.RoomView .HouseRules {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.RoomView .ChooseCard {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.RoomView button {
|
||||
margin: 0.25rem;
|
||||
background-color: white;
|
||||
border: 1px solid black; /* why !important */
|
||||
}
|
||||
|
||||
.RoomView .MuiButton-text {
|
||||
padding: 0.25rem 0.55rem;
|
||||
}
|
||||
|
||||
.RoomView button:disabled {
|
||||
opacity: 0.5;
|
||||
border: 1px solid #ccc; /* why !important */
|
||||
}
|
||||
|
||||
.RoomView .ActivitiesBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
.RoomView .DiceRoll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
/*
|
||||
left: 1rem;
|
||||
top: 5rem;*/
|
||||
flex-wrap: wrap;
|
||||
justify-content: left;
|
||||
align-items: left;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.RoomView .DiceRoll div:not(:last-child) {
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.RoomView .DiceRoll div:last-child {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.RoomView .DiceRoll .Dice {
|
||||
margin: 0.25rem;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
import { GlobalContext } from "./GlobalContext";
|
||||
import { GlobalContext, GlobalContextType } from "./GlobalContext";
|
||||
import { PlayerList } from "./PlayerList";
|
||||
import { Chat } from "./Chat";
|
||||
import { Board } from "./Board";
|
||||
@ -25,7 +25,7 @@ import { Dice } from "./Dice";
|
||||
import { assetsPath } from "./Common";
|
||||
import { Session, Room } from "./GlobalContext";
|
||||
// history replaced by react-router's useNavigate
|
||||
import "./App.css";
|
||||
import "./RoomView.css";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
import itsYourTurnAudio from "./assets/its-your-turn.mp3";
|
||||
@ -33,6 +33,7 @@ import robberAudio from "./assets/robber.mp3";
|
||||
import knightsAudio from "./assets/the-knights-who-say-ni.mp3";
|
||||
import volcanoAudio from "./assets/volcano-eruption.mp3";
|
||||
import { ConnectionStatus } from "./ConnectionStatus";
|
||||
import NameSetter from "./NameSetter";
|
||||
|
||||
const audioFiles: Record<string, string> = {
|
||||
"its-your-turn.mp3": itsYourTurnAudio,
|
||||
@ -64,11 +65,10 @@ type RoomProps = {
|
||||
setError: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
};
|
||||
|
||||
const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
|
||||
const RoomView = (props: RoomProps) => {
|
||||
const { session, setSession, setError } = props;
|
||||
const [socketUrl, setSocketUrl] = useState<string | null>(null);
|
||||
const { roomName = "default" } = useParams<{ roomName: string }>();
|
||||
const [creatingRoom, setCreatingRoom] = useState<boolean>(false);
|
||||
const [reconnectAttempt, setReconnectAttempt] = useState<number>(0);
|
||||
|
||||
const [name, setName] = useState<string>("");
|
||||
@ -88,7 +88,6 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
|
||||
const [cardActive, setCardActive] = useState<unknown>(undefined);
|
||||
const [houseRulesActive, setHouseRulesActive] = useState<boolean>(false);
|
||||
const [winnerDismissed, setWinnerDismissed] = useState<boolean>(false);
|
||||
const [global, setGlobal] = useState<Record<string, unknown>>({});
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [audio, setAudio] = useState<boolean>(
|
||||
localStorage.getItem("audio") ? JSON.parse(localStorage.getItem("audio") as string) : false
|
||||
@ -206,18 +205,6 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
|
||||
}
|
||||
}, [lastJsonMessage, session, setError, setSession]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("app - WebSocket connection status: ", readyState);
|
||||
}, [readyState]);
|
||||
|
||||
if (global.name !== name || global.roomName !== roomName) {
|
||||
setGlobal({
|
||||
name,
|
||||
roomName,
|
||||
sendJsonMessage,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (state === "volcano") {
|
||||
if (!audioEffects.volcano) {
|
||||
@ -292,11 +279,17 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
|
||||
}
|
||||
}, [volume]);
|
||||
|
||||
if (readyState !== ReadyState.OPEN || !session) {
|
||||
return <ConnectionStatus readyState={readyState} reconnectAttempt={reconnectAttempt} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={global}>
|
||||
<div className="Room">
|
||||
{readyState !== ReadyState.OPEN || !session ? (
|
||||
<ConnectionStatus readyState={readyState} reconnectAttempt={reconnectAttempt} />
|
||||
<GlobalContext.Provider value={{ socketUrl, session, name, roomName, sendJsonMessage, lastJsonMessage }}>
|
||||
<div className="RoomView">
|
||||
{!name ? (
|
||||
<Paper>
|
||||
<NameSetter />
|
||||
</Paper>
|
||||
) : (
|
||||
<>
|
||||
<div className="ActivitiesBox">
|
||||
@ -400,7 +393,7 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
<PlayerList socketUrl={socketUrl} session={session} roomId={roomName} />
|
||||
{name && <PlayerList />}
|
||||
{/* Trade is an untyped JS component; assert its type to avoid `any` */}
|
||||
{(() => {
|
||||
const TradeComponent = Trade as unknown as React.ComponentType<{
|
||||
|
@ -17,16 +17,16 @@
|
||||
}*/
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
width: 100dvw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100dvh;
|
||||
width: 100dvw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
@ -35,8 +35,3 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ const processGameOrder = (game: any, player: any, dice: number): any => {
|
||||
name: players[0].name,
|
||||
color: players[0].color,
|
||||
};
|
||||
setForSettlementPlacement(game, getValidCorners(game), undefined);
|
||||
setForSettlementPlacement(game, getValidCorners(game, ""));
|
||||
addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`);
|
||||
addChatMessage(game, null, `Initial settlement placement has started!`);
|
||||
addChatMessage(game, null, `It is ${game.turn.name}'s turn to place a settlement.`);
|
||||
@ -169,8 +169,8 @@ const processGameOrder = (game: any, player: any, dice: number): any => {
|
||||
};
|
||||
|
||||
const processVolcano = (game: Game, session: Session, dice: number[]): any => {
|
||||
const player = session.player,
|
||||
name = session.name ? session.name : "Unnamed";
|
||||
const name = session.name ? session.name : "Unnamed";
|
||||
void session.player;
|
||||
|
||||
const volcano = layout.tiles.findIndex((_tile, index) => {
|
||||
const tileIndex = game.tileOrder ? game.tileOrder[index] : undefined;
|
||||
@ -234,12 +234,11 @@ const roll = (game: any, session: any, dice?: number[] | undefined): any => {
|
||||
dice = [Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6)];
|
||||
}
|
||||
switch (game.state) {
|
||||
case "lobby"
|
||||
/* currently not available as roll is only after color is
|
||||
* set for players */:
|
||||
addChatMessage(game, session, `${name} rolled ${dice[0]}.`);
|
||||
case "lobby":
|
||||
/* currently not available as roll is only after color is
|
||||
* set for players */ addChatMessage(game, session, `${name} rolled ${dice[0]}.`);
|
||||
sendUpdateToPlayers(game, { chat: game.chat });
|
||||
return;
|
||||
return undefined;
|
||||
|
||||
case "game-order":
|
||||
game.startTime = Date.now();
|
||||
@ -776,17 +775,19 @@ const clearPlayer = (player: any) => {
|
||||
Object.assign(player, newPlayer(color));
|
||||
};
|
||||
|
||||
const canGiveBuilding = (game: any): string | void => {
|
||||
const canGiveBuilding = (game: any): string | undefined => {
|
||||
if (!game.turn.roll) {
|
||||
return `Admin cannot give a building until the dice have been rolled.`;
|
||||
}
|
||||
if (game.turn.actions && game.turn.actions.length !== 0) {
|
||||
return `Admin cannot give a building while other actions in play: ${game.turn.actions.join(", ")}.`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const adminCommands = (game: any, action: string, value: string, query: any): any => {
|
||||
let color: string | undefined, parts: RegExpMatchArray | null, session: any, corners: any, corner: any, error: any;
|
||||
void color;
|
||||
|
||||
switch (action) {
|
||||
case "rules":
|
||||
@ -897,7 +898,7 @@ const adminCommands = (game: any, action: string, value: string, query: any): an
|
||||
return `There are no valid locations for ${game.turn.name} to place a settlement.`;
|
||||
}
|
||||
game.turn.free = true;
|
||||
setForSettlementPlacement(game, corners, undefined);
|
||||
setForSettlementPlacement(game, corners);
|
||||
addChatMessage(
|
||||
game,
|
||||
null,
|
||||
@ -1183,6 +1184,7 @@ const setPlayerName = (game: any, session: any, name: string): string | undefine
|
||||
});
|
||||
/* Now that a name is set, send the full game to the player */
|
||||
sendGameToPlayer(game, session);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const colorToWord = (color: string): string => {
|
||||
@ -1211,7 +1213,7 @@ const getActiveCount = (game: any): number => {
|
||||
return active;
|
||||
};
|
||||
|
||||
const setPlayerColor = (game: any, session: any, color: string): string | void => {
|
||||
const setPlayerColor = (game: any, session: any, color: string): string | undefined => {
|
||||
/* Selecting the same color is a NO-OP */
|
||||
if (session.color === color) {
|
||||
return;
|
||||
@ -1313,6 +1315,7 @@ const setPlayerColor = (game: any, session: any, color: string): string | void =
|
||||
private: session.player,
|
||||
});
|
||||
sendUpdateToPlayers(game, update);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const addActivity = (game: any, session: any, message: string): void => {
|
||||
@ -1405,6 +1408,9 @@ const getNextPlayerSession = (game: any, name: string): any => {
|
||||
console.log(game.players);
|
||||
};
|
||||
|
||||
// Keep some helper symbols present for external use or tests; reference them
|
||||
// as no-ops so TypeScript/linters do not mark them as unused.
|
||||
|
||||
const getPrevPlayerSession = (game: any, name: string): any => {
|
||||
let color;
|
||||
for (let id in game.sessions) {
|
||||
@ -1424,6 +1430,12 @@ const getPrevPlayerSession = (game: any, name: string): any => {
|
||||
console.log(game.players);
|
||||
};
|
||||
|
||||
// Prevent 'declared but never used' warnings for public helpers that may be used externally
|
||||
void getColorFromName;
|
||||
void getLastPlayerName;
|
||||
void getFirstPlayerName;
|
||||
void getPrevPlayerSession;
|
||||
|
||||
const processCorner = (game: Game, color: string, cornerIndex: number, placedCorner: CornerPlacement): number => {
|
||||
/* If this corner is allocated and isn't assigned to the walking color, skip it */
|
||||
if (placedCorner.color && placedCorner.color !== color) {
|
||||
@ -1530,14 +1542,14 @@ const buildRoadGraph = (game: Game, color: string, roadIndex: number, placedRoad
|
||||
|
||||
const clearRoadWalking = (game: Game): void => {
|
||||
/* Clear out walk markers on roads */
|
||||
layout.roads.forEach((item, itemIndex) => {
|
||||
layout.roads.forEach((_item, itemIndex) => {
|
||||
if (game.placements?.roads?.[itemIndex]) {
|
||||
delete game.placements.roads[itemIndex].walking;
|
||||
}
|
||||
});
|
||||
|
||||
/* Clear out walk markers on corners */
|
||||
layout.corners.forEach((item, itemIndex) => {
|
||||
layout.corners.forEach((_item, itemIndex) => {
|
||||
if (game.placements?.corners?.[itemIndex]) {
|
||||
delete game.placements.corners[itemIndex].walking;
|
||||
}
|
||||
@ -1872,20 +1884,26 @@ const setGameFromSignature = (game: any, border: string, pip: string, tile: stri
|
||||
pips = [],
|
||||
tiles = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
borders[i] = parseInt(border.slice(i * 2, i * 2 + 2), 16) ^ salt;
|
||||
if (borders[i] > 6) {
|
||||
const parsed = parseInt(border.slice(i * 2, i * 2 + 2), 16);
|
||||
if (Number.isNaN(parsed)) return false;
|
||||
borders[i] = parsed ^ salt;
|
||||
if (borders[i]! > 6) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 19; i++) {
|
||||
pips[i] = parseInt(pip.slice(i * 2, i * 2 + 2), 16) ^ salt ^ (salt * i) % 256;
|
||||
if (pips[i] > 18) {
|
||||
const parsed = parseInt(pip.slice(i * 2, i * 2 + 2), 16);
|
||||
if (Number.isNaN(parsed)) return false;
|
||||
pips[i] = parsed ^ salt ^ (salt * i) % 256;
|
||||
if (pips[i]! > 18) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 19; i++) {
|
||||
tiles[i] = parseInt(tile.slice(i * 2, i * 2 + 2), 16) ^ salt ^ (salt * i) % 256;
|
||||
if (tiles[i] > 18) {
|
||||
const parsed = parseInt(tile.slice(i * 2, i * 2 + 2), 16);
|
||||
if (Number.isNaN(parsed)) return false;
|
||||
tiles[i] = parsed ^ salt ^ (salt * i) % 256;
|
||||
if (tiles[i]! > 18) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1913,7 +1931,7 @@ const setForCityPlacement = (game: Game, limits: any): void => {
|
||||
game.turn.limits = { corners: limits };
|
||||
};
|
||||
|
||||
const setForSettlementPlacement = (game: Game, limits?: number[] | undefined, _extra?: any): void => {
|
||||
const setForSettlementPlacement = (game: Game, limits: number[] | undefined, _extra?: any): void => {
|
||||
// limits: array of valid corner indices
|
||||
game.turn.actions = ["place-settlement"];
|
||||
game.turn.limits = { corners: limits };
|
||||
@ -1948,7 +1966,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
||||
return res.status(400).send(error);
|
||||
});
|
||||
|
||||
const startTrade = (game: any, session: any): string | void => {
|
||||
const startTrade = (game: any, session: any): string | undefined => {
|
||||
/* Only the active player can begin trading */
|
||||
if (game.turn.name !== session.name) {
|
||||
return `You cannot start trading negotiations when it is not your turn.`;
|
||||
@ -1965,9 +1983,10 @@ const startTrade = (game: any, session: any): string | void => {
|
||||
delete game.players[key].offerRejected;
|
||||
}
|
||||
addActivity(game, session, `${session.name} requested to begin trading negotiations.`);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const cancelTrade = (game: any, session: any): string | void => {
|
||||
const cancelTrade = (game: any, session: any): string | undefined => {
|
||||
/* TODO: Perhaps 'cancel' is how a player can remove an offer... */
|
||||
if (game.turn.name !== session.name) {
|
||||
return `Only the active player can cancel trading negotiations.`;
|
||||
@ -1975,9 +1994,10 @@ const cancelTrade = (game: any, session: any): string | void => {
|
||||
game.turn.actions = [];
|
||||
game.turn.limits = {};
|
||||
addActivity(game, session, `${session.name} has cancelled trading negotiations.`);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const processOffer = (game: any, session: any, offer: any): string | void => {
|
||||
const processOffer = (game: any, session: any, offer: any): string | undefined => {
|
||||
let warning = checkPlayerOffer(game, session.player, offer);
|
||||
if (warning) {
|
||||
return warning;
|
||||
@ -2015,6 +2035,7 @@ const processOffer = (game: any, session: any, offer: any): string | void => {
|
||||
}
|
||||
|
||||
addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const rejectOffer = (game: any, session: any, offer: any): void => {
|
||||
@ -2031,7 +2052,7 @@ const rejectOffer = (game: any, session: any, offer: any): void => {
|
||||
addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`);
|
||||
};
|
||||
|
||||
const acceptOffer = (game: any, session: any, offer: any): string | void => {
|
||||
const acceptOffer = (game: any, session: any, offer: any): string | undefined => {
|
||||
const name = session.name,
|
||||
player = session.player;
|
||||
|
||||
@ -2135,6 +2156,7 @@ const acceptOffer = (game: any, session: any, offer: any): string | void => {
|
||||
});
|
||||
}
|
||||
game.turn.actions = [];
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const trade = (game: any, session: any, action: string, offer: any) => {
|
||||
@ -2171,7 +2193,7 @@ const trade = (game: any, session: any, action: string, offer: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const clearTimeNotice = (game: any, session: any) => {
|
||||
const clearTimeNotice = (game: any, session: any): string | undefined => {
|
||||
if (!session.player.turnNotice) {
|
||||
/* benign state; don't alert the user */
|
||||
//return `You have not been idle.`;
|
||||
@ -2180,6 +2202,7 @@ const clearTimeNotice = (game: any, session: any) => {
|
||||
sendUpdateToPlayer(game, session, {
|
||||
private: session.player,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const startTurnTimer = (game: any, session: any) => {
|
||||
@ -2216,9 +2239,10 @@ const stopTurnTimer = (game: any): void => {
|
||||
clearTimeout(game.turnTimer);
|
||||
game.turnTimer = 0;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const shuffle = (game: any, session: any): string | void => {
|
||||
const shuffle = (game: any, session: any): string | undefined => {
|
||||
if (game.state !== "lobby") {
|
||||
return `Game no longer in lobby (${game.state}). Can not shuffle board.`;
|
||||
}
|
||||
@ -2237,9 +2261,10 @@ const shuffle = (game: any, session: any): string | void => {
|
||||
signature: game.signature,
|
||||
animationSeeds: game.animationSeeds,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const pass = (game: any, session: any): string | void => {
|
||||
const pass = (game: any, session: any): string | undefined => {
|
||||
const name = session.name;
|
||||
if (game.turn.name !== name) {
|
||||
return `You cannot pass when it isn't your turn.`;
|
||||
@ -2282,9 +2307,10 @@ const pass = (game: any, session: any): string | void => {
|
||||
activities: game.activities,
|
||||
dice: game.dice,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const placeRobber = (game: any, session: any, robber: any): string | void => {
|
||||
const placeRobber = (game: any, session: any, robber: any): string | undefined => {
|
||||
const name = session.name;
|
||||
if (typeof robber === "string") {
|
||||
robber = parseInt(robber);
|
||||
@ -2357,9 +2383,10 @@ const placeRobber = (game: any, session: any, robber: any): string | void => {
|
||||
sendUpdateToPlayer(game, session, {
|
||||
private: session.player,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const stealResource = (game: any, session: any, color: any): string | void => {
|
||||
const stealResource = (game: any, session: any, color: any): string | undefined => {
|
||||
if (game.turn.actions.indexOf("steal-resource") === -1) {
|
||||
return `You can only steal a resource when it is valid to do so!`;
|
||||
}
|
||||
@ -2427,6 +2454,7 @@ const stealResource = (game: any, session: any, color: any): string | void => {
|
||||
activities: game.activities,
|
||||
players: getFilteredPlayers(game),
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buyDevelopment = (game: any, session: any): string | undefined => {
|
||||
@ -2484,7 +2512,11 @@ const buyDevelopment = (game: any, session: any): string | undefined => {
|
||||
if (game.mostDeveloped !== session.color) {
|
||||
game.mostDeveloped = session.color;
|
||||
game.mostPortCount = player.developmentCards;
|
||||
addChatMessage(game, session, `${session.name} now has the most development cards (${player.developmentCards})!`);
|
||||
addChatMessage(
|
||||
game,
|
||||
session,
|
||||
`${session.name} now has the most development cards (${player.developmentCards})!`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2637,7 +2669,7 @@ const playCard = (game: any, session: any, card: any): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const placeSettlement = (game: any, session: any, index: any): string | void => {
|
||||
const placeSettlement = (game: any, session: any, index: any): string | undefined => {
|
||||
const player = session.player;
|
||||
if (typeof index === "string") index = parseInt(index);
|
||||
|
||||
@ -2786,6 +2818,76 @@ const placeSettlement = (game: any, session: any, index: any): string | void =>
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const placeRoad = (game: any, session: any, index: any): string | undefined => {
|
||||
const player = session.player;
|
||||
if (typeof index === "string") index = parseInt(index);
|
||||
|
||||
if (!game || !game.turn) {
|
||||
return `Invalid game state.`;
|
||||
}
|
||||
|
||||
if (session.color !== game.turn.color) {
|
||||
return `It is not your turn! It is ${game.turn.name}'s turn.`;
|
||||
}
|
||||
|
||||
if (game.placements.roads[index] === undefined) {
|
||||
return `You have requested to place a road illegally!`;
|
||||
}
|
||||
|
||||
if (!game.turn.limits || !game.turn.limits.roads || game.turn.limits.roads.indexOf(index) === -1) {
|
||||
return `You tried to cheat! You should not try to break the rules.`;
|
||||
}
|
||||
|
||||
const road = game.placements.roads[index];
|
||||
if (road.color) {
|
||||
return `This location already has a road belonging to ${game.players[road.color].name}!`;
|
||||
}
|
||||
|
||||
if (game.state === "normal") {
|
||||
if (!game.turn.free) {
|
||||
if (player.brick < 1 || player.wood < 1) {
|
||||
return `You have insufficient resources to build a road.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (player.roads < 1) {
|
||||
return `You have already built all of your roads.`;
|
||||
}
|
||||
|
||||
if (!game.turn.free) {
|
||||
addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to build a road.`);
|
||||
player.brick--;
|
||||
player.wood--;
|
||||
player.resources = 0;
|
||||
["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => {
|
||||
player.resources += player[resource];
|
||||
});
|
||||
}
|
||||
delete game.turn.free;
|
||||
}
|
||||
|
||||
road.color = session.color;
|
||||
road.type = "road";
|
||||
player.roads--;
|
||||
|
||||
game.turn.actions = [];
|
||||
game.turn.limits = {};
|
||||
|
||||
calculateRoadLengths(game, session);
|
||||
|
||||
sendUpdateToPlayer(game, session, {
|
||||
private: session.player,
|
||||
});
|
||||
sendUpdateToPlayers(game, {
|
||||
placements: game.placements,
|
||||
turn: game.turn,
|
||||
chat: game.chat,
|
||||
activities: game.activities,
|
||||
players: getFilteredPlayers(game),
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getVictoryPointRule = (game: any): number => {
|
||||
const minVP = 10;
|
||||
if (!isRuleEnabled(game, "victory-points") || !("points" in game.rules["victory-points"])) {
|
||||
@ -2793,7 +2895,7 @@ const getVictoryPointRule = (game: any): number => {
|
||||
}
|
||||
return game.rules["victory-points"].points;
|
||||
};
|
||||
const supportedRules: Record<string, (game: any, session: any, rule: any, rules: any) => string | void> = {
|
||||
const supportedRules: Record<string, (game: any, session: any, rule: any, rules: any) => string | void | undefined> = {
|
||||
"victory-points": (game: any, session: any, rule: any, rules: any) => {
|
||||
if (!("points" in rules[rule])) {
|
||||
return `No points specified for victory-points`;
|
||||
@ -2803,6 +2905,7 @@ const supportedRules: Record<string, (game: any, session: any, rule: any, rules:
|
||||
} else {
|
||||
addChatMessage(game, null, `${getName(session)} set the minimum Victory Points to ` + `${rules[rule].points}`);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
"roll-double-roll-again": (game: any, session: any, rule: any, rules: any) => {
|
||||
addChatMessage(
|
||||
@ -2810,6 +2913,7 @@ const supportedRules: Record<string, (game: any, session: any, rule: any, rules:
|
||||
null,
|
||||
`${getName(session)} has ${rules[rule].enabled ? "en" : "dis"}abled the Roll Double, Roll Again house rule.`
|
||||
);
|
||||
return undefined;
|
||||
},
|
||||
volcano: (game: any, session: any, rule: any, rules: any) => {
|
||||
if (!rules[rule].enabled) {
|
||||
@ -2839,6 +2943,7 @@ const supportedRules: Record<string, (game: any, session: any, rule: any, rules:
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"twelve-and-two-are-synonyms": (game: any, session: any, rule: any, rules: any) => {
|
||||
addChatMessage(
|
||||
game,
|
||||
@ -2918,7 +3023,7 @@ const setRules = (game: any, session: any, rules: any): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const discard = (game: any, session: any, discards: Record<string, any>): string | void => {
|
||||
const discard = (game: any, session: any, discards: Record<string, any>): string | undefined => {
|
||||
const player = session.player;
|
||||
|
||||
if (game.turn.roll !== 7) {
|
||||
@ -2983,9 +3088,10 @@ const discard = (game: any, session: any, discards: Record<string, any>): string
|
||||
chat: game.chat,
|
||||
turn: game.turn,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buyRoad = (game: any, session: any): string | void => {
|
||||
const buyRoad = (game: any, session: any): string | undefined => {
|
||||
const player = session.player;
|
||||
|
||||
if (game.state !== "normal") {
|
||||
@ -3019,10 +3125,12 @@ const buyRoad = (game: any, session: any): string | void => {
|
||||
chat: game.chat,
|
||||
activities: game.activities,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const selectResources = (game: any, session: any, cards: string[]): string | void => {
|
||||
const selectResources = (game: any, session: any, cards: string[]): string | undefined => {
|
||||
const player = session.player;
|
||||
void player;
|
||||
if (!game || !game.turn || !game.turn.actions || game.turn.actions.indexOf("select-resources") === -1) {
|
||||
return `Please, let's not cheat. Ok?`;
|
||||
}
|
||||
@ -3168,6 +3276,7 @@ const selectResources = (game: any, session: any, cards: string[]): string | voi
|
||||
activities: game.activities,
|
||||
players: getFilteredPlayers(game),
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buySettlement = (game: any, session: any): string | undefined => {
|
||||
@ -3196,7 +3305,7 @@ const buySettlement = (game: any, session: any): string | undefined => {
|
||||
if (corners.length === 0) {
|
||||
return `There are no valid locations for you to place a settlement.`;
|
||||
}
|
||||
setForSettlementPlacement(game, corners, undefined);
|
||||
setForSettlementPlacement(game, corners);
|
||||
addActivity(game, session, `${game.turn.name} is considering placing a settlement.`);
|
||||
sendUpdateToPlayers(game, {
|
||||
turn: game.turn,
|
||||
@ -3206,7 +3315,7 @@ const buySettlement = (game: any, session: any): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buyCity = (game: any, session: any): string | void => {
|
||||
const buyCity = (game: any, session: any): string | undefined => {
|
||||
const player = session.player;
|
||||
if (game.state !== "normal") {
|
||||
return `You cannot purchase a development card unless the game is active (${game.state}).`;
|
||||
@ -3239,9 +3348,10 @@ const buyCity = (game: any, session: any): string | void => {
|
||||
chat: game.chat,
|
||||
activities: game.activities,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const placeCity = (game: any, session: any, index: any): string | void => {
|
||||
const placeCity = (game: any, session: any, index: any): string | undefined => {
|
||||
const player = session.player;
|
||||
if (typeof index === "string") index = parseInt(index);
|
||||
if (game.state !== "normal") {
|
||||
@ -3300,6 +3410,7 @@ const placeCity = (game: any, session: any, index: any): string | void => {
|
||||
activities: game.activities,
|
||||
players: getFilteredPlayers(game),
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const ping = (session: any) => {
|
||||
@ -3320,6 +3431,7 @@ const ping = (session: any) => {
|
||||
};
|
||||
|
||||
const wsInactive = (game: any, req: any) => {
|
||||
void game; // referenced for API completeness
|
||||
const playerCookie = req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : "";
|
||||
const session = getSession(game, playerCookie || "");
|
||||
|
||||
@ -3346,7 +3458,10 @@ const wsInactive = (game: any, req: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const setGameState = (game: any, session: any, state: any): string | void => {
|
||||
// keep a void reference so linters/typecheckers don't complain about unused declarations
|
||||
void wsInactive;
|
||||
|
||||
const setGameState = (game: any, session: any, state: any): string | undefined => {
|
||||
if (!state) {
|
||||
return `Invalid state.`;
|
||||
}
|
||||
@ -3385,9 +3500,11 @@ const setGameState = (game: any, session: any, state: any): string | void => {
|
||||
});
|
||||
break;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resetDisconnectCheck = (game: any, req: any): void => {
|
||||
const resetDisconnectCheck = (_game: any, req: any): void => {
|
||||
void _game;
|
||||
if (req.disconnectCheck) {
|
||||
clearTimeout(req.disconnectCheck);
|
||||
}
|
||||
@ -3543,7 +3660,7 @@ const saveGame = async (game: any): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const departLobby = (game: any, session: any, color?: string): void => {
|
||||
const departLobby = (game: any, session: any, _color?: string): void => {
|
||||
const update: any = {};
|
||||
update.unselected = getFilteredUnselected(game);
|
||||
|
||||
@ -3938,7 +4055,8 @@ const calculatePoints = (game: any, update: any): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const clearGame = (game: any, session: any): void => {
|
||||
const clearGame = (game: any, _session: any): string | undefined => {
|
||||
void _session;
|
||||
resetGame(game);
|
||||
addChatMessage(
|
||||
game,
|
||||
@ -3946,6 +4064,7 @@ const clearGame = (game: any, session: any): void => {
|
||||
`The game has been reset. You can play again with this board, or ` + `click 'New Table' to mix things up a bit.`
|
||||
);
|
||||
sendGameToPlayers(game);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const gotoLobby = (game: any, session: any): string | undefined => {
|
||||
@ -3992,6 +4111,7 @@ const gotoLobby = (game: any, session: any): string | undefined => {
|
||||
};
|
||||
|
||||
router.ws("/ws/:id", async (ws, req) => {
|
||||
console.log("New WebSocket connection");
|
||||
if (!req.cookies || !(req.cookies as any)["player"]) {
|
||||
// If the client hasn't established a session cookie, they cannot
|
||||
// participate in a websocket-backed game session. Log the request
|
||||
@ -4028,10 +4148,12 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
const gameId = id;
|
||||
|
||||
if (!gameId) {
|
||||
console.log("Missing game id");
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: "error", error: "Missing game id" }));
|
||||
} catch (e) {}
|
||||
try {
|
||||
console.log("Missing game id");
|
||||
ws.close && ws.close(1008, "Missing game id");
|
||||
} catch (e) {}
|
||||
return;
|
||||
@ -4241,8 +4363,6 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
switch (incoming.type) {
|
||||
case "join":
|
||||
// Accept either legacy `config` or newer `data` field from clients
|
||||
|
||||
|
||||
|
||||
join(audio[gameId], session, data.config || data.data || {});
|
||||
break;
|
||||
@ -4326,7 +4446,7 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
}
|
||||
|
||||
const cfg = data.config || data.data || {};
|
||||
const { peer_id, muted, video_on } = cfg;
|
||||
const { muted, video_on } = cfg;
|
||||
if (!session.name) {
|
||||
console.error(`${session.id}: peer_state_update - unnamed session`);
|
||||
return;
|
||||
@ -4543,7 +4663,6 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
}
|
||||
|
||||
processed = true;
|
||||
const _priorSession = session;
|
||||
|
||||
switch (incoming.type) {
|
||||
case "roll":
|
||||
@ -5236,15 +5355,25 @@ const shuffleBoard = (game: any): void => {
|
||||
[7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 8, 4, 5, 10, 14, 13, 9],
|
||||
];
|
||||
const sequence = order[Math.floor(Math.random() * order.length)];
|
||||
if (!sequence || !Array.isArray(sequence)) {
|
||||
// Defensive: should not happen, but guard for TS strictness
|
||||
return;
|
||||
}
|
||||
game.pipOrder = [];
|
||||
game.animationSeeds = [];
|
||||
for (let i = 0, p = 0; i < sequence.length; i++) {
|
||||
const target = sequence[i];
|
||||
if (typeof target !== "number") {
|
||||
continue;
|
||||
}
|
||||
/* If the target tile is the desert (18), then set the
|
||||
* pip value to the robber (18) otherwise set
|
||||
* the target pip value to the currently incremeneting
|
||||
* pip value. */
|
||||
if (game.tiles[game.tileOrder[target]].type === "desert") {
|
||||
const tileIdx = typeof game.tileOrder?.[target] === "number" ? game.tileOrder[target] : undefined;
|
||||
const tileType = typeof tileIdx === "number" && game.tiles?.[tileIdx] ? game.tiles[tileIdx].type : undefined;
|
||||
if (!game.pipOrder) game.pipOrder = [];
|
||||
if (tileType === "desert") {
|
||||
game.robber = target;
|
||||
game.pipOrder[target] = 18;
|
||||
} else {
|
||||
@ -5320,7 +5449,7 @@ router.post("/:id?", async (req, res /*, next*/) => {
|
||||
} else {
|
||||
console.log(`[${playerId.substring(0, 8)}]: Creating new game.`);
|
||||
}
|
||||
const game = await loadGame(id); /* will create game if it doesn't exist */
|
||||
const game = await loadGame(String(id || "")); /* will create game if it doesn't exist */
|
||||
console.log(`[${playerId.substring(0, 8)}]: ${game.id} loaded.`);
|
||||
|
||||
return res.status(200).send({ id: game.id });
|
||||
|
Loading…
x
Reference in New Issue
Block a user