354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
/* ============================================================
|
||
sockets/arena.js
|
||
1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby
|
||
============================================================ */
|
||
|
||
const waitingPool = new Map(); // socketId → { socket, player }
|
||
const LEVEL_RANGE = 5;
|
||
const READY_TIMEOUT = 30;
|
||
|
||
/* ── 2v2 State ── */
|
||
const teams2v2 = new Map(); // teamId → { id, players, ready }
|
||
const readyTeams2v2 = new Map();
|
||
|
||
/* ── 4v4 State ── */
|
||
const teams4v4 = new Map(); // teamId → { id, players, ready }
|
||
const readyTeams4v4 = new Map();
|
||
|
||
function generateId() {
|
||
return `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
HELPER: Generisch für 2v2 UND 4v4
|
||
mode: "2v2" | "4v4"
|
||
maxPlayers: 2 | 4
|
||
═══════════════════════════════════════════════════════════ */
|
||
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 teams = getTeamMap(mode);
|
||
const team = teams.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,
|
||
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 {
|
||
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 (DRY)
|
||
═══════════════════════════════════════════════════════════ */
|
||
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,
|
||
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);
|
||
});
|
||
|
||
/* 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 } = 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",
|
||
});
|
||
startReadyTimer(io, matchId);
|
||
} else {
|
||
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 });
|
||
});
|
||
|
||
/* ── 2v2 & 4v4 (generisch) ── */
|
||
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 };
|