Game almost launches
This commit is contained in:
parent
a586f3b491
commit
f4fe180fb1
@ -1338,6 +1338,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
const spacerRef = useRef<HTMLDivElement>(null);
|
const spacerRef = useRef<HTMLDivElement>(null);
|
||||||
const controlsRef = useRef<HTMLDivElement>(null);
|
const controlsRef = useRef<HTMLDivElement>(null);
|
||||||
const indicatorsRef = useRef<HTMLDivElement>(null);
|
const indicatorsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const moveableRef = useRef<any>(null);
|
const moveableRef = useRef<any>(null);
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
// Controls expansion state for hover/tap compact mode
|
// Controls expansion state for hover/tap compact mode
|
||||||
@ -1508,6 +1509,43 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [muted, peer?.attributes?.srcObject, peer?.dead, peer]);
|
}, [muted, peer?.attributes?.srcObject, peer?.dead, peer]);
|
||||||
|
|
||||||
|
// Attach remote stream to a hidden <audio> element so audio can be played/unmuted
|
||||||
|
useEffect(() => {
|
||||||
|
if (!audioRef.current) return;
|
||||||
|
const audioEl = audioRef.current;
|
||||||
|
if (!peer || peer.dead || !peer.attributes?.srcObject) {
|
||||||
|
// Clear srcObject when no media
|
||||||
|
try {
|
||||||
|
audioEl.pause();
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
(audioEl as any).srcObject = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = peer.attributes.srcObject as MediaStream;
|
||||||
|
(audioEl as any).srcObject = stream;
|
||||||
|
// Ensure audio element muted state matches our UI muted flag (local consumption mute)
|
||||||
|
audioEl.muted = !!(peer.local || muted);
|
||||||
|
|
||||||
|
// Try to play - if browser blocks autoplay, this will be allowed when user interacts
|
||||||
|
audioEl
|
||||||
|
.play()
|
||||||
|
.then(() => console.log(`media-agent - audio element playing for ${peer.peer_name}`))
|
||||||
|
.catch((err) => console.log(`media-agent - audio play blocked for ${peer.peer_name}:`, err));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
audioEl.pause();
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
(audioEl as any).srcObject = null;
|
||||||
|
};
|
||||||
|
// We intentionally depend on muted so the audio element updates when user toggles mute
|
||||||
|
}, [peer?.attributes?.srcObject, peer?.dead, peer?.local, muted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peer || peer.dead || !peer.attributes?.srcObject) return;
|
if (!peer || peer.dead || !peer.attributes?.srcObject) return;
|
||||||
|
|
||||||
@ -1540,6 +1578,21 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have an attached audio element (remote or local copy), update it and try to play when unmuting.
|
||||||
|
try {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.muted = !!(peer.local || newMutedState);
|
||||||
|
if (!audioRef.current.muted) {
|
||||||
|
audioRef.current.play().catch((err) => console.log("media-agent - play error:", err));
|
||||||
|
} else {
|
||||||
|
// When muting, pause to stop consuming resources
|
||||||
|
audioRef.current.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("media-agent - toggleMute audioRef handling failed:", err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[peer, muted, videoOn, sendJsonMessage, isSelf]
|
[peer, muted, videoOn, sendJsonMessage, isSelf]
|
||||||
);
|
);
|
||||||
@ -1750,6 +1803,14 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
muted={peer.local || muted}
|
muted={peer.local || muted}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
/>
|
/>
|
||||||
|
{/* Hidden audio element to ensure audio playback/unmute works reliably */}
|
||||||
|
<audio
|
||||||
|
ref={(el) => {
|
||||||
|
audioRef.current = el;
|
||||||
|
}}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
// Important: playsInline not standard on audio but keep attributes minimal
|
||||||
|
/>
|
||||||
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
|
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
|
||||||
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
|
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
|
||||||
</>
|
</>
|
||||||
|
41
client/src/NameSetter.css
Normal file
41
client/src/NameSetter.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/* Styles for NameSetter component when no name is set */
|
||||||
|
.NameSetter-root {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NameSetter-titleWrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
z-index: 50; /* behind dialog which MUI renders with higher z-index */
|
||||||
|
}
|
||||||
|
|
||||||
|
.NameSetter-titleImage {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
/* width: 100%; */
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When name is not set, position the dialog 10% from bottom and center it horizontally */
|
||||||
|
.NameSetter-dialogPaperNoName {
|
||||||
|
position: fixed !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translateX(-50%) !important;
|
||||||
|
bottom: 10vh !important; /* 10% from bottom of viewport */
|
||||||
|
margin: 0 !important;
|
||||||
|
max-width: 600px;
|
||||||
|
width: min(90%, 600px);
|
||||||
|
z-index: 100; /* above the title image */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the dialog title content centered when overlaying the image */
|
||||||
|
.NameSetter-dialogTitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the input doesn't stretch full width too much when overlaying */
|
||||||
|
.NameSetter-nameInput {
|
||||||
|
text-align: center;
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
import React, { useState, KeyboardEvent, useRef, useContext } from "react";
|
import React, { useState, KeyboardEvent, useRef, useContext } from "react";
|
||||||
|
import "./NameSetter.css";
|
||||||
|
import pokTitle from "./assets/pok-title.png";
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Button,
|
Button,
|
||||||
@ -9,6 +11,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
|
Paper,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { GlobalContext, Session } from "./GlobalContext";
|
import { GlobalContext, Session } from "./GlobalContext";
|
||||||
|
|
||||||
@ -67,51 +70,40 @@ const NameSetter: React.FC<NameSetterProps> = ({ onNameSet, initialName = "", in
|
|||||||
setName(newName);
|
setName(newName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenDialog = () => {
|
|
||||||
setEditName(session.name || "");
|
|
||||||
setEditPassword("");
|
|
||||||
setShowDialog(true);
|
|
||||||
// Focus the name input when dialog opens
|
|
||||||
setTimeout(() => {
|
|
||||||
if (nameInputRef.current) {
|
|
||||||
nameInputRef.current.focus();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseDialog = () => {
|
|
||||||
setShowDialog(false);
|
|
||||||
setEditName("");
|
|
||||||
setEditPassword("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasNameChanged = editName.trim() !== (session.name || "");
|
const hasNameChanged = editName.trim() !== (session.name || "");
|
||||||
const canSubmit = editName.trim() && hasNameChanged && !isSubmitting;
|
const canSubmit = editName.trim() && hasNameChanged && !isSubmitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ gap: 1, display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
|
<Box
|
||||||
{session.name && !showDialog && (
|
className="NameSetter-root"
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
sx={{
|
||||||
<Typography>You are logged in as: {session.name}</Typography>
|
display: "flex",
|
||||||
<Button variant="outlined" size="small" onClick={handleOpenDialog}>
|
flexDirection: "column",
|
||||||
Change Name
|
alignItems: "center",
|
||||||
</Button>
|
maxWidth: "800px",
|
||||||
</Box>
|
maxHeight: "100dvh",
|
||||||
)}
|
width: "100%",
|
||||||
|
height: "100dvh",
|
||||||
{/* Dialog for name change */}
|
margin: "auto",
|
||||||
<Dialog open={showDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
backgroundImage: `url(${pokTitle})`,
|
||||||
<DialogTitle>{session.name ? "Change Your Name" : "Enter Your Name"}</DialogTitle>
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundPosition: "top center",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "10%",
|
||||||
|
width: "calc(100% - 2rem)",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.8) !important",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle className="NameSetter-dialogTitle">
|
||||||
|
{session.name ? "Change Your Name" : "How shall you be called?"}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, pt: 1 }}>
|
<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."}
|
|
||||||
</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.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
inputRef={nameInputRef}
|
inputRef={nameInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@ -120,40 +112,36 @@ const NameSetter: React.FC<NameSetterProps> = ({ onNameSet, initialName = "", in
|
|||||||
setEditName(e.target.value);
|
setEditName(e.target.value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={handleNameKeyDown}
|
onKeyDown={handleNameKeyDown}
|
||||||
placeholder="Your name"
|
placeholder="Your quipy handle"
|
||||||
fullWidth
|
fullWidth
|
||||||
autoFocus
|
autoFocus
|
||||||
|
className="NameSetter-nameInput"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
|
||||||
inputRef={passwordInputRef}
|
|
||||||
type="password"
|
|
||||||
value={editPassword}
|
|
||||||
onChange={(e): void => setEditPassword(e.target.value)}
|
|
||||||
onKeyDown={handlePasswordKeyDown}
|
|
||||||
placeholder="Optional password"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip title="Optional: choose a short password to reserve this name. Keep it secret.">
|
|
||||||
<span />
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCloseDialog} disabled={isSubmitting}>
|
{/* <Button onClick={handleCloseDialog} disabled={isSubmitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button> */}
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
color={hasNameChanged ? "primary" : "inherit"}
|
color={hasNameChanged ? "primary" : "inherit"}
|
||||||
|
sx={
|
||||||
|
hasNameChanged
|
||||||
|
? {
|
||||||
|
backgroundColor: "primary.main",
|
||||||
|
color: "#444",
|
||||||
|
"&:hover": { backgroundColor: "primary.dark", color: "#fff" },
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Changing..." : session.name ? "Change Name" : "Join"}
|
{isSubmitting ? "Changing..." : session.name ? "Change Name" : "Enter Ketran!"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -305,9 +305,7 @@ const RoomView = (props: RoomProps) => {
|
|||||||
<GlobalContext.Provider value={{ socketUrl, session, name, roomName, sendJsonMessage, lastJsonMessage, readyState }}>
|
<GlobalContext.Provider value={{ socketUrl, session, name, roomName, sendJsonMessage, lastJsonMessage, readyState }}>
|
||||||
<div className="RoomView">
|
<div className="RoomView">
|
||||||
{!name ? (
|
{!name ? (
|
||||||
<Paper>
|
<NameSetter />
|
||||||
<NameSetter />
|
|
||||||
</Paper>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="ActivitiesBox">
|
<div className="ActivitiesBox">
|
||||||
|
BIN
client/src/assets/pok-title.png
Executable file
BIN
client/src/assets/pok-title.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
@ -6,7 +6,7 @@ import { getVictoryPointRule } from "./rules";
|
|||||||
|
|
||||||
export const addActivity = (game: Game, session: Session | null, message: string): void => {
|
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) game.activities = [];
|
||||||
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++;
|
||||||
}
|
}
|
||||||
@ -20,7 +20,7 @@ export const addActivity = (game: Game, session: Session | null, message: string
|
|||||||
export const addChatMessage = (game: Game, session: Session | null, 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) game.chat = [];
|
||||||
if (game.chat.length) {
|
if (game.chat.length) {
|
||||||
lastTime = game.chat[game.chat.length - 1].date;
|
lastTime = game.chat[game.chat.length - 1].date;
|
||||||
}
|
}
|
||||||
@ -136,15 +136,8 @@ export const getPrevPlayerSession = (game: Game, name: string): Session | undefi
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const clearPlayer = (player: Player) => {
|
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use shared factory to ensure a single source of defaults
|
// Use shared factory to ensure a single source of defaults
|
||||||
Object.assign(player, newPlayer(color || ""));
|
Object.assign(player, newPlayer(player.color));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canGiveBuilding = (game: Game): string | undefined => {
|
export const canGiveBuilding = (game: Game): string | undefined => {
|
||||||
@ -176,14 +169,22 @@ export const setForSettlementPlacement = (game: Game, limits: number[]): void =>
|
|||||||
export const adjustResources = (player: Player, deltas: Partial<Record<string, number>>): void => {
|
export const adjustResources = (player: Player, deltas: Partial<Record<string, number>>): void => {
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
let total = player.resources || 0;
|
let total = player.resources || 0;
|
||||||
const keys = Object.keys(deltas || {});
|
const keys = Object.keys(deltas);
|
||||||
keys.forEach((k) => {
|
keys.forEach((type) => {
|
||||||
const v = deltas[k] || 0;
|
const v = deltas[type] || 0;
|
||||||
// update named resource slot if present
|
// update named resource slot if present
|
||||||
try {
|
try {
|
||||||
const current = (player as any)[k] || 0;
|
switch (type) {
|
||||||
(player as any)[k] = current + v;
|
case "wood":
|
||||||
total += v;
|
case "brick":
|
||||||
|
case "sheep":
|
||||||
|
case "wheat":
|
||||||
|
case "stone":
|
||||||
|
const current = player[type] || 0;
|
||||||
|
player[type] = current + v;
|
||||||
|
total += v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore unexpected keys
|
// ignore unexpected keys
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user