dok/sockets/arena_socket.js
2026-04-09 08:53:51 +01:00

354 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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 };