dok/sockets/arena_socket.js
2026-04-07 15:32:44 +01:00

573 lines
17 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, Spielfeld & Bereit-System
============================================================ */
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)
function generateId() {
return `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
}
/* ── 2v2 Lobby-Liste an alle schicken ── */
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);
}
/* ── 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);
}
}
/* ── 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);
io.to(p.socketId).emit("match_found_2v2", {
matchId,
myTeam: p.team,
teammates,
opponents,
mySlot: `team${p.team}_player${team1.players.indexOf(p) >= 0 ? team1.players.findIndex(x=>x.socketId===p.socketId)+1 : team2.players.findIndex(x=>x.socketId===p.socketId)+1}`,
});
}
console.log(`[2v2] Match: Team1(${team1.players.map(p=>p.player.name)}) vs Team2(${team2.players.map(p=>p.player.name)}) | ${matchId}`);
}
// Werden beim ersten Event lazy initialisiert (auf io gespeichert)
// io._arenaRooms → matchId → { sockets, names }
// io._arenaReady → matchId → Set of ready slots
// io._arenaTimers → matchId → intervalId
const READY_TIMEOUT = 30; // Sekunden bis Match abgebrochen wird
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).`);
}
}
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;
}
}
}
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.`);
}
});
/* ══════════════════════════════════════════
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) => {
// Erst aus alten Teams entfernen
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); // Team aus offener Liste entfernen
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`);
// Beide bereit → Team in Matchmaking-Pool
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);
}
});
/* ── Spielfeld: Spieler betritt Arena-Room ── */
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,
});
}
});
/* ── 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);
}
});
/* ── 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 });
});
/* ── Disconnect ── */
socket.on("disconnect", () => {
if (waitingPool.delete(socket.id)) {
console.log(`[1v1] ${socket.id} disconnected aus 1v1-Pool entfernt.`);
}
leaveAllTeams(socket.id, io);
});
}
/* ── Hilfsfunktion: 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 {
// Verbleibenden Spieler informieren
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;
}
}
module.exports = { registerArenaHandlers };
function startReadyTimer(io, matchId) {
if (!io._arenaTimers) io._arenaTimers = new Map();
if (io._arenaTimers.has(matchId)) return; // läuft bereits
let remaining = READY_TIMEOUT;
// Sofort ersten Tick senden
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);
// Match abbrechen Funktion noch offen
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).`);
}
}
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;
}
}
}
function registerArenaHandlers(io, socket) {
/* ── 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);
});
/* ── 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.`);
}
});
/* ── Spielfeld: Spieler betritt Arena-Room ── */
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"]}`);
// 30-Sekunden Bereit-Timer starten
startReadyTimer(io, matchId);
} else {
socket.to("arena_" + matchId).emit("arena_opponent_joined", {
name: room.names[slot],
slot,
});
}
});
/* ── 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);
}
});
/* ── Aufgeben ── */
socket.on("player_surrender", (data) => {
const { matchId, slot } = data;
console.log(`[1v1] ${slot} hat aufgegeben in Match ${matchId}`);
// Aufgabe-Logik kommt hier rein
io.to("arena_" + matchId).emit("player_surrendered", { slot });
});
/* ── Disconnect: aus Pool entfernen ── */
socket.on("disconnect", () => {
if (waitingPool.delete(socket.id)) {
console.log(`[1v1] ${socket.id} disconnected aus Pool entfernt.`);
}
});
}
module.exports = { registerArenaHandlers };