Version 1.0 #102

Merged
FernandoJVideira merged 163 commits from develop into master 2026-01-03 14:49:00 +00:00
9 changed files with 227 additions and 143 deletions
Showing only changes of commit 3dcd8df4c7 - Show all commits
@@ -244,4 +244,19 @@ class GameController extends Controller
'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();
}
}
+73 -19
View File
@@ -291,18 +291,88 @@ const handleBeforeUnload = (event) => {
}
};
onBeforeRouteLeave((to, from, next) => {
if (gameOver.value || !shouldShowBoard.value) {
// --- FIX: SURRENDER LOGIC ---
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();
return;
}
// CASE C: Game is Active/Playing (The Surrender Logic we built before)
const answer = window.confirm(
"If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?"
);
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();
next();
} 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 () => {
await fetchGameMetadata()
wsStore.connect()
+2
View File
@@ -97,6 +97,8 @@ const getPendingMatches = async (mode = selectedMode.value) => {
mLoading.value = true;
try {
// FIX: Incorrect endpoint used previously
// TAG: 404
const response = await axios.get(`${API_BASE_URL}/matches/open`, {
params: {
type: mode,
+7 -1
View File
@@ -621,7 +621,8 @@
</div>
<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">
{{ new Date(game.began_at).toLocaleDateString() }}
{{
@@ -1185,10 +1186,15 @@ const fetchData = async () => {
return {
id: game.id,
player1_id: game.player1.id,
player2_id: game.player2.id,
variant: `Type ${game.type}`,
outcome: outcome,
opponent: null,
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`,
began_at: game.began_at,
details: {
-1
View File
@@ -92,7 +92,6 @@ export const useSocketStore = defineStore('websocket', () => {
socket.value.emit('play-card', { gameId, cardId })
}
// --- NEW: Surrender Action ---
const surrender = (gameId) => {
if (!socket.value?.connected) return
console.log('[Game] Surrendering:', gameId)
+1
View File
@@ -96,6 +96,7 @@ const performGameEnd = (io, game, gameId, winnerId, loserId, reason) => {
action: "game_ended",
winnerId: parseInt(winnerId),
loserId: parseInt(loserId),
totalTime: game.startTime ? Math.floor((Date.now() - game.startTime) / 1000) : 0,
isDraw: false,
player1_points: game.players[Object.keys(game.players)[0]]?.points || 0,
player2_points: game.players[Object.keys(game.players)[1]]?.points || 0,
+124 -115
View File
@@ -6,128 +6,137 @@ import gameEvents from "./events/game.js";
import matchEvents from "./events/match.js";
export const server = {
io: null,
io: null,
};
const API_URL = "http://localhost:8000/api/v1";
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) => {
server.io = new Server(port, {
cors: {
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"
server.io = new Server(port, {
cors: {
origin: "*",
},
});
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);
}
});
});
};
-2
View File
@@ -33,5 +33,3 @@ export const removeGame = (id) => {
}
};
export { createGame, getGame, removeGame };