From 67a02cc646f2d48c221264e1025b53c2f0173bf5 Mon Sep 17 00:00:00 2001 From: Cay Date: Wed, 18 Mar 2026 11:43:30 +0000 Subject: [PATCH] hzluz --- app.js | 235 +--------------------------------------- public/css/1v1.css | 74 +++++++++++++ sockets/arena_socket.js | 199 ++++++++++++++++++++++++++++++++++ sockets/chat_socket.js | 89 +++++++++++++++ views/1v1_spielfeld.ejs | 59 +++++++++- 5 files changed, 424 insertions(+), 232 deletions(-) create mode 100644 sockets/arena_socket.js create mode 100644 sockets/chat_socket.js diff --git a/app.js b/app.js index a419abf..77105bd 100644 --- a/app.js +++ b/app.js @@ -23,6 +23,8 @@ const equipment = require("./routes/equipment"); const blackmarket = require("./routes/blackmarket"); const mineRoute = require("./routes/mine_route"); const arenaRoutes = require("./routes/routes_arena"); +const { registerArenaHandlers } = require("./sockets/arena"); +const { registerChatHandlers } = require("./sockets/chat"); const compression = require("compression"); @@ -249,240 +251,13 @@ app.use((req, res) => { }); /* ======================== - Chat + 1v1 Matchmaking System + Socket.io Handler ======================== */ -let onlineUsers = {}; - -// ── 1v1 Matchmaking Pool ───────────────────────────────────────────────────── -// Map: socketId → { socket, player: { id, name, level } } -const waitingPool = new Map(); -const LEVEL_RANGE = 5; - -function tryMatchmaking(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) { - // Match gefunden – beide aus dem Pool entfernen - waitingPool.delete(newSocketId); - waitingPool.delete(id); - - const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; - - // Spieler 1 benachrichtigen - challenger.socket.emit("match_found", { - matchId, - opponent: entry.player, - mySlot: "player1", - }); - - // Spieler 2 benachrichtigen - entry.socket.emit("match_found", { - matchId, - opponent: challenger.player, - mySlot: "player2", - }); - - console.log( - `[1v1] Match gestartet: ${challenger.player.name} (Lvl ${challenger.player.level}) ` + - `vs ${entry.player.name} (Lvl ${entry.player.level}) | ID: ${matchId}` - ); - - return; // Nur ein Match pro Aufruf – nächste Iteration startet neu - } - } -} -// ───────────────────────────────────────────────────────────────────────────── - io.on("connection", (socket) => { console.log("Spieler verbunden:", socket.id); - - /* ── Chat: Registrierung ── */ - socket.on("register", async (username) => { - const [rows] = await db.query( - "SELECT ingame_name FROM accounts WHERE username = ?", - [username], - ); - - if (!rows.length) return; - - const ingameName = rows[0].ingame_name; - socket.user = ingameName; - onlineUsers[ingameName] = socket.id; - io.emit("onlineUsers", Object.keys(onlineUsers)); - }); - - /* ── 1v1: Queue beitreten ── */ - socket.on("join_1v1", (playerData) => { - // Doppelt-Eintrag verhindern - 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}) betritt den Pool. Poolgröße: ${waitingPool.size}`); - - tryMatchmaking(socket.id); - }); - - /* ── 1v1: Queue verlassen ── */ - socket.on("leave_1v1", () => { - if (waitingPool.delete(socket.id)) { - socket.emit("queue_status", { status: "left" }); - console.log(`[1v1] Spieler ${socket.id} hat den Pool verlassen.`); - } - }); - - /* ── 1v1: Bereit-System ── */ - if (!io._arenaReady) io._arenaReady = new Map(); // matchId → Set of ready slots - - socket.on("player_ready", (data) => { - const { matchId, slot } = data; - if (!matchId || !slot) return; - - if (!io._arenaReady.has(matchId)) { - io._arenaReady.set(matchId, new Set()); - } - - const readySet = io._arenaReady.get(matchId); - readySet.add(slot); - - // Beide Spieler in der Arena-Room benachrichtigen - io.to("arena_" + matchId).emit("ready_status", { - readyCount: readySet.size, - }); - - console.log(`[1v1] ${slot} ist bereit in Match ${matchId} (${readySet.size}/2)`); - - // Aufräumen wenn beide bereit - if (readySet.size >= 2) { - io._arenaReady.delete(matchId); - } - }); - - 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 }); - }); - - /* ── 1v1: Spielfeld-Verbindung (beide Spieler im iframe) ── */ - // Map: matchId → { player1: socketId, player2: socketId, names: {} } - if (!io._arenaRooms) io._arenaRooms = new Map(); - - socket.on("arena_join", (data) => { - const { matchId, slot } = data; - if (!matchId || !slot) return; - - 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]) { - // Beide sind da → arena_ready an alle im Raum senden - 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"]}`); - } else { - // Erster Spieler → dem Gegner mitteilen sobald er kommt - socket.to("arena_" + matchId).emit("arena_opponent_joined", { - name: room.names[slot], - slot, - }); - } - }); - - /* ── Chat: Nachrichten ── */ - socket.on("chatMessage", (data) => { - if (data.channel === "global") { - io.emit("chatMessage", { - user: socket.user, - message: data.message, - channel: "global", - }); - } - - if (data.channel === "guild") { - io.to("guild_" + data.guild).emit("chatMessage", { - user: socket.user, - message: data.message, - channel: "guild", - }); - } - }); - - socket.on("whisper", (data) => { - const targetSocket = onlineUsers[data.to]; - - if (!targetSocket) { - socket.emit("systemMessage", { message: data.to + " ist offline" }); - return; - } - - io.to(targetSocket).emit("chatMessage", { - user: socket.user, - message: data.message, - channel: "private", - }); - - socket.emit("chatMessage", { - user: "(an " + data.to + ")", - message: data.message, - channel: "private", - }); - }); - - socket.on("privateMessage", (data) => { - const target = onlineUsers[data.to]; - if (target) { - io.to(target).emit("chatMessage", { - user: socket.user, - message: data.message, - channel: "private", - }); - } - }); - - /* ── Disconnect ── */ - socket.on("disconnect", () => { - // Aus Chat entfernen - if (socket.user) { - delete onlineUsers[socket.user]; - io.emit("onlineUsers", Object.keys(onlineUsers)); - } - - // Aus 1v1 Pool entfernen - if (waitingPool.delete(socket.id)) { - console.log(`[1v1] Spieler ${socket.id} disconnected – aus Pool entfernt.`); - } - }); + registerChatHandlers(io, socket); + registerArenaHandlers(io, socket); }); /* ======================== diff --git a/public/css/1v1.css b/public/css/1v1.css index ba5355f..4b36e75 100644 --- a/public/css/1v1.css +++ b/public/css/1v1.css @@ -593,3 +593,77 @@ body { backdrop-filter: blur(2px); cursor: not-allowed; } + +/* ── Bereit-Timer Box (zentriert im Lock-Overlay) ── */ +#board-lock-overlay { + display: flex; + align-items: center; + justify-content: center; +} +#ready-timer-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + background: rgba(10, 8, 5, 0.92); + border: 1px solid rgba(255, 215, 80, 0.35); + border-radius: 16px; + padding: 32px 40px; + box-shadow: 0 8px 40px rgba(0,0,0,0.8); + min-width: 260px; +} +#ready-timer-label { + font-family: "Cinzel", serif; + font-size: 18px; + letter-spacing: 4px; + color: rgba(255, 215, 80, 0.9); + text-transform: uppercase; +} +#ready-timer-ring { + position: relative; + width: 80px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; +} +.timer-track { + fill: none; + stroke: rgba(255,255,255,0.1); + stroke-width: 6; +} +.timer-fill { + fill: none; + stroke: #27ae60; + stroke-width: 6; + stroke-linecap: round; + transform: rotate(-90deg); + transform-origin: center; + transition: stroke-dashoffset 0.9s linear, stroke 0.3s ease; +} +#ready-timer-number { + position: absolute; + font-family: "Cinzel", serif; + font-size: 26px; + font-weight: 700; + color: #fff; + text-shadow: 0 0 10px rgba(0,0,0,0.8); +} +#ready-timer-sub { + font-family: "Cinzel", serif; + font-size: 11px; + color: rgba(255,255,255,0.4); + letter-spacing: 1px; + text-align: center; +} +#ready-status-row { + display: flex; + gap: 20px; +} +.ready-pip { + font-family: "Cinzel", serif; + font-size: 12px; + color: rgba(255,255,255,0.5); + letter-spacing: 1px; + transition: color 0.3s; +} diff --git a/sockets/arena_socket.js b/sockets/arena_socket.js new file mode 100644 index 0000000..2f70808 --- /dev/null +++ b/sockets/arena_socket.js @@ -0,0 +1,199 @@ +/* ============================================================ + sockets/arena.js + Alle Socket-Events rund um 1v1 Matchmaking, Spielfeld & Bereit-System +============================================================ */ + +const waitingPool = new Map(); // socketId → { socket, player } +const LEVEL_RANGE = 5; + +// 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; // 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 }; diff --git a/sockets/chat_socket.js b/sockets/chat_socket.js new file mode 100644 index 0000000..79d1ae7 --- /dev/null +++ b/sockets/chat_socket.js @@ -0,0 +1,89 @@ +/* ============================================================ + sockets/chat.js + Alle Socket-Events rund um Chat, Whisper & Online-Status +============================================================ */ + +const db = require("../database/database"); + +const onlineUsers = {}; // ingameName → socketId + +function registerChatHandlers(io, socket) { + + /* ── Registrierung ── */ + socket.on("register", async (username) => { + const [rows] = await db.query( + "SELECT ingame_name FROM accounts WHERE username = ?", + [username], + ); + + if (!rows.length) return; + + const ingameName = rows[0].ingame_name; + socket.user = ingameName; + onlineUsers[ingameName] = socket.id; + io.emit("onlineUsers", Object.keys(onlineUsers)); + }); + + /* ── Chat-Nachrichten ── */ + socket.on("chatMessage", (data) => { + if (data.channel === "global") { + io.emit("chatMessage", { + user: socket.user, + message: data.message, + channel: "global", + }); + } + + if (data.channel === "guild") { + io.to("guild_" + data.guild).emit("chatMessage", { + user: socket.user, + message: data.message, + channel: "guild", + }); + } + }); + + /* ── Flüstern ── */ + socket.on("whisper", (data) => { + const targetSocket = onlineUsers[data.to]; + + if (!targetSocket) { + socket.emit("systemMessage", { message: data.to + " ist offline" }); + return; + } + + io.to(targetSocket).emit("chatMessage", { + user: socket.user, + message: data.message, + channel: "private", + }); + + socket.emit("chatMessage", { + user: "(an " + data.to + ")", + message: data.message, + channel: "private", + }); + }); + + /* ── Private Nachricht ── */ + socket.on("privateMessage", (data) => { + const target = onlineUsers[data.to]; + if (target) { + io.to(target).emit("chatMessage", { + user: socket.user, + message: data.message, + channel: "private", + }); + } + }); + + /* ── Disconnect: aus Online-Liste entfernen ── */ + socket.on("disconnect", () => { + if (socket.user) { + delete onlineUsers[socket.user]; + io.emit("onlineUsers", Object.keys(onlineUsers)); + } + }); +} + +module.exports = { registerChatHandlers }; diff --git a/views/1v1_spielfeld.ejs b/views/1v1_spielfeld.ejs index fcac459..93c4a88 100644 --- a/views/1v1_spielfeld.ejs +++ b/views/1v1_spielfeld.ejs @@ -42,7 +42,20 @@ -
+
+
+
Bereit machen
+
+ +
30
+
+
Beide Spieler müssen BEREIT klicken
+
+
⬜ Spieler 1
+
⬜ Spieler 2
+
+
+
@@ -240,8 +253,37 @@ socket.emit("player_ready", { matchId, slot: mySlot }); } + // Timer-Kreis: Umfang des Kreises (r=34 → 2*π*34 ≈ 213.6) + const CIRCUMFERENCE = 2 * Math.PI * 34; + const timerCircle = document.getElementById("timer-circle"); + if (timerCircle) timerCircle.style.strokeDasharray = CIRCUMFERENCE; + + socket.on("ready_timer", (data) => { + const { remaining } = data; + const num = document.getElementById("ready-timer-number"); + if (num) num.textContent = remaining; + + // Kreis-Fortschritt aktualisieren + if (timerCircle) { + const progress = remaining / 30; + const offset = CIRCUMFERENCE * (1 - progress); + timerCircle.style.strokeDashoffset = offset; + // Farbe: grün → gelb → rot + if (remaining > 15) timerCircle.style.stroke = "#27ae60"; + else if (remaining > 7) timerCircle.style.stroke = "#f39c12"; + else timerCircle.style.stroke = "#e74c3c"; + } + }); + socket.on("ready_status", (data) => { - // data.readyCount = wie viele Spieler bereits bereit sind + // Pips aktualisieren + const pip1 = document.getElementById("pip-player1"); + const pip2 = document.getElementById("pip-player2"); + if (data.readySlots && pip1 && pip2) { + if (data.readySlots.includes("player1")) pip1.textContent = "✅ " + (document.getElementById("nameLeft")?.textContent || "Spieler 1"); + if (data.readySlots.includes("player2")) pip2.textContent = "✅ " + (document.getElementById("nameRight")?.textContent || "Spieler 2"); + } + if (data.readyCount === 2) { // Beide bereit → Sperre aufheben const lock = document.getElementById("board-lock-overlay"); @@ -255,6 +297,19 @@ } }); + socket.on("match_cancelled", (data) => { + const lock = document.getElementById("board-lock-overlay"); + if (lock) { + lock.innerHTML = ` +
+
⏰ Zeit abgelaufen
+
${data.message || "Match abgebrochen."}
+
+ `; + } + // Aufgabe-Logik kommt hier rein + }); + function handleAufgeben() { // Funktion offen – hier kann später die Aufgabe-Logik rein socket.emit("player_surrender", { matchId, slot: mySlot });