feature/bestof-series-interface #99
@@ -244,4 +244,19 @@ class GameController extends Controller
|
|||||||
'game' => $game->fresh(),
|
'game' => $game->fresh(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Game $game)
|
||||||
|
{
|
||||||
|
// Ensure only the host can delete, and ONLY if it's still Pending
|
||||||
|
if ($game->player1_user_id !== auth()->id()) {
|
||||||
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($game->status !== 'Pending') {
|
||||||
|
return response()->json(['message' => 'Cannot delete a game in progress'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$game->delete();
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -291,18 +291,88 @@ const handleBeforeUnload = (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeRouteLeave((to, from, next) => {
|
// --- FIX: SURRENDER LOGIC ---
|
||||||
if (gameOver.value || !shouldShowBoard.value) {
|
const handleSurrender = () => {
|
||||||
|
if(confirm('Are you sure you want to surrender? You will lose.')) {
|
||||||
|
wsStore.surrender(gameId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseModal = async () => {
|
||||||
|
// 1. If game is still in Lobby (Pending) and I am the Host, DELETE it.
|
||||||
|
if (!shouldShowBoard.value && amIHost.value && !gameOver.value) {
|
||||||
|
try {
|
||||||
|
// This calls Route::delete('/{game}', ...) in your api.php
|
||||||
|
await axios.delete(`${API_BASE_URL}/games/${gameId.value}`);
|
||||||
|
console.log("Lobby cancelled and deleted.");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete pending game:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Notify server of voluntary exit (to skip the 10s reconnect timer)
|
||||||
|
if (wsStore.socket) {
|
||||||
|
wsStore.socket.emit('notify_disconnect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Disconnect and go home
|
||||||
|
wsStore.disconnect();
|
||||||
|
router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LIFECYCLE ---
|
||||||
|
|
||||||
|
onBeforeRouteLeave(async (to, from, next) => {
|
||||||
|
// CASE A: Game is Pending (Waiting Room)
|
||||||
|
if (!shouldShowBoard.value && !gameOver.value) {
|
||||||
|
if (amIHost.value) {
|
||||||
|
// Ask for confirmation to close the lobby
|
||||||
|
const confirmClose = window.confirm("This will cancel the lobby. Are you sure?");
|
||||||
|
if (confirmClose) {
|
||||||
|
try {
|
||||||
|
// Delete the game from DB so it doesn't get stuck as "Pending"
|
||||||
|
await axios.delete(`${API_BASE_URL}/games/${gameId.value}`);
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
|
||||||
|
if(wsStore.socket) wsStore.socket.emit('notify_disconnect');
|
||||||
|
wsStore.disconnect();
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(false); // Stay on page
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// If I am NOT host (just a guest waiting), just leave
|
||||||
|
if(wsStore.socket) wsStore.socket.emit('notify_disconnect');
|
||||||
|
wsStore.disconnect();
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE B: Game is Over (Standard exit)
|
||||||
|
if (gameOver.value) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CASE C: Game is Active/Playing (The Surrender Logic we built before)
|
||||||
const answer = window.confirm(
|
const answer = window.confirm(
|
||||||
"If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?"
|
"If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
wsStore.surrender(gameId.value);
|
// ... (Your existing surrender logic) ...
|
||||||
|
try { await wsStore.surrender(gameId.value); } catch (e) {}
|
||||||
|
|
||||||
|
if (wsStore.socket) {
|
||||||
|
wsStore.socket.emit('notify_disconnect', () => {
|
||||||
|
wsStore.disconnect();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
setTimeout(() => { if (wsStore.connected) { wsStore.disconnect(); next(); } }, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
wsStore.disconnect();
|
wsStore.disconnect();
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
@@ -310,22 +380,6 @@ onBeforeRouteLeave((to, from, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- FIX: SURRENDER LOGIC ---
|
|
||||||
const handleSurrender = () => {
|
|
||||||
if(confirm('Are you sure you want to surrender? You will lose.')) {
|
|
||||||
// Send event to server. Do NOT disconnect locally yet.
|
|
||||||
// Wait for the server to send "Game Over" event back.
|
|
||||||
wsStore.surrender(gameId.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
wsStore.disconnect()
|
|
||||||
router.push({ name: 'home' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- LIFECYCLE ---
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchGameMetadata()
|
await fetchGameMetadata()
|
||||||
wsStore.connect()
|
wsStore.connect()
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ const getPendingMatches = async (mode = selectedMode.value) => {
|
|||||||
mLoading.value = true;
|
mLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// FIX: Incorrect endpoint used previously
|
||||||
|
// TAG: 404
|
||||||
const response = await axios.get(`${API_BASE_URL}/matches/open`, {
|
const response = await axios.get(`${API_BASE_URL}/matches/open`, {
|
||||||
params: {
|
params: {
|
||||||
type: mode,
|
type: mode,
|
||||||
|
|||||||
@@ -621,7 +621,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="font-bold text-sm">{{ game.amount }} pts</div>
|
|
||||||
|
<div class="font-bold text-sm">{{ game.points }} pts</div>
|
||||||
<div class="text-[10px] text-muted-foreground">
|
<div class="text-[10px] text-muted-foreground">
|
||||||
{{ new Date(game.began_at).toLocaleDateString() }} •
|
{{ new Date(game.began_at).toLocaleDateString() }} •
|
||||||
{{
|
{{
|
||||||
@@ -1185,10 +1186,15 @@ const fetchData = async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: game.id,
|
id: game.id,
|
||||||
|
player1_id: game.player1.id,
|
||||||
|
player2_id: game.player2.id,
|
||||||
variant: `Type ${game.type}`,
|
variant: `Type ${game.type}`,
|
||||||
outcome: outcome,
|
outcome: outcome,
|
||||||
opponent: null,
|
opponent: null,
|
||||||
amount: game.total_points || 0,
|
amount: game.total_points || 0,
|
||||||
|
points: game.player1_user_id === authStore.currentUser.id
|
||||||
|
? game.player1_points
|
||||||
|
: game.player2_points,
|
||||||
duration: `${Math.floor(game.total_time / 60)}m ${game.total_time % 60}s`,
|
duration: `${Math.floor(game.total_time / 60)}m ${game.total_time % 60}s`,
|
||||||
began_at: game.began_at,
|
began_at: game.began_at,
|
||||||
details: {
|
details: {
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export const useSocketStore = defineStore('websocket', () => {
|
|||||||
socket.value.emit('play-card', { gameId, cardId })
|
socket.value.emit('play-card', { gameId, cardId })
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEW: Surrender Action ---
|
|
||||||
const surrender = (gameId) => {
|
const surrender = (gameId) => {
|
||||||
if (!socket.value?.connected) return
|
if (!socket.value?.connected) return
|
||||||
console.log('[Game] Surrendering:', gameId)
|
console.log('[Game] Surrendering:', gameId)
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ const performGameEnd = (io, game, gameId, winnerId, loserId, reason) => {
|
|||||||
action: "game_ended",
|
action: "game_ended",
|
||||||
winnerId: parseInt(winnerId),
|
winnerId: parseInt(winnerId),
|
||||||
loserId: parseInt(loserId),
|
loserId: parseInt(loserId),
|
||||||
|
totalTime: game.startTime ? Math.floor((Date.now() - game.startTime) / 1000) : 0,
|
||||||
isDraw: false,
|
isDraw: false,
|
||||||
player1_points: game.players[Object.keys(game.players)[0]]?.points || 0,
|
player1_points: game.players[Object.keys(game.players)[0]]?.points || 0,
|
||||||
player2_points: game.players[Object.keys(game.players)[1]]?.points || 0,
|
player2_points: game.players[Object.keys(game.players)[1]]?.points || 0,
|
||||||
|
|||||||
+124
-115
@@ -6,128 +6,137 @@ import gameEvents from "./events/game.js";
|
|||||||
import matchEvents from "./events/match.js";
|
import matchEvents from "./events/match.js";
|
||||||
|
|
||||||
export const server = {
|
export const server = {
|
||||||
io: null,
|
io: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_URL = "http://localhost:8000/api/v1";
|
const API_URL = "http://localhost:8000/api/v1";
|
||||||
|
|
||||||
const disconnectTimers = new Map();
|
const disconnectTimers = new Map();
|
||||||
const DISCONNECT_TIMEOUT = 1 * 60 * 1000; // 1 minutes
|
const RECONNECT_GRACE_PERIOD = 1 * 60 * 1000; // 1 minutes
|
||||||
|
|
||||||
|
const userId = (socket) => socket.user && socket.user.id;
|
||||||
|
|
||||||
export const serverStart = (port) => {
|
export const serverStart = (port) => {
|
||||||
server.io = new Server(port, {
|
server.io = new Server(port, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: "*",
|
origin: "*",
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
server.io.use(async (socket, next) => {
|
|
||||||
let token = socket.handshake.auth.token;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return next(new Error("Authentication error: No token provided"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!token.startsWith("Bearer ")) {
|
|
||||||
token = "Bearer " + token;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${API_URL}/users/me`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: token,
|
|
||||||
Accept: "application/json"
|
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
const user = response.data.data || response.data;
|
|
||||||
|
|
||||||
if (!user || !user.id) {
|
|
||||||
return next(new Error("Authentication error: User data not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.user = user;
|
|
||||||
socket.token = token;
|
|
||||||
socket.handshake.auth.token = token;
|
|
||||||
|
|
||||||
if (disconnectTimers.has(user.id)) {
|
|
||||||
console.log(`[Connection] User ${user.id} reconnected! Cancelling surrender timer.`);
|
|
||||||
clearTimeout(disconnectTimers.get(user.id));
|
|
||||||
disconnectTimers.delete(user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
addUser(socket.id, user);
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response) {
|
|
||||||
console.error(
|
|
||||||
"❌ Erro Laravel:",
|
|
||||||
error.response.status,
|
|
||||||
error.response.data
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("❌ Erro Rede:", error.message);
|
|
||||||
}
|
|
||||||
next(new Error("Authentication error: Invalid token"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.io.on("connection", (socket) => {
|
|
||||||
gameEvents(server.io, socket);
|
|
||||||
console.log(`[Connection] User ${userId} connected. Socket ID: ${socket.id}`);
|
|
||||||
|
|
||||||
matchEvents(server.io, socket);
|
|
||||||
|
|
||||||
handleConnectionEvents(server.io, socket);
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
// Remove from active socket list
|
|
||||||
removeUser(socket.id);
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
console.log(`[Disconnect] User ${userId} disconnected. Waiting ${RECONNECT_GRACE_PERIOD/1000}s...`);
|
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
// 2. START SURRENDER TIMER
|
|
||||||
// -----------------------------------------------------------
|
|
||||||
const timer = setTimeout(async () => {
|
|
||||||
console.log(`[Timeout] User ${userId} did not return. Forcing surrender.`);
|
|
||||||
|
|
||||||
// A. Find the Game ID this user is playing (You need a way to look this up)
|
|
||||||
// Ideally, your 'socket' object or a global map knows which gameID the user is in.
|
|
||||||
// For this example, let's assume you stored `socket.activeGameId` when they joined.
|
|
||||||
const gameId = socket.activeGameId;
|
|
||||||
|
|
||||||
if (gameId) {
|
|
||||||
try {
|
|
||||||
// B. Call Laravel API to resign on their behalf
|
|
||||||
// We use the token we saved during the handshake
|
|
||||||
await axios.post(`${API_URL}/games/${gameId}/resign`, {}, {
|
|
||||||
headers: {
|
|
||||||
Authorization: socket.handshake.auth.token,
|
|
||||||
Accept: "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// C. Notify the room (The API likely triggers a Pusher/Socket event,
|
|
||||||
// but we can also emit locally if needed)
|
|
||||||
server.io.to(gameId).emit("game_over", {
|
|
||||||
winner: "opponent",
|
|
||||||
reason: "disconnect"
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[Timeout] Successfully resigned game ${gameId} for user ${userId}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[Timeout] Failed to resign game for user ${userId}:`, err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectTimers.delete(userId);
|
|
||||||
}, RECONNECT_GRACE_PERIOD);
|
|
||||||
|
|
||||||
disconnectTimers.set(userId, timer);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
server.io.use(async (socket, next) => {
|
||||||
|
let token = socket.handshake.auth.token;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error("Authentication error: No token provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token.startsWith("Bearer ")) {
|
||||||
|
token = "Bearer " + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/users/me`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: token,
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = response.data.data || response.data;
|
||||||
|
|
||||||
|
if (!user || !user.id) {
|
||||||
|
return next(new Error("Authentication error: User data not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.user = user;
|
||||||
|
socket.token = token;
|
||||||
|
socket.handshake.auth.token = token;
|
||||||
|
|
||||||
|
if (disconnectTimers.has(user.id)) {
|
||||||
|
console.log(`[Connection] User ${user.id} reconnected! Cancelling surrender timer.`);
|
||||||
|
clearTimeout(disconnectTimers.get(user.id));
|
||||||
|
disconnectTimers.delete(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser(socket.id, user);
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
console.error(
|
||||||
|
"❌ Erro Laravel:",
|
||||||
|
error.response.status,
|
||||||
|
error.response.data
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("❌ Erro Rede:", error.message);
|
||||||
|
}
|
||||||
|
next(new Error("Authentication error: Invalid token"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.io.on("connection", (socket) => {
|
||||||
|
gameEvents(server.io, socket);
|
||||||
|
matchEvents(server.io, socket);
|
||||||
|
|
||||||
|
handleConnectionEvents(server.io, socket);
|
||||||
|
|
||||||
|
socket.on("notify_disconnect", () => {
|
||||||
|
socket.isVoluntaryDisconnect = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
// Remove from active socket list
|
||||||
|
removeUser(socket.id);
|
||||||
|
|
||||||
|
if (socket.isVoluntaryDisconnect) {
|
||||||
|
console.log(`[Disconnect] User ${userId} disconnected voluntarily. No surrender needed.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
console.log(`[Disconnect] User ${userId} disconnected. Waiting ${RECONNECT_GRACE_PERIOD / 1000}s...`);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 2. START SURRENDER TIMER
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
console.log(`[Timeout] User ${userId} did not return. Forcing surrender.`);
|
||||||
|
|
||||||
|
// A. Find the Game ID this user is playing (You need a way to look this up)
|
||||||
|
// Ideally, your 'socket' object or a global map knows which gameID the user is in.
|
||||||
|
// For this example, let's assume you stored `socket.activeGameId` when they joined.
|
||||||
|
const gameId = socket.activeGameId;
|
||||||
|
|
||||||
|
if (gameId) {
|
||||||
|
try {
|
||||||
|
// B. Call Laravel API to resign on their behalf
|
||||||
|
// We use the token we saved during the handshake
|
||||||
|
await axios.post(`${API_URL}/games/${gameId}/resign`, {}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: socket.handshake.auth.token,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// C. Notify the room (The API likely triggers a Pusher/Socket event,
|
||||||
|
// but we can also emit locally if needed)
|
||||||
|
server.io.to(gameId).emit("game_over", {
|
||||||
|
winner: "opponent",
|
||||||
|
reason: "disconnect"
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Timeout] Successfully resigned game ${gameId} for user ${userId}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Timeout] Failed to resign game for user ${userId}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectTimers.delete(userId);
|
||||||
|
}, RECONNECT_GRACE_PERIOD);
|
||||||
|
|
||||||
|
disconnectTimers.set(userId, timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,5 +33,3 @@ export const removeGame = (id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { createGame, getGame, removeGame };
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user