feat: Enhance session persistence by marking players offline in active games and improving rejoin room with server callbacks.
All checks were successful
Build and Deploy / build (push) Successful in 1m11s
All checks were successful
Build and Deploy / build (push) Successful in 1m11s
This commit is contained in:
@@ -62,6 +62,24 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
setMessages(initialRoom.messages || []);
|
||||
}, [initialRoom]);
|
||||
|
||||
// React to prop updates for draft state (Crucial for resume)
|
||||
useEffect(() => {
|
||||
if (initialDraftState) {
|
||||
setDraftState(initialDraftState);
|
||||
}
|
||||
}, [initialDraftState]);
|
||||
|
||||
// Handle kicked event
|
||||
useEffect(() => {
|
||||
const socket = socketService.socket;
|
||||
const onKicked = () => {
|
||||
alert("You have been kicked from the room.");
|
||||
onExit();
|
||||
};
|
||||
socket.on('kicked', onKicked);
|
||||
return () => { socket.off('kicked', onKicked); };
|
||||
}, [onExit]);
|
||||
|
||||
// Scroll to bottom of chat
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
@@ -243,25 +261,55 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2">
|
||||
<Users className="w-4 h-4" /> Lobby
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
||||
{room.players.map(p => {
|
||||
const isReady = (p as any).ready;
|
||||
const isMe = p.id === currentPlayerId;
|
||||
|
||||
return (
|
||||
<div key={p.id} className="flex items-center justify-between bg-slate-900/50 p-2 rounded-lg border border-slate-700/50">
|
||||
<div key={p.id} className="flex items-center justify-between bg-slate-900/50 p-2 rounded-lg border border-slate-700/50 group">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs ${p.role === 'spectator' ? 'bg-slate-700 text-slate-300' : 'bg-gradient-to-br from-purple-500 to-blue-500 text-white'}`}>
|
||||
{p.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className={`text-sm font-medium ${p.id === currentPlayerId ? 'text-white' : 'text-slate-300'}`}>
|
||||
{p.name}
|
||||
<span className={`text-sm font-medium ${isMe ? 'text-white' : 'text-slate-300'}`}>
|
||||
{p.name} {isMe && '(You)'}
|
||||
</span>
|
||||
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-500">
|
||||
{p.role} {p.isHost && <span className="text-amber-500 ml-1">• Host</span>}
|
||||
{isReady && room.status === 'deck_building' && <span className="text-emerald-500 ml-1">• Ready</span>}
|
||||
{p.isOffline && <span className="text-red-500 ml-1">• Offline</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{isMe && (
|
||||
<button
|
||||
onClick={onExit}
|
||||
className="p-1 hover:bg-slate-700 rounded text-slate-400 hover:text-red-400"
|
||||
title="Leave Room"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{isMeHost && !isMe && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Kick ${p.name}?`)) {
|
||||
socketService.socket.emit('kick_player', { roomId: room.id, targetId: p.id });
|
||||
}
|
||||
}}
|
||||
className="p-1 hover:bg-red-900/50 rounded text-slate-500 hover:text-red-500"
|
||||
title="Kick Player"
|
||||
>
|
||||
<LogOut className="w-4 h-4 rotate-180" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -155,9 +155,19 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
|
||||
if (savedRoomId && !activeRoom && playerId) {
|
||||
setLoading(true);
|
||||
connect();
|
||||
socketService.emitPromise('rejoin_room', { roomId: savedRoomId })
|
||||
.then(() => {
|
||||
// Rejoin logic mostly handled by onRoomUpdate via socket
|
||||
socketService.emitPromise('rejoin_room', { roomId: savedRoomId, playerId })
|
||||
.then((response: any) => {
|
||||
if (response.success) {
|
||||
console.log("Rejoined session successfully");
|
||||
setActiveRoom(response.room);
|
||||
if (response.draftState) {
|
||||
setInitialDraftState(response.draftState);
|
||||
}
|
||||
} else {
|
||||
console.warn("Rejoin failed by server: ", response.message);
|
||||
localStorage.removeItem('active_room_id');
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn("Reconnection failed", err);
|
||||
|
||||
@@ -151,7 +151,7 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
// RE-IMPLEMENTING rejoin_room with playerId
|
||||
socket.on('rejoin_room', ({ roomId, playerId }) => {
|
||||
socket.on('rejoin_room', ({ roomId, playerId }, callback) => {
|
||||
socket.join(roomId);
|
||||
|
||||
if (playerId) {
|
||||
@@ -172,15 +172,29 @@ io.on('connection', (socket) => {
|
||||
resumeRoomTimers(roomId);
|
||||
}
|
||||
|
||||
// Prepare Draft State if exists
|
||||
let currentDraft = null;
|
||||
if (room.status === 'drafting') {
|
||||
const draft = draftManager.getDraft(roomId);
|
||||
if (draft) socket.emit('draft_update', draft);
|
||||
currentDraft = draftManager.getDraft(roomId);
|
||||
if (currentDraft) socket.emit('draft_update', currentDraft);
|
||||
}
|
||||
|
||||
// ACK Callback
|
||||
if (typeof callback === 'function') {
|
||||
callback({ success: true, room, draftState: currentDraft });
|
||||
}
|
||||
} else {
|
||||
// Room found but player not in it? Or room not found?
|
||||
// If room exists but player not in list, it failed.
|
||||
if (typeof callback === 'function') {
|
||||
callback({ success: false, message: 'Player not found in room or room closed' });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Just get room if no playerId? Should rare happen
|
||||
const room = roomManager.getRoom(roomId);
|
||||
if (room) socket.emit('room_update', room);
|
||||
// Missing playerId
|
||||
if (typeof callback === 'function') {
|
||||
callback({ success: false, message: 'Missing Player ID' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -202,6 +216,29 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('kick_player', ({ roomId, targetId }) => {
|
||||
const context = getContext();
|
||||
if (!context || !context.player.isHost) return; // Verify host
|
||||
|
||||
// Get target socketId before removal to notify them
|
||||
// Note: getPlayerBySocket works if they are connected.
|
||||
// We might need to find target in room.players directly.
|
||||
const room = roomManager.getRoom(roomId);
|
||||
if (room) {
|
||||
const target = room.players.find(p => p.id === targetId);
|
||||
if (target) {
|
||||
const updatedRoom = roomManager.kickPlayer(roomId, targetId);
|
||||
if (updatedRoom) {
|
||||
io.to(roomId).emit('room_update', updatedRoom);
|
||||
if (target.socketId) {
|
||||
io.to(target.socketId).emit('kicked', { message: 'You have been kicked by the host.' });
|
||||
}
|
||||
console.log(`Player ${targetId} kicked from room ${roomId} by host.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Secure helper to get player context
|
||||
const getContext = () => roomManager.getPlayerBySocket(socket.id);
|
||||
|
||||
|
||||
@@ -105,18 +105,32 @@ export class RoomManager {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
room.players = room.players.filter(p => p.id !== playerId);
|
||||
if (room.status === 'waiting') {
|
||||
// Normal logic: Remove player completely
|
||||
room.players = room.players.filter(p => p.id !== playerId);
|
||||
|
||||
// If host leaves, assign new host from remaining players
|
||||
if (room.players.length === 0) {
|
||||
this.rooms.delete(roomId);
|
||||
return null;
|
||||
} else if (room.hostId === playerId) {
|
||||
const nextPlayer = room.players.find(p => p.role === 'player') || room.players[0];
|
||||
if (nextPlayer) {
|
||||
room.hostId = nextPlayer.id;
|
||||
nextPlayer.isHost = true;
|
||||
// If host leaves, assign new host from remaining players
|
||||
if (room.players.length === 0) {
|
||||
this.rooms.delete(roomId);
|
||||
return null;
|
||||
} else if (room.hostId === playerId) {
|
||||
const nextPlayer = room.players.find(p => p.role === 'player') || room.players[0];
|
||||
if (nextPlayer) {
|
||||
room.hostId = nextPlayer.id;
|
||||
nextPlayer.isHost = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Game in progress (Drafting/Playing)
|
||||
// DO NOT REMOVE PLAYER. Just mark offline.
|
||||
// This allows them to rejoin and reclaim their seat (and deck).
|
||||
const player = room.players.find(p => p.id === playerId);
|
||||
if (player) {
|
||||
player.isOffline = true;
|
||||
// Note: socketId is already handled by disconnect event usually, but if explicit leave, we should clear it?
|
||||
player.socketId = undefined;
|
||||
}
|
||||
console.log(`Player ${playerId} left active game in room ${roomId}. Marked as offline.`);
|
||||
}
|
||||
return room;
|
||||
}
|
||||
@@ -132,6 +146,16 @@ export class RoomManager {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
kickPlayer(roomId: string, playerId: string): Room | null {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
room.players = room.players.filter(p => p.id !== playerId);
|
||||
|
||||
// If game was running, we might need more cleanup, but for now just removal.
|
||||
return room;
|
||||
}
|
||||
|
||||
addMessage(roomId: string, sender: string, text: string): ChatMessage | null {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
Reference in New Issue
Block a user