/* ============================================================ sockets/arena.js Alle Socket-Events rund um 1v1 Matchmaking & 2v2 Team-Lobby ============================================================ */ const waitingPool = new Map(); // socketId → { socket, player } const LEVEL_RANGE = 5; // 2v2 Team-Lobby // teams2v2: teamId → { id, players: [{socketId, player}], ready: Set } const teams2v2 = new Map(); const readyTeams = new Map(); // teamId → team (fertige Teams warten auf Matchmaking) const READY_TIMEOUT = 30; // Sekunden bis Match abgebrochen wird function generateId() { return `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; } /* ───────────────────────────────────────────────── HELPER: 2v2 Lobby-Liste an alle senden ───────────────────────────────────────────────── */ function broadcastLobbies(io) { const list = []; for (const [teamId, team] of teams2v2) { if (team.players.length < 2) { list.push({ teamId, leader: team.players[0]?.player?.name || "?", leaderLevel: team.players[0]?.player?.level || 1, count: team.players.length, }); } } io.emit("2v2_lobbies", list); } /* ───────────────────────────────────────────────── HELPER: 2v2 Team-Status an Teammitglieder senden ───────────────────────────────────────────────── */ function broadcastTeamStatus(io, teamId) { const team = teams2v2.get(teamId); if (!team) return; const data = { teamId, players: team.players.map(p => ({ name: p.player.name, level: p.player.level, ready: team.ready.has(p.socketId), })), count: team.players.length, }; for (const p of team.players) { io.to(p.socketId).emit("2v2_team_update", data); } } /* ───────────────────────────────────────────────── HELPER: 2v2 Matchmaking ───────────────────────────────────────────────── */ function tryMatchmaking2v2(io) { const readyList = Array.from(readyTeams.values()); if (readyList.length < 2) return; const team1 = readyList[0]; const team2 = readyList[1]; readyTeams.delete(team1.id); readyTeams.delete(team2.id); teams2v2.delete(team1.id); teams2v2.delete(team2.id); const matchId = `2v2_${generateId()}`; const allPlayers = [ { team: 1, ...team1.players[0] }, { team: 1, ...team1.players[1] }, { team: 2, ...team2.players[0] }, { team: 2, ...team2.players[1] }, ]; 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_2v2", { matchId, myTeam: p.team, teammates, opponents, mySlot: `team${p.team}_player${slotIndex}`, }); } console.log( `[2v2] Match: Team1(${team1.players.map(p => p.player.name)})` + ` vs Team2(${team2.players.map(p => p.player.name)}) | ${matchId}` ); } /* ───────────────────────────────────────────────── HELPER: 1v1 Matchmaking ───────────────────────────────────────────────── */ function tryMatchmaking(io, newSocketId) { const challenger = waitingPool.get(newSocketId); if (!challenger) return; for (const [id, entry] of waitingPool) { if (id === newSocketId) continue; const levelDiff = Math.abs(entry.player.level - challenger.player.level); if (levelDiff <= 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] Match: ${challenger.player.name} (Lvl ${challenger.player.level})` + ` vs ${entry.player.name} (Lvl ${entry.player.level}) | ${matchId}` ); return; } } } /* ───────────────────────────────────────────────── HELPER: 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 – Match wird abgebrochen.", }); } }, 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); console.log(`[1v1] Timer für Match ${matchId} gestoppt (beide bereit).`); } } /* ───────────────────────────────────────────────── HELPER: Spieler aus allen 2v2-Teams entfernen ───────────────────────────────────────────────── */ function leaveAllTeams(socketId, io) { for (const [teamId, team] of teams2v2) { 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); readyTeams.delete(teamId); if (team.players.length === 0) { teams2v2.delete(teamId); console.log(`[2v2] Team ${teamId} aufgelöst.`); } else { broadcastTeamStatus(io, teamId); for (const p of team.players) { io.to(p.socketId).emit("2v2_partner_left", { name: playerName }); } console.log(`[2v2] ${playerName} hat Team ${teamId} verlassen.`); } broadcastLobbies(io); break; } } /* ═══════════════════════════════════════════════════════════ HAUPT-HANDLER – wird einmal pro Socket-Verbindung gerufen ═══════════════════════════════════════════════════════════ */ function registerArenaHandlers(io, socket) { /* ── 1v1: Queue beitreten ── */ 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, message: `Suche Gegner (Level ${player.level - LEVEL_RANGE}–${player.level + LEVEL_RANGE})…`, }); console.log(`[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`); tryMatchmaking(io, socket.id); }); /* ── 1v1: Queue verlassen ── */ socket.on("leave_1v1", () => { if (waitingPool.delete(socket.id)) { socket.emit("queue_status", { status: "left" }); console.log(`[1v1] ${socket.id} hat Pool verlassen.`); } }); /* ── 1v1: Spielfeld betreten ── */ socket.on("arena_join", (data) => { const { matchId, slot } = data; if (!matchId || !slot) 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; room.names[slot] = socket.user || "Spieler"; socket.join("arena_" + matchId); const otherSlot = slot === "player1" ? "player2" : "player1"; if (room.sockets[otherSlot]) { io.to("arena_" + matchId).emit("arena_ready", { player1: room.names["player1"] || "Spieler 1", player2: room.names["player2"] || "Spieler 2", }); console.log(`[Arena] Match ${matchId} bereit: ${room.names["player1"]} vs ${room.names["player2"]}`); startReadyTimer(io, matchId); } else { socket.to("arena_" + matchId).emit("arena_opponent_joined", { name: room.names[slot], slot, }); } }); /* ── 1v1: Bereit-System ── */ 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), }); console.log(`[1v1] ${slot} bereit in ${matchId} (${readySet.size}/2)`); if (readySet.size >= 2) { stopReadyTimer(io, matchId); io._arenaReady.delete(matchId); } }); /* ── 1v1: Aufgeben ── */ socket.on("player_surrender", (data) => { const { matchId, slot } = data; console.log(`[1v1] ${slot} hat aufgegeben in Match ${matchId}`); io.to("arena_" + matchId).emit("player_surrendered", { slot }); }); /* ════════════════════════════════════════════ 2v2 TEAM LOBBY ════════════════════════════════════════════ */ /* ── Lobby-Liste anfordern ── */ socket.on("get_2v2_lobbies", () => { const list = []; for (const [teamId, team] of teams2v2) { if (team.players.length < 2) { list.push({ teamId, leader: team.players[0]?.player?.name || "?", leaderLevel: team.players[0]?.player?.level || 1, count: team.players.length, }); } } socket.emit("2v2_lobbies", list); }); /* ── Neues Team erstellen ── */ socket.on("create_2v2_team", (playerData) => { leaveAllTeams(socket.id, io); const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; const teamId = `team_${generateId()}`; teams2v2.set(teamId, { id: teamId, players: [{ socketId: socket.id, player }], ready: new Set(), }); socket.emit("2v2_team_joined", { teamId, isLeader: true }); broadcastTeamStatus(io, teamId); broadcastLobbies(io); console.log(`[2v2] Team ${teamId} erstellt von ${player.name}`); }); /* ── Team beitreten ── */ socket.on("join_2v2_team", (data) => { const { teamId, playerData } = data; const team = teams2v2.get(teamId); if (!team) return socket.emit("2v2_error", { message: "Team nicht mehr verfügbar." }); if (team.players.length >= 2) return socket.emit("2v2_error", { message: "Team ist bereits voll." }); if (team.players.find(p => p.socketId === socket.id)) return; leaveAllTeams(socket.id, io); const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; team.players.push({ socketId: socket.id, player }); socket.emit("2v2_team_joined", { teamId, isLeader: false }); broadcastTeamStatus(io, teamId); broadcastLobbies(io); console.log(`[2v2] ${player.name} hat Team ${teamId} beigetreten`); }); /* ── Team verlassen ── */ socket.on("leave_2v2_team", () => { leaveAllTeams(socket.id, io); }); /* ── Bereit klicken ── */ socket.on("2v2_player_ready", (data) => { const { teamId } = data; const team = teams2v2.get(teamId); if (!team || team.players.length < 2) return; team.ready.add(socket.id); broadcastTeamStatus(io, teamId); console.log(`[2v2] Bereit in Team ${teamId}: ${team.ready.size}/2`); if (team.ready.size >= 2) { readyTeams.set(teamId, team); for (const p of team.players) { io.to(p.socketId).emit("2v2_searching", { message: "Suche nach Gegnerteam…" }); } console.log(`[2v2] Team ${teamId} sucht Gegner. Ready-Teams: ${readyTeams.size}`); tryMatchmaking2v2(io); } }); /* ── Disconnect ── */ socket.on("disconnect", () => { if (waitingPool.delete(socket.id)) { console.log(`[1v1] ${socket.id} disconnected – aus 1v1-Pool entfernt.`); } leaveAllTeams(socket.id, io); }); } module.exports = { registerArenaHandlers };