dok/sockets/arena_socket.js
2026-04-08 17:02:16 +01:00

403 lines
14 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
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<socketId> }
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 };