/* ============================================================ sockets/arena.js 1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby inkl. Kick-Funktion für Team-Leader wurde getestet ============================================================ */ const waitingPool = new Map(); const LEVEL_RANGE = 5; const READY_TIMEOUT = 30; /* ── 2v2 State ── */ const teams2v2 = new Map(); const readyTeams2v2 = new Map(); /* ── 4v4 State ── */ const teams4v4 = new Map(); const readyTeams4v4 = new Map(); function generateId() { return `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; } /* ═══════════════════════════════════════════════════════════ HELPER: Generisch für 2v2 UND 4v4 ═══════════════════════════════════════════════════════════ */ function getTeamMap(mode) { return mode === "4v4" ? teams4v4 : teams2v2; } function getReadyMap(mode) { return mode === "4v4" ? readyTeams4v4 : readyTeams2v2; } function getMaxPlayers(mode) { return mode === "4v4" ? 4 : 2; } function broadcastLobbies(io, mode) { const teams = getTeamMap(mode); const max = getMaxPlayers(mode); const list = []; for (const [teamId, team] of teams) { if (team.players.length < max) { list.push({ teamId, leader: team.players[0]?.player?.name || "?", leaderLevel: team.players[0]?.player?.level || 1, count: team.players.length, max, }); } } io.emit(`${mode}_lobbies`, list); } function broadcastTeamStatus(io, teamId, mode) { const team = getTeamMap(mode).get(teamId); if (!team) return; const data = { teamId, leaderId: team.leaderId, // ← Leader-SocketId mitschicken players: team.players.map((p) => ({ socketId: p.socketId, // ← für Kick-Button im Frontend name: p.player.name, level: p.player.level, ready: team.ready.has(p.socketId), })), count: team.players.length, max: getMaxPlayers(mode), }; for (const p of team.players) { io.to(p.socketId).emit(`${mode}_team_update`, data); } } function tryMatchmaking_nvn(io, mode) { const readyMap = getReadyMap(mode); const readyList = Array.from(readyMap.values()); if (readyList.length < 2) return; const team1 = readyList[0]; const team2 = readyList[1]; readyMap.delete(team1.id); readyMap.delete(team2.id); getTeamMap(mode).delete(team1.id); getTeamMap(mode).delete(team2.id); const matchId = `${mode}_${generateId()}`; const allPlayers = [ ...team1.players.map((p) => ({ team: 1, ...p })), ...team2.players.map((p) => ({ team: 2, ...p })), ]; for (const p of allPlayers) { const opponents = allPlayers .filter((x) => x.team !== p.team) .map((x) => x.player.name); const teammates = allPlayers .filter((x) => x.team === p.team && x.socketId !== p.socketId) .map((x) => x.player.name); const teamRef = p.team === 1 ? team1 : team2; const slotIndex = teamRef.players.findIndex((x) => x.socketId === p.socketId) + 1; io.to(p.socketId).emit(`match_found_${mode}`, { matchId, myTeam: p.team, teammates, opponents, mySlot: `team${p.team}_player${slotIndex}`, }); } console.log( `[${mode}] Match: Team1(${team1.players.map((p) => p.player.name)})` + ` vs Team2(${team2.players.map((p) => p.player.name)}) | ${matchId}`, ); } function leaveAllTeams_nvn(socketId, io, mode) { const teams = getTeamMap(mode); const ready = getReadyMap(mode); for (const [teamId, team] of teams) { const idx = team.players.findIndex((p) => p.socketId === socketId); if (idx === -1) continue; const playerName = team.players[idx].player.name; team.players.splice(idx, 1); team.ready.delete(socketId); ready.delete(teamId); if (team.players.length === 0) { teams.delete(teamId); console.log(`[${mode}] Team ${teamId} aufgelöst.`); } else { // Falls Leader das Team verlässt → nächsten Spieler zum Leader machen if (team.leaderId === socketId) { team.leaderId = team.players[0].socketId; console.log( `[${mode}] Neuer Leader in Team ${teamId}: ${team.players[0].player.name}`, ); } broadcastTeamStatus(io, teamId, mode); for (const p of team.players) { io.to(p.socketId).emit(`${mode}_partner_left`, { name: playerName }); } console.log(`[${mode}] ${playerName} hat Team ${teamId} verlassen.`); } broadcastLobbies(io, mode); break; } } /* ── 1v1 Matchmaking ── */ function tryMatchmaking(io, newSocketId) { const challenger = waitingPool.get(newSocketId); if (!challenger) return; for (const [id, entry] of waitingPool) { if (id === newSocketId) continue; if (Math.abs(entry.player.level - challenger.player.level) <= LEVEL_RANGE) { waitingPool.delete(newSocketId); waitingPool.delete(id); const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; challenger.socket.emit("match_found", { matchId, opponent: entry.player, mySlot: "player1", }); entry.socket.emit("match_found", { matchId, opponent: challenger.player, mySlot: "player2", }); console.log( `[1v1] ${challenger.player.name} vs ${entry.player.name} | ${matchId}`, ); return; } } } /* ── Bereit-Timer (1v1) ── */ function startReadyTimer(io, matchId) { if (!io._arenaTimers) io._arenaTimers = new Map(); if (io._arenaTimers.has(matchId)) return; let remaining = READY_TIMEOUT; io.to("arena_" + matchId).emit("ready_timer", { remaining }); const interval = setInterval(() => { remaining--; io.to("arena_" + matchId).emit("ready_timer", { remaining }); if (remaining <= 0) { clearInterval(interval); io._arenaTimers.delete(matchId); console.log(`[1v1] Match ${matchId} abgebrochen – Zeit abgelaufen.`); io.to("arena_" + matchId).emit("match_cancelled", { reason: "timeout", message: "Zeit abgelaufen.", }); } }, 1000); io._arenaTimers.set(matchId, interval); } function stopReadyTimer(io, matchId) { if (!io._arenaTimers) return; const interval = io._arenaTimers.get(matchId); if (interval) { clearInterval(interval); io._arenaTimers.delete(matchId); } } /* ═══════════════════════════════════════════════════════════ Handler-Generator für 2v2 und 4v4 ═══════════════════════════════════════════════════════════ */ function registerTeamModeHandlers(io, socket, mode) { const max = getMaxPlayers(mode); /* Lobby-Liste anfordern */ socket.on(`get_${mode}_lobbies`, () => { const teams = getTeamMap(mode); const list = []; for (const [teamId, team] of teams) { if (team.players.length < max) { list.push({ teamId, leader: team.players[0]?.player?.name || "?", leaderLevel: team.players[0]?.player?.level || 1, count: team.players.length, max, }); } } socket.emit(`${mode}_lobbies`, list); }); /* Team erstellen */ socket.on(`create_${mode}_team`, (playerData) => { leaveAllTeams_nvn(socket.id, io, mode); const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; const teamId = `team_${mode}_${generateId()}`; getTeamMap(mode).set(teamId, { id: teamId, leaderId: socket.id, // ← Ersteller wird Leader players: [{ socketId: socket.id, player }], ready: new Set(), }); socket.emit(`${mode}_team_joined`, { teamId, isLeader: true }); broadcastTeamStatus(io, teamId, mode); broadcastLobbies(io, mode); console.log(`[${mode}] Team ${teamId} erstellt von ${player.name}`); }); /* Team beitreten */ socket.on(`join_${mode}_team`, (data) => { const { teamId, playerData } = data; const team = getTeamMap(mode).get(teamId); if (!team) return socket.emit(`${mode}_error`, { message: "Team nicht mehr verfügbar.", }); if (team.players.length >= max) return socket.emit(`${mode}_error`, { message: "Team ist bereits voll.", }); if (team.players.find((p) => p.socketId === socket.id)) return; leaveAllTeams_nvn(socket.id, io, mode); const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; team.players.push({ socketId: socket.id, player }); socket.emit(`${mode}_team_joined`, { teamId, isLeader: false }); broadcastTeamStatus(io, teamId, mode); broadcastLobbies(io, mode); console.log(`[${mode}] ${player.name} hat Team ${teamId} beigetreten`); }); /* Team verlassen */ socket.on(`leave_${mode}_team`, () => { leaveAllTeams_nvn(socket.id, io, mode); }); /* ── Kick (nur Leader) ── */ socket.on(`kick_from_${mode}_team`, (data) => { const { teamId, targetSocketId } = data; const team = getTeamMap(mode).get(teamId); if (!team) return; // Nur der Leader darf kicken if (team.leaderId !== socket.id) { return socket.emit(`${mode}_error`, { message: "Nur der Team-Leader kann Spieler entfernen.", }); } // Sich selbst kicken nicht erlaubt if (targetSocketId === socket.id) return; const idx = team.players.findIndex((p) => p.socketId === targetSocketId); if (idx === -1) return; const kickedName = team.players[idx].player.name; team.players.splice(idx, 1); team.ready.delete(targetSocketId); getReadyMap(mode).delete(teamId); // Gekickten Spieler benachrichtigen io.to(targetSocketId).emit(`${mode}_kicked`, { message: "Du wurdest vom Team-Leader aus dem Team entfernt.", }); broadcastTeamStatus(io, teamId, mode); broadcastLobbies(io, mode); console.log(`[${mode}] ${kickedName} wurde aus Team ${teamId} gekickt.`); }); /* Bereit klicken */ socket.on(`${mode}_player_ready`, (data) => { const { teamId } = data; const team = getTeamMap(mode).get(teamId); if (!team || team.players.length < max) return; team.ready.add(socket.id); broadcastTeamStatus(io, teamId, mode); console.log( `[${mode}] Bereit in Team ${teamId}: ${team.ready.size}/${max}`, ); if (team.ready.size >= max) { getReadyMap(mode).set(teamId, team); for (const p of team.players) { io.to(p.socketId).emit(`${mode}_searching`, { message: "Suche nach Gegnerteam…", }); } console.log(`[${mode}] Team ${teamId} sucht Gegner.`); tryMatchmaking_nvn(io, mode); } }); } /* ═══════════════════════════════════════════════════════════ HAUPT-HANDLER ═══════════════════════════════════════════════════════════ */ function registerArenaHandlers(io, socket) { /* ── 1v1 ── */ socket.on("join_1v1", (playerData) => { if (waitingPool.has(socket.id)) return; const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; waitingPool.set(socket.id, { socket, player }); socket.emit("queue_status", { status: "waiting", poolSize: waitingPool.size, }); console.log( `[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`, ); tryMatchmaking(io, socket.id); }); socket.on("leave_1v1", () => { if (waitingPool.delete(socket.id)) socket.emit("queue_status", { status: "left" }); }); socket.on("arena_join", (data) => { const { matchId, slot, playerName } = data; console.log( `[1v1] arena_join empfangen: matchId=${matchId}, slot=${slot}, name=${playerName}, socketId=${socket.id}`, ); if (!matchId || !slot) { console.warn(`[1v1] arena_join abgewiesen – matchId oder slot fehlt`); return; } if (!io._arenaRooms) io._arenaRooms = new Map(); if (!io._arenaRooms.has(matchId)) io._arenaRooms.set(matchId, { sockets: {}, names: {} }); const room = io._arenaRooms.get(matchId); room.sockets[slot] = socket.id; // Name aus socket.user (Objekt) oder vom Client mitgesendet const u = socket.user; room.names[slot] = (u && (u.ingame_name || u.username || u.name)) || playerName || "Spieler"; socket.join("arena_" + matchId); const otherSlot = slot === "player1" ? "player2" : "player1"; if (room.sockets[otherSlot]) { console.log( `[1v1] Beide Spieler da → arena_ready senden | Match ${matchId}`, ); io.to("arena_" + matchId).emit("arena_ready", { player1: room.names["player1"] || "Spieler 1", player2: room.names["player2"] || "Spieler 2", }); startReadyTimer(io, matchId); } else { console.log( `[1v1] Erster Spieler joined, warte auf zweiten | Match ${matchId}`, ); socket .to("arena_" + matchId) .emit("arena_opponent_joined", { name: room.names[slot], slot }); } }); socket.on("player_ready", (data) => { const { matchId, slot } = data; if (!matchId || !slot) return; if (!io._arenaReady) io._arenaReady = new Map(); if (!io._arenaReady.has(matchId)) io._arenaReady.set(matchId, new Set()); const readySet = io._arenaReady.get(matchId); readySet.add(slot); io.to("arena_" + matchId).emit("ready_status", { readyCount: readySet.size, readySlots: Array.from(readySet), }); if (readySet.size >= 2) { stopReadyTimer(io, matchId); io._arenaReady.delete(matchId); } }); socket.on("player_surrender", (data) => { const { matchId, slot } = data; io.to("arena_" + matchId).emit("player_surrendered", { slot }); }); /* ── Zugwechsel ── */ socket.on("end_turn", (data) => { const { matchId, slot } = data; if (!matchId || !slot) return; const nextSlot = slot === "player1" ? "player2" : "player1"; io.to("arena_" + matchId).emit("turn_change", { activeSlot: nextSlot }); console.log(`[1v1] Zug: ${slot} → ${nextSlot} | Match ${matchId}`); }); socket.on("end_turn_init", (data) => { const { matchId, starterSlot } = data; if (!matchId || !starterSlot) return; // Nur einmal senden (erster Empfang gewinnt) if (!io._turnInit) io._turnInit = new Set(); if (io._turnInit.has(matchId)) return; io._turnInit.add(matchId); io.to("arena_" + matchId).emit("turn_change", { activeSlot: starterSlot }); console.log(`[1v1] Startzug: ${starterSlot} | Match ${matchId}`); // Cleanup nach 10s setTimeout(() => io._turnInit?.delete(matchId), 10000); }); /* ── 2v2 & 4v4 ── */ registerTeamModeHandlers(io, socket, "2v2"); registerTeamModeHandlers(io, socket, "4v4"); /* ── Disconnect ── */ socket.on("disconnect", () => { if (waitingPool.delete(socket.id)) { console.log(`[1v1] ${socket.id} disconnected – aus Pool entfernt.`); } leaveAllTeams_nvn(socket.id, io, "2v2"); leaveAllTeams_nvn(socket.id, io, "4v4"); }); } module.exports = { registerArenaHandlers };