1
0

Continuing...

This commit is contained in:
James Ketr 2025-10-07 14:59:14 -07:00
parent 6b4e5d1e58
commit b9d7523800
3 changed files with 202 additions and 149 deletions

View File

@ -5,7 +5,6 @@ import "./PlayerList.css";
import { MediaControl, MediaAgent, Peer } from "./MediaControl"; import { MediaControl, MediaAgent, Peer } from "./MediaControl";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { GlobalContext } from "./GlobalContext"; import { GlobalContext } from "./GlobalContext";
import useWebSocket from "react-use-websocket";
type Player = { type Player = {
name: string; name: string;
@ -22,8 +21,8 @@ type Player = {
}; };
const PlayerList: React.FC = () => { const PlayerList: React.FC = () => {
const { session, socketUrl } = useContext(GlobalContext); const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [Players, setPlayers] = useState<Player[] | null>(null); const [players, setPlayers] = useState<Player[] | null>(null);
const [peers, setPeers] = useState<Record<string, Peer>>({}); const [peers, setPeers] = useState<Record<string, Peer>>({});
const sortPlayers = useCallback( const sortPlayers = useCallback(
@ -55,38 +54,33 @@ const PlayerList: React.FC = () => {
); );
// Use the WebSocket hook for room events with automatic reconnection // Use the WebSocket hook for room events with automatic reconnection
const { sendJsonMessage } = useWebSocket(socketUrl, { useEffect(() => {
share: true, if (!lastJsonMessage) {
shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss
reconnectInterval: 5000,
onMessage: (event: MessageEvent) => {
if (!session) {
return; return;
} }
const message = JSON.parse(event.data); const data: any = lastJsonMessage;
const data: any = message.data; switch (data.type) {
switch (message.type) { case "players": {
case "room_state": {
type RoomStateData = { type RoomStateData = {
participants: Player[]; participants: Player[];
}; };
const room_state = data as RoomStateData; const room_state = data as RoomStateData;
console.log(`Players - room_state`, room_state.participants); console.log(`Players - room_state`, room_state.participants);
room_state.participants.forEach((Player) => { room_state.participants.forEach((player) => {
Player.local = Player.session_id === session.id; player.local = player.session_id === session.id;
}); });
room_state.participants.sort(sortPlayers); room_state.participants.sort(sortPlayers);
setPlayers(room_state.participants); setPlayers(room_state.participants);
// Initialize peers with remote mute/video state // Initialize peers with remote mute/video state
setPeers((prevPeers) => { setPeers((prevPeers) => {
const updated: Record<string, Peer> = { ...prevPeers }; const updated: Record<string, Peer> = { ...prevPeers };
room_state.participants.forEach((Player) => { room_state.participants.forEach((player) => {
// Only update remote peers, never overwrite local peer object // Only update remote peers, never overwrite local peer object
if (!Player.local && updated[Player.session_id]) { if (!player.local && updated[player.session_id]) {
updated[Player.session_id] = { updated[player.session_id] = {
...updated[Player.session_id], ...updated[player.session_id],
muted: Player.muted ?? false, muted: player.muted ?? false,
video_on: Player.video_on ?? true, video_on: player.video_on ?? true,
}; };
} }
}); });
@ -120,17 +114,16 @@ const PlayerList: React.FC = () => {
default: default:
break; break;
} }
}, }, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]);
});
useEffect(() => { useEffect(() => {
if (Players !== null) { if (players !== null) {
return; return;
} }
sendJsonMessage({ sendJsonMessage({
type: "list_Players", type: "list_Players",
}); });
}, [Players, sendJsonMessage]); }, [players, sendJsonMessage]);
return ( return (
<Box sx={{ position: "relative", width: "100%" }}> <Box sx={{ position: "relative", width: "100%" }}>
@ -144,17 +137,17 @@ const PlayerList: React.FC = () => {
> >
<MediaAgent {...{ session, socketUrl, peers, setPeers }} /> <MediaAgent {...{ session, socketUrl, peers, setPeers }} />
<List className="PlayerSelector"> <List className="PlayerSelector">
{Players?.map((Player) => ( {players?.map((player) => (
<Box <Box
key={Player.session_id} key={player.session_id}
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }} sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
className={`PlayerEntry ${Player.local ? "PlayerSelf" : ""}`} className={`PlayerEntry ${player.local ? "PlayerSelf" : ""}`}
> >
<Box> <Box>
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}> <Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
<Box style={{ display: "flex-wrap", alignItems: "center" }}> <Box style={{ display: "flex-wrap", alignItems: "center" }}>
<div className="Name">{Player.name ? Player.name : Player.session_id}</div> <div className="Name">{player.name ? player.name : player.session_id}</div>
{Player.protected && ( {player.protected && (
<div <div
style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }} style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }}
title="This name is protected with a password" title="This name is protected with a password"
@ -162,26 +155,26 @@ const PlayerList: React.FC = () => {
🔒 🔒
</div> </div>
)} )}
{Player.bot_instance_id && ( {player.bot_instance_id && (
<div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot"> <div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
🤖 🤖
</div> </div>
)} )}
</Box> </Box>
</Box> </Box>
{Player.name && !Player.live && <div className="NoNetwork"></div>} {player.name && !player.live && <div className="NoNetwork"></div>}
</Box> </Box>
{Player.name && Player.live && peers[Player.session_id] && (Player.local || Player.has_media !== false) ? ( {player.name && player.live && peers[player.session_id] && (player.local || player.has_media !== false) ? (
<MediaControl <MediaControl
className="Medium" className="Medium"
key={Player.session_id} key={player.session_id}
peer={peers[Player.session_id]} peer={peers[player.session_id]}
isSelf={Player.local} isSelf={player.local}
sendJsonMessage={Player.local ? sendJsonMessage : undefined} sendJsonMessage={player.local ? sendJsonMessage : undefined}
remoteAudioMuted={peers[Player.session_id].muted} remoteAudioMuted={peers[player.session_id].muted}
remoteVideoOff={peers[Player.session_id].video_on === false} remoteVideoOff={peers[player.session_id].video_on === false}
/> />
) : Player.name && Player.live && Player.has_media === false ? ( ) : player.name && player.live && player.has_media === false ? (
<div <div
className="Video fade-in" className="Video fade-in"
style={{ style={{

View File

@ -26,7 +26,16 @@ const router = express.Router();
// normalizeIncoming imported from './games/utils' // normalizeIncoming imported from './games/utils'
import { initGameDB } from "./games/store"; import { initGameDB } from "./games/store";
import { addActivity, addChatMessage, getNextPlayerSession } from "./games/helpers"; import {
addActivity,
addChatMessage,
getNextPlayerSession,
clearPlayer,
canGiveBuilding,
setForRoadPlacement,
setForCityPlacement,
setForSettlementPlacement,
} from "./games/helpers";
import type { GameDB } from "./games/store"; import type { GameDB } from "./games/store";
let gameDB: GameDB | undefined; let gameDB: GameDB | undefined;
@ -767,24 +776,6 @@ const loadGame = async (id: string) => {
return game; return game;
}; };
const clearPlayer = (player: any) => {
const color = player.color;
for (let key in player) {
delete player[key];
}
Object.assign(player, newPlayer(color));
};
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 => { 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; let color: string | undefined, parts: RegExpMatchArray | null, session: any, corners: any, corner: any, error: any;
void color; void color;
@ -1318,7 +1309,6 @@ const setPlayerColor = (game: any, session: any, color: string): string | undefi
return undefined; return undefined;
}; };
const processCorner = (game: Game, color: string, cornerIndex: number, placedCorner: CornerPlacement): number => { 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 this corner is allocated and isn't assigned to the walking color, skip it */
if (placedCorner.color && placedCorner.color !== color) { if (placedCorner.color && placedCorner.color !== color) {
@ -1804,21 +1794,7 @@ const offerToString = (offer: any): string => {
); );
}; };
const setForRoadPlacement = (game: Game, limits: any): void => {
game.turn.actions = ["place-road"];
game.turn.limits = { roads: limits };
};
const setForCityPlacement = (game: Game, limits: any): void => {
game.turn.actions = ["place-city"];
game.turn.limits = { corners: limits };
};
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 };
};
router.put("/:id/:action/:value?", async (req, res) => { router.put("/:id/:action/:value?", async (req, res) => {
const { action, id } = req.params, const { action, id } = req.params,

View File

@ -1,5 +1,8 @@
export const addActivity = (game: any, session: any, message: string): void => { import type { Game, Session, Player } from "./types";
export const addActivity = (game: Game, session: Session | null, message: string): void => {
let date = Date.now(); let date = Date.now();
if (!game.activities) game.activities = [] as any[];
if (game.activities.length && game.activities[game.activities.length - 1].date === date) { if (game.activities.length && game.activities[game.activities.length - 1].date === date) {
date++; date++;
} }
@ -9,9 +12,10 @@ export const addActivity = (game: any, session: any, message: string): void => {
} }
}; };
export const addChatMessage = (game: any, session: any, message: string, isNormalChat?: boolean) => { export const addChatMessage = (game: Game, session: Session | null, message: string, isNormalChat?: boolean) => {
let now = Date.now(); let now = Date.now();
let lastTime = 0; let lastTime = 0;
if (!game.chat) game.chat = [] as any[];
if (game.chat.length) { if (game.chat.length) {
lastTime = game.chat[game.chat.length - 1].date; lastTime = game.chat[game.chat.length - 1].date;
} }
@ -38,71 +42,151 @@ export const addChatMessage = (game: any, session: any, message: string, isNorma
} }
}; };
export const getColorFromName = (game: any, name: string): string => { export const getColorFromName = (game: Game, name: string): string => {
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].name === name) { const s = game.sessions[id];
return game.sessions[id].color; if (s && s.name === name) {
return s.color || "";
} }
} }
return ""; return "";
}; };
export const getLastPlayerName = (game: any): string => { export const getLastPlayerName = (game: Game): string => {
let index = game.playerOrder.length - 1; const index = (game.playerOrder || []).length - 1;
const color = (game.playerOrder || [])[index];
if (!color) return "";
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) { const s = game.sessions[id];
return game.sessions[id].name; if (s && s.color === color) {
return s.name || "";
} }
} }
return ""; return "";
}; };
export const getFirstPlayerName = (game: any): string => { export const getFirstPlayerName = (game: Game): string => {
let index = 0; const color = (game.playerOrder || [])[0];
if (!color) return "";
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) { const s = game.sessions[id];
return game.sessions[id].name; if (s && s.color === color) {
return s.name || "";
} }
} }
return ""; return "";
}; };
export const getNextPlayerSession = (game: any, name: string): any => { export const getNextPlayerSession = (game: Game, name: string): Session | undefined => {
let color; let color: string | undefined;
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].name === name) { const s = game.sessions[id];
color = game.sessions[id].color; if (s && s.name === name) {
color = s.color;
break; break;
} }
} }
if (!color) return undefined;
let index = game.playerOrder.indexOf(color); const order = game.playerOrder || [];
index = (index + 1) % game.playerOrder.length; let index = order.indexOf(color);
color = game.playerOrder[index]; if (index === -1) return undefined;
index = (index + 1) % order.length;
const nextColor = order[index];
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].color === color) { const s = game.sessions[id];
return game.sessions[id]; if (s && s.color === nextColor) {
return s;
} }
} }
console.error(`getNextPlayerSession -- no player found!`); console.error(`getNextPlayerSession -- no player found!`);
console.log(game.players); console.log(game.players);
return undefined;
}; };
export const getPrevPlayerSession = (game: any, name: string): any => { export const getPrevPlayerSession = (game: Game, name: string): Session | undefined => {
let color; let color: string | undefined;
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].name === name) { const s = game.sessions[id];
color = game.sessions[id].color; if (s && s.name === name) {
color = s.color;
break; break;
} }
} }
let index = game.playerOrder.indexOf(color); if (!color) return undefined;
index = (index - 1) % game.playerOrder.length; const order = game.playerOrder || [];
let index = order.indexOf(color);
if (index === -1) return undefined;
index = (index - 1 + order.length) % order.length;
const prevColor = order[index];
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) { const s = game.sessions[id];
return game.sessions[id]; if (s && s.color === prevColor) {
return s;
} }
} }
console.error(`getNextPlayerSession -- no player found!`); console.error(`getPrevPlayerSession -- no player found!`);
console.log(game.players); console.log(game.players);
return undefined;
};
export const clearPlayer = (player: Player) => {
const color = player.color;
for (let key in player) {
// delete all runtime fields
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (player as any)[key];
}
// Inline minimal newPlayer factory to avoid circular import at runtime
const base = {
roads: 15,
cities: 4,
settlements: 5,
points: 0,
status: "Not active",
lastActive: 0,
resources: 0,
order: 0,
stone: 0,
wheat: 0,
sheep: 0,
wood: 0,
brick: 0,
army: 0,
development: [],
color: color,
name: "",
totalTime: 0,
turnStart: 0,
ports: 0,
developmentCards: 0,
} as Player;
Object.assign(player, base);
};
export const canGiveBuilding = (game: Game): 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;
};
export const setForRoadPlacement = (game: Game, limits: any): void => {
game.turn.actions = ["place-road"];
game.turn.limits = { roads: limits };
};
export const setForCityPlacement = (game: Game, limits: any): void => {
game.turn.actions = ["place-city"];
game.turn.limits = { corners: limits };
};
export const setForSettlementPlacement = (game: Game, limits: number[] | undefined, _extra?: any): void => {
game.turn.actions = ["place-settlement"];
game.turn.limits = { corners: limits };
}; };