This commit is contained in:
cay 2026-04-10 08:55:33 +01:00
parent fea1b776e5
commit fea2a99b62

View File

@ -1,19 +1,19 @@
/* ============================================================ /* ============================================================
sockets/arena.js sockets/arena.js
1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby 1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby
inkl. Kick-Funktion für Team-Leader inkl. Kick-Funktion für Team-Leader wurde getestet
============================================================ */ ============================================================ */
const waitingPool = new Map(); const waitingPool = new Map();
const LEVEL_RANGE = 5; const LEVEL_RANGE = 5;
const READY_TIMEOUT = 30; const READY_TIMEOUT = 30;
/* ── 2v2 State ── */ /* ── 2v2 State ── */
const teams2v2 = new Map(); const teams2v2 = new Map();
const readyTeams2v2 = new Map(); const readyTeams2v2 = new Map();
/* ── 4v4 State ── */ /* ── 4v4 State ── */
const teams4v4 = new Map(); const teams4v4 = new Map();
const readyTeams4v4 = new Map(); const readyTeams4v4 = new Map();
function generateId() { function generateId() {
@ -23,21 +23,27 @@ function generateId() {
/* /*
HELPER: Generisch für 2v2 UND 4v4 HELPER: Generisch für 2v2 UND 4v4
*/ */
function getTeamMap(mode) { return mode === "4v4" ? teams4v4 : teams2v2; } function getTeamMap(mode) {
function getReadyMap(mode) { return mode === "4v4" ? readyTeams4v4 : readyTeams2v2; } return mode === "4v4" ? teams4v4 : teams2v2;
function getMaxPlayers(mode) { return mode === "4v4" ? 4 : 2; } }
function getReadyMap(mode) {
return mode === "4v4" ? readyTeams4v4 : readyTeams2v2;
}
function getMaxPlayers(mode) {
return mode === "4v4" ? 4 : 2;
}
function broadcastLobbies(io, mode) { function broadcastLobbies(io, mode) {
const teams = getTeamMap(mode); const teams = getTeamMap(mode);
const max = getMaxPlayers(mode); const max = getMaxPlayers(mode);
const list = []; const list = [];
for (const [teamId, team] of teams) { for (const [teamId, team] of teams) {
if (team.players.length < max) { if (team.players.length < max) {
list.push({ list.push({
teamId, teamId,
leader: team.players[0]?.player?.name || "?", leader: team.players[0]?.player?.name || "?",
leaderLevel: team.players[0]?.player?.level || 1, leaderLevel: team.players[0]?.player?.level || 1,
count: team.players.length, count: team.players.length,
max, max,
}); });
} }
@ -50,15 +56,15 @@ function broadcastTeamStatus(io, teamId, mode) {
if (!team) return; if (!team) return;
const data = { const data = {
teamId, teamId,
leaderId: team.leaderId, // ← Leader-SocketId mitschicken leaderId: team.leaderId, // ← Leader-SocketId mitschicken
players: team.players.map(p => ({ players: team.players.map((p) => ({
socketId: p.socketId, // ← für Kick-Button im Frontend socketId: p.socketId, // ← für Kick-Button im Frontend
name: p.player.name, name: p.player.name,
level: p.player.level, level: p.player.level,
ready: team.ready.has(p.socketId), ready: team.ready.has(p.socketId),
})), })),
count: team.players.length, count: team.players.length,
max: getMaxPlayers(mode), max: getMaxPlayers(mode),
}; };
for (const p of team.players) { for (const p of team.players) {
io.to(p.socketId).emit(`${mode}_team_update`, data); io.to(p.socketId).emit(`${mode}_team_update`, data);
@ -66,7 +72,7 @@ function broadcastTeamStatus(io, teamId, mode) {
} }
function tryMatchmaking_nvn(io, mode) { function tryMatchmaking_nvn(io, mode) {
const readyMap = getReadyMap(mode); const readyMap = getReadyMap(mode);
const readyList = Array.from(readyMap.values()); const readyList = Array.from(readyMap.values());
if (readyList.length < 2) return; if (readyList.length < 2) return;
@ -78,17 +84,22 @@ function tryMatchmaking_nvn(io, mode) {
getTeamMap(mode).delete(team1.id); getTeamMap(mode).delete(team1.id);
getTeamMap(mode).delete(team2.id); getTeamMap(mode).delete(team2.id);
const matchId = `${mode}_${generateId()}`; const matchId = `${mode}_${generateId()}`;
const allPlayers = [ const allPlayers = [
...team1.players.map(p => ({ team: 1, ...p })), ...team1.players.map((p) => ({ team: 1, ...p })),
...team2.players.map(p => ({ team: 2, ...p })), ...team2.players.map((p) => ({ team: 2, ...p })),
]; ];
for (const p of allPlayers) { for (const p of allPlayers) {
const opponents = allPlayers.filter(x => x.team !== p.team).map(x => x.player.name); const opponents = allPlayers
const teammates = allPlayers.filter(x => x.team === p.team && x.socketId !== p.socketId).map(x => x.player.name); .filter((x) => x.team !== p.team)
const teamRef = p.team === 1 ? team1 : team2; .map((x) => x.player.name);
const slotIndex = teamRef.players.findIndex(x => x.socketId === p.socketId) + 1; 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}`, { io.to(p.socketId).emit(`match_found_${mode}`, {
matchId, matchId,
@ -100,8 +111,8 @@ function tryMatchmaking_nvn(io, mode) {
} }
console.log( console.log(
`[${mode}] Match: Team1(${team1.players.map(p => p.player.name)})` + `[${mode}] Match: Team1(${team1.players.map((p) => p.player.name)})` +
` vs Team2(${team2.players.map(p => p.player.name)}) | ${matchId}` ` vs Team2(${team2.players.map((p) => p.player.name)}) | ${matchId}`,
); );
} }
@ -110,7 +121,7 @@ function leaveAllTeams_nvn(socketId, io, mode) {
const ready = getReadyMap(mode); const ready = getReadyMap(mode);
for (const [teamId, team] of teams) { for (const [teamId, team] of teams) {
const idx = team.players.findIndex(p => p.socketId === socketId); const idx = team.players.findIndex((p) => p.socketId === socketId);
if (idx === -1) continue; if (idx === -1) continue;
const playerName = team.players[idx].player.name; const playerName = team.players[idx].player.name;
@ -125,7 +136,9 @@ function leaveAllTeams_nvn(socketId, io, mode) {
// Falls Leader das Team verlässt → nächsten Spieler zum Leader machen // Falls Leader das Team verlässt → nächsten Spieler zum Leader machen
if (team.leaderId === socketId) { if (team.leaderId === socketId) {
team.leaderId = team.players[0].socketId; team.leaderId = team.players[0].socketId;
console.log(`[${mode}] Neuer Leader in Team ${teamId}: ${team.players[0].player.name}`); console.log(
`[${mode}] Neuer Leader in Team ${teamId}: ${team.players[0].player.name}`,
);
} }
broadcastTeamStatus(io, teamId, mode); broadcastTeamStatus(io, teamId, mode);
for (const p of team.players) { for (const p of team.players) {
@ -150,10 +163,20 @@ function tryMatchmaking(io, newSocketId) {
waitingPool.delete(id); waitingPool.delete(id);
const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
challenger.socket.emit("match_found", { matchId, opponent: entry.player, mySlot: "player1" }); challenger.socket.emit("match_found", {
entry.socket.emit("match_found", { matchId, opponent: challenger.player, mySlot: "player2" }); 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}`); console.log(
`[1v1] ${challenger.player.name} vs ${entry.player.name} | ${matchId}`,
);
return; return;
} }
} }
@ -174,7 +197,10 @@ function startReadyTimer(io, matchId) {
clearInterval(interval); clearInterval(interval);
io._arenaTimers.delete(matchId); io._arenaTimers.delete(matchId);
console.log(`[1v1] Match ${matchId} abgebrochen Zeit abgelaufen.`); console.log(`[1v1] Match ${matchId} abgebrochen Zeit abgelaufen.`);
io.to("arena_" + matchId).emit("match_cancelled", { reason: "timeout", message: "Zeit abgelaufen." }); io.to("arena_" + matchId).emit("match_cancelled", {
reason: "timeout",
message: "Zeit abgelaufen.",
});
} }
}, 1000); }, 1000);
@ -199,14 +225,14 @@ function registerTeamModeHandlers(io, socket, mode) {
/* Lobby-Liste anfordern */ /* Lobby-Liste anfordern */
socket.on(`get_${mode}_lobbies`, () => { socket.on(`get_${mode}_lobbies`, () => {
const teams = getTeamMap(mode); const teams = getTeamMap(mode);
const list = []; const list = [];
for (const [teamId, team] of teams) { for (const [teamId, team] of teams) {
if (team.players.length < max) { if (team.players.length < max) {
list.push({ list.push({
teamId, teamId,
leader: team.players[0]?.player?.name || "?", leader: team.players[0]?.player?.name || "?",
leaderLevel: team.players[0]?.player?.level || 1, leaderLevel: team.players[0]?.player?.level || 1,
count: team.players.length, count: team.players.length,
max, max,
}); });
} }
@ -218,14 +244,18 @@ function registerTeamModeHandlers(io, socket, mode) {
socket.on(`create_${mode}_team`, (playerData) => { socket.on(`create_${mode}_team`, (playerData) => {
leaveAllTeams_nvn(socket.id, io, mode); leaveAllTeams_nvn(socket.id, io, mode);
const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1 }; const player = {
id: playerData.id,
name: playerData.name,
level: Number(playerData.level) || 1,
};
const teamId = `team_${mode}_${generateId()}`; const teamId = `team_${mode}_${generateId()}`;
getTeamMap(mode).set(teamId, { getTeamMap(mode).set(teamId, {
id: teamId, id: teamId,
leaderId: socket.id, // ← Ersteller wird Leader leaderId: socket.id, // ← Ersteller wird Leader
players: [{ socketId: socket.id, player }], players: [{ socketId: socket.id, player }],
ready: new Set(), ready: new Set(),
}); });
socket.emit(`${mode}_team_joined`, { teamId, isLeader: true }); socket.emit(`${mode}_team_joined`, { teamId, isLeader: true });
@ -239,13 +269,23 @@ function registerTeamModeHandlers(io, socket, mode) {
const { teamId, playerData } = data; const { teamId, playerData } = data;
const team = getTeamMap(mode).get(teamId); const team = getTeamMap(mode).get(teamId);
if (!team) return socket.emit(`${mode}_error`, { message: "Team nicht mehr verfügbar." }); if (!team)
if (team.players.length >= max) return socket.emit(`${mode}_error`, { message: "Team ist bereits voll." }); return socket.emit(`${mode}_error`, {
if (team.players.find(p => p.socketId===socket.id)) return; 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); leaveAllTeams_nvn(socket.id, io, mode);
const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1 }; const player = {
id: playerData.id,
name: playerData.name,
level: Number(playerData.level) || 1,
};
team.players.push({ socketId: socket.id, player }); team.players.push({ socketId: socket.id, player });
socket.emit(`${mode}_team_joined`, { teamId, isLeader: false }); socket.emit(`${mode}_team_joined`, { teamId, isLeader: false });
@ -268,13 +308,15 @@ function registerTeamModeHandlers(io, socket, mode) {
// Nur der Leader darf kicken // Nur der Leader darf kicken
if (team.leaderId !== socket.id) { if (team.leaderId !== socket.id) {
return socket.emit(`${mode}_error`, { message: "Nur der Team-Leader kann Spieler entfernen." }); return socket.emit(`${mode}_error`, {
message: "Nur der Team-Leader kann Spieler entfernen.",
});
} }
// Sich selbst kicken nicht erlaubt // Sich selbst kicken nicht erlaubt
if (targetSocketId === socket.id) return; if (targetSocketId === socket.id) return;
const idx = team.players.findIndex(p => p.socketId === targetSocketId); const idx = team.players.findIndex((p) => p.socketId === targetSocketId);
if (idx === -1) return; if (idx === -1) return;
const kickedName = team.players[idx].player.name; const kickedName = team.players[idx].player.name;
@ -300,12 +342,16 @@ function registerTeamModeHandlers(io, socket, mode) {
team.ready.add(socket.id); team.ready.add(socket.id);
broadcastTeamStatus(io, teamId, mode); broadcastTeamStatus(io, teamId, mode);
console.log(`[${mode}] Bereit in Team ${teamId}: ${team.ready.size}/${max}`); console.log(
`[${mode}] Bereit in Team ${teamId}: ${team.ready.size}/${max}`,
);
if (team.ready.size >= max) { if (team.ready.size >= max) {
getReadyMap(mode).set(teamId, team); getReadyMap(mode).set(teamId, team);
for (const p of team.players) { for (const p of team.players) {
io.to(p.socketId).emit(`${mode}_searching`, { message: "Suche nach Gegnerteam…" }); io.to(p.socketId).emit(`${mode}_searching`, {
message: "Suche nach Gegnerteam…",
});
} }
console.log(`[${mode}] Team ${teamId} sucht Gegner.`); console.log(`[${mode}] Team ${teamId} sucht Gegner.`);
tryMatchmaking_nvn(io, mode); tryMatchmaking_nvn(io, mode);
@ -317,54 +363,71 @@ function registerTeamModeHandlers(io, socket, mode) {
HAUPT-HANDLER HAUPT-HANDLER
*/ */
function registerArenaHandlers(io, socket) { function registerArenaHandlers(io, socket) {
/* ── 1v1 ── */ /* ── 1v1 ── */
socket.on("join_1v1", (playerData) => { socket.on("join_1v1", (playerData) => {
if (waitingPool.has(socket.id)) return; if (waitingPool.has(socket.id)) return;
const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1 }; const player = {
id: playerData.id,
name: playerData.name,
level: Number(playerData.level) || 1,
};
waitingPool.set(socket.id, { socket, player }); waitingPool.set(socket.id, { socket, player });
socket.emit("queue_status", { status: "waiting", poolSize: waitingPool.size }); socket.emit("queue_status", {
console.log(`[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`); status: "waiting",
poolSize: waitingPool.size,
});
console.log(
`[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`,
);
tryMatchmaking(io, socket.id); tryMatchmaking(io, socket.id);
}); });
socket.on("leave_1v1", () => { socket.on("leave_1v1", () => {
if (waitingPool.delete(socket.id)) socket.emit("queue_status", { status: "left" }); if (waitingPool.delete(socket.id))
socket.emit("queue_status", { status: "left" });
}); });
socket.on("arena_join", (data) => { socket.on("arena_join", (data) => {
const { matchId, slot, playerName } = data; const { matchId, slot, playerName } = data;
console.log(`[1v1] arena_join empfangen: matchId=${matchId}, slot=${slot}, name=${playerName}, socketId=${socket.id}`); console.log(
`[1v1] arena_join empfangen: matchId=${matchId}, slot=${slot}, name=${playerName}, socketId=${socket.id}`,
);
if (!matchId || !slot) { if (!matchId || !slot) {
console.warn(`[1v1] arena_join abgewiesen matchId oder slot fehlt`); console.warn(`[1v1] arena_join abgewiesen matchId oder slot fehlt`);
return; return;
} }
if (!io._arenaRooms) io._arenaRooms = new Map(); if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) io._arenaRooms.set(matchId, { sockets: {}, names: {} }); if (!io._arenaRooms.has(matchId))
io._arenaRooms.set(matchId, { sockets: {}, names: {} });
const room = io._arenaRooms.get(matchId); const room = io._arenaRooms.get(matchId);
room.sockets[slot] = socket.id; room.sockets[slot] = socket.id;
// Name aus socket.user (Objekt) oder vom Client mitgesendet // Name aus socket.user (Objekt) oder vom Client mitgesendet
const u = socket.user; const u = socket.user;
room.names[slot] = (u && (u.ingame_name || u.username || u.name)) room.names[slot] =
|| playerName (u && (u.ingame_name || u.username || u.name)) || playerName || "Spieler";
|| "Spieler";
socket.join("arena_" + matchId); socket.join("arena_" + matchId);
const otherSlot = slot === "player1" ? "player2" : "player1"; const otherSlot = slot === "player1" ? "player2" : "player1";
if (room.sockets[otherSlot]) { if (room.sockets[otherSlot]) {
console.log(`[1v1] Beide Spieler da → arena_ready senden | Match ${matchId}`); console.log(
`[1v1] Beide Spieler da → arena_ready senden | Match ${matchId}`,
);
io.to("arena_" + matchId).emit("arena_ready", { io.to("arena_" + matchId).emit("arena_ready", {
player1: room.names["player1"] || "Spieler 1", player1: room.names["player1"] || "Spieler 1",
player2: room.names["player2"] || "Spieler 2", player2: room.names["player2"] || "Spieler 2",
}); });
startReadyTimer(io, matchId); startReadyTimer(io, matchId);
} else { } else {
console.log(`[1v1] Erster Spieler joined, warte auf zweiten | Match ${matchId}`); console.log(
socket.to("arena_" + matchId).emit("arena_opponent_joined", { name: room.names[slot], slot }); `[1v1] Erster Spieler joined, warte auf zweiten | Match ${matchId}`,
);
socket
.to("arena_" + matchId)
.emit("arena_opponent_joined", { name: room.names[slot], slot });
} }
}); });
@ -375,8 +438,14 @@ function registerArenaHandlers(io, socket) {
if (!io._arenaReady.has(matchId)) io._arenaReady.set(matchId, new Set()); if (!io._arenaReady.has(matchId)) io._arenaReady.set(matchId, new Set());
const readySet = io._arenaReady.get(matchId); const readySet = io._arenaReady.get(matchId);
readySet.add(slot); readySet.add(slot);
io.to("arena_" + matchId).emit("ready_status", { readyCount: readySet.size, readySlots: Array.from(readySet) }); io.to("arena_" + matchId).emit("ready_status", {
if (readySet.size >= 2) { stopReadyTimer(io, matchId); io._arenaReady.delete(matchId); } readyCount: readySet.size,
readySlots: Array.from(readySet),
});
if (readySet.size >= 2) {
stopReadyTimer(io, matchId);
io._arenaReady.delete(matchId);
}
}); });
socket.on("player_surrender", (data) => { socket.on("player_surrender", (data) => {