diff --git a/app.js b/app.js index ce48cc2..9d8fc7e 100644 --- a/app.js +++ b/app.js @@ -96,7 +96,6 @@ function requireLogin(req, res, next) { if (!req.session.user) { return res.status(401).json({ error: "Nicht eingeloggt" }); } - next(); } @@ -121,7 +120,6 @@ app.get("/api/building/:id", requireLogin, async (req, res) => { "INSERT INTO user_buildings (user_id,building_id,level,points) VALUES (?,?,1,0)", [userId, buildingId], ); - building = { level: 1, points: 0 }; } else { building = userBuilding[0]; @@ -140,7 +138,7 @@ app.get("/api/building/:id", requireLogin, async (req, res) => { const buildingInfo = info[0] || {}; res.json({ name: buildingInfo.name || "GebΓ€ude", - type: buildingId, // π DAS HINZUFΓGEN + type: buildingId, level: building.level, points: building.points, nextLevelPoints: nextLevel[0]?.required_points || null, @@ -251,14 +249,60 @@ app.use((req, res) => { }); /* ======================== - Chat System + Chat + 1v1 Matchmaking System ======================== */ let onlineUsers = {}; -io.on("connection", (socket) => { - console.log("Spieler verbunden"); +// ββ 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 = ?", @@ -268,21 +312,80 @@ io.on("connection", (socket) => { if (!rows.length) return; const ingameName = rows[0].ingame_name; - socket.user = ingameName; - onlineUsers[ingameName] = socket.id; - io.emit("onlineUsers", Object.keys(onlineUsers)); }); - socket.on("disconnect", () => { - if (socket.user) { - delete onlineUsers[socket.user]; - 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: 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", { @@ -305,10 +408,7 @@ io.on("connection", (socket) => { const targetSocket = onlineUsers[data.to]; if (!targetSocket) { - socket.emit("systemMessage", { - message: data.to + " ist offline", - }); - + socket.emit("systemMessage", { message: data.to + " ist offline" }); return; } @@ -327,7 +427,6 @@ io.on("connection", (socket) => { socket.on("privateMessage", (data) => { const target = onlineUsers[data.to]; - if (target) { io.to(target).emit("chatMessage", { user: socket.user, @@ -336,6 +435,20 @@ io.on("connection", (socket) => { }); } }); + + /* ββ 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.`); + } + }); }); /* ======================== diff --git a/public/js/buildings/arena.js b/public/js/buildings/arena.js index 2926a16..7f3f6ae 100644 --- a/public/js/buildings/arena.js +++ b/public/js/buildings/arena.js @@ -29,15 +29,18 @@ export async function loadArena() { + +
+ `; - injectArenaPopupStyles(); + injectArenaStyles(); initArenaModes(); } -/* ββ Styles einmalig ins injizieren βββββββββββββββββββββββββββββββ */ -function injectArenaPopupStyles() { +/* ββ Styles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ +function injectArenaStyles() { if (document.getElementById("arena-popup-styles")) return; const style = document.createElement("style"); @@ -51,7 +54,39 @@ function injectArenaPopupStyles() { from { transform: scale(0.94); opacity: 0; } to { transform: scale(1); opacity: 1; } } + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + /* ββ Queue Status Box ββ */ + #arena-queue-status { + margin-top: 18px; + padding: 12px 20px; + border-radius: 10px; + background: rgba(255, 215, 80, 0.08); + border: 1px solid rgba(255, 215, 80, 0.3); + color: rgba(255, 215, 80, 0.9); + font-family: "Cinzel", serif; + font-size: 13px; + letter-spacing: 1px; + text-align: center; + animation: pulse 2s ease-in-out infinite; + } + #arena-queue-status .qs-cancel { + display: inline-block; + margin-top: 8px; + font-size: 11px; + color: rgba(255,100,100,0.8); + cursor: pointer; + text-decoration: underline; + animation: none; + } + #arena-queue-status .qs-cancel:hover { + color: #e74c3c; + } + + /* ββ Backdrop ββ */ #arena-backdrop { position: fixed; inset: 0; @@ -61,6 +96,7 @@ function injectArenaPopupStyles() { animation: arenaFadeIn 0.25s ease; } + /* ββ Popup ββ */ #arena-popup { position: fixed; inset: 50px; @@ -93,7 +129,6 @@ function injectArenaPopupStyles() { align-items: center; gap: 10px; } - /* macOS-style traffic lights */ #arena-popup-titlebar .ap-dots { display: flex; gap: 7px; @@ -105,13 +140,10 @@ function injectArenaPopupStyles() { cursor: pointer; transition: filter 0.15s; } - #arena-popup-titlebar .ap-dot:hover { - filter: brightness(1.3); - } + #arena-popup-titlebar .ap-dot:hover { filter: brightness(1.3); } #arena-popup-titlebar .ap-dot.close { background: #e74c3c; border: 1px solid rgba(0,0,0,0.25); } #arena-popup-titlebar .ap-dot.min { background: #f1c40f; border: 1px solid rgba(0,0,0,0.25); } #arena-popup-titlebar .ap-dot.expand { background: #2ecc71; border: 1px solid rgba(0,0,0,0.25); } - #arena-popup-titlebar .ap-title { font-family: "Cinzel", serif; font-size: 13px; @@ -126,6 +158,48 @@ function injectArenaPopupStyles() { letter-spacing: 1px; } + /* ββ Match-Found Overlay ββ */ + #match-found-overlay { + position: fixed; + inset: 0; + z-index: 10000; + background: rgba(0,0,0,0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + animation: arenaFadeIn 0.3s ease; + } + #match-found-overlay .mfo-title { + font-family: "Cinzel", serif; + font-size: 36px; + color: #ffd750; + text-shadow: 0 0 30px rgba(255,215,80,0.6); + letter-spacing: 6px; + margin-bottom: 12px; + } + #match-found-overlay .mfo-vs { + font-family: "Cinzel", serif; + font-size: 18px; + color: rgba(255,255,255,0.75); + letter-spacing: 3px; + } + #match-found-overlay .mfo-bar { + width: 300px; + height: 4px; + background: rgba(255,215,80,0.2); + border-radius: 2px; + margin-top: 24px; + overflow: hidden; + } + #match-found-overlay .mfo-bar-fill { + height: 100%; + background: #ffd750; + width: 0%; + border-radius: 2px; + transition: width 1.5s ease; + } + /* ββ iframe ββ */ #arena-popup iframe { flex: 1; @@ -133,21 +207,158 @@ function injectArenaPopupStyles() { width: 100%; display: block; } + + /* ββ Card: gesucht-Zustand ββ */ + .arena-mode-card.searching { + opacity: 0.6; + pointer-events: none; + border-color: rgba(255,215,80,0.5) !important; + } `; document.head.appendChild(style); } -/* ββ Popup ΓΆffnen ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ -function openArenaPopup(src) { - // Backdrop +/* ββ Socket-Referenz holen βββββββββββββββββββββββββββββββββββββββββββββββββββ */ +function getSocket() { + // Wird von der Haupt-App als window._socket bereitgestellt + return window._socket || null; +} + +/* ββ Klick-Handler initialisieren βββββββββββββββββββββββββββββββββββββββββββ */ +function initArenaModes() { + document.querySelectorAll(".arena-mode-card").forEach((card) => { + card.addEventListener("click", () => { + const mode = card.dataset.mode; + + if (mode === "1v1") { + handle1v1Click(card); + } else { + console.log("Arena Modus gewΓ€hlt:", mode); + // Platzhalter fΓΌr 2v2 / 4v4 + } + }); + }); +} + +/* ββ 1v1: Hauptlogik βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ +async function handle1v1Click(card) { + const socket = getSocket(); + if (!socket) { + showArenaError("Keine Verbindung zum Server. Bitte Seite neu laden."); + return; + } + + // Bereits in Suche? + if (card.classList.contains("searching")) return; + + // Spielerdaten laden + let me; + try { + const res = await fetch("/arena/me"); + if (!res.ok) throw new Error("Status " + res.status); + me = await res.json(); + } catch (err) { + console.error("[1v1] Spielerdaten konnten nicht geladen werden:", err); + showArenaError("Spielerdaten konnten nicht geladen werden."); + return; + } + + // UI: Suche lΓ€uft + setCardSearching(card, true); + showQueueStatus(me.level); + + // Sicherstellen, dass keine alten Listener hΓ€ngen + socket.off("match_found"); + socket.off("queue_status"); + + // Queue-Status empfangen + socket.on("queue_status", (data) => { + if (data.status === "waiting") { + showQueueStatus(me.level, data.poolSize); + } else if (data.status === "left") { + setCardSearching(card, false); + hideQueueStatus(); + } + }); + + // Match gefunden + socket.once("match_found", (data) => { + socket.off("queue_status"); + setCardSearching(card, false); + hideQueueStatus(); + + showMatchFoundOverlay(me.name, data.opponent.name, () => { + openArenaPopup( + `/arena/1v1?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`, + data.opponent.name, + data.matchId, + ); + }); + }); + + // Matchmaking starten + socket.emit("join_1v1", { + id: me.id, + name: me.name, + level: me.level, + }); +} + +/* ββ Queue abbrechen βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ +function cancelQueue(card) { + const socket = getSocket(); + if (socket) { + socket.emit("leave_1v1"); + socket.off("match_found"); + socket.off("queue_status"); + } + setCardSearching(card, false); + hideQueueStatus(); +} + +/* ββ Match-Found Splash ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ +function showMatchFoundOverlay(myName, opponentName, onDone) { + // Verhindert doppeltes Γffnen + if (document.getElementById("match-found-overlay")) return; + + const overlay = document.createElement("div"); + overlay.id = "match-found-overlay"; + overlay.innerHTML = ` +Verbindung wird hergestellt
+