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 = ` +
βš”οΈ Match gefunden!
+
${myName}  vs  ${opponentName}
+
+ `; + document.body.appendChild(overlay); + + // Ladebalken animieren + requestAnimationFrame(() => { + const bar = document.getElementById("mfBar"); + if (bar) bar.style.width = "100%"; + }); + + // Nach 1.6s zum Spielfeld + setTimeout(() => { + overlay.remove(); + onDone(); + }, 1600); +} + +/* ── Popup ΓΆffnen ──────────────────────────────────────────────────────────── */ +function openArenaPopup(src, opponentName, matchId) { + // Vorhandenen Popup schließen (falls mehrfach) + document.getElementById("arena-backdrop")?.remove(); + document.getElementById("arena-popup")?.remove(); + const backdrop = document.createElement("div"); backdrop.id = "arena-backdrop"; - // Popup-Rahmen const popup = document.createElement("div"); popup.id = "arena-popup"; - // Titelleiste + const title = opponentName + ? `βš”οΈ 1v1  Β·  vs ${opponentName}` + : "βš”οΈ Arena  Β·  1v1"; + popup.innerHTML = `
@@ -156,9 +367,9 @@ function openArenaPopup(src) {
- βš”οΈ Arena  Β·  1v1 + ${title}
- ${src} + ${matchId || src} `; @@ -166,27 +377,79 @@ function openArenaPopup(src) { document.body.appendChild(backdrop); document.body.appendChild(popup); - /* Schließen: roter Dot oder Backdrop-Klick */ - const close = () => { backdrop.remove(); popup.remove(); }; + /* Schließen */ + const close = () => { + backdrop.remove(); + popup.remove(); + }; document.getElementById("arena-close-btn").addEventListener("click", close); - backdrop.addEventListener("click", close); + backdrop.addEventListener("click", (e) => { + // Nur schließen wenn wirklich auf den Backdrop geklickt (nicht auf Popup) + if (e.target === backdrop) close(); + }); - /* GrΓΌner Dot β†’ Vollbild */ + /* Vollbild */ document.getElementById("arena-fullscreen-btn").addEventListener("click", () => { popup.requestFullscreen?.(); }); } -/* ── Click-Handler auf den Modus-Karten ───────────────────────────────── */ -function initArenaModes() { - document.querySelectorAll(".arena-mode-card").forEach((card) => { - card.addEventListener("click", () => { - const mode = card.dataset.mode; - if (mode === "1v1") { - openArenaPopup("/arena/1v1"); - } else { - console.log("Arena Modus gewΓ€hlt:", mode); - } - }); +/* ── UI Hilfsfunktionen ────────────────────────────────────────────────────── */ +function setCardSearching(card, searching) { + const label = card.querySelector(".arena-mode-label"); + const desc = card.querySelector(".arena-mode-desc"); + + if (searching) { + card.classList.add("searching"); + label.textContent = "⏳ Suche…"; + desc.textContent = "Warte auf passenden Gegner…"; + } else { + card.classList.remove("searching"); + label.textContent = "1v1"; + desc.textContent = "Einzelkampf – Beweis deine StΓ€rke im Duell"; + } +} + +function showQueueStatus(myLevel, poolSize) { + const box = document.getElementById("arena-queue-status"); + if (!box) return; + + const range = 5; + const min = Math.max(1, myLevel - range); + const max = myLevel + range; + const pool = poolSize ? ` Β· ${poolSize} Spieler im Pool` : ""; + + box.style.display = "block"; + box.innerHTML = ` + ⏳ Suche Gegner (Level ${min}–${max})${pool} +
+ Suche abbrechen + `; + + // Cancel-Button – Card-Referenz ΓΌber data-attribute + document.getElementById("qs-cancel-btn")?.addEventListener("click", () => { + const card = document.querySelector(".arena-mode-card[data-mode='1v1']"); + if (card) cancelQueue(card); }); } + +function hideQueueStatus() { + const box = document.getElementById("arena-queue-status"); + if (box) box.style.display = "none"; +} + +function showArenaError(msg) { + const box = document.getElementById("arena-queue-status"); + if (!box) return; + box.style.display = "block"; + box.style.animation = "none"; + box.style.borderColor = "rgba(231,76,60,0.5)"; + box.style.color = "#e74c3c"; + box.textContent = "❌ " + msg; + setTimeout(() => { + box.style.display = "none"; + box.style.animation = ""; + box.style.borderColor = ""; + box.style.color = ""; + }, 3000); +} diff --git a/routes/routes_arena.js b/routes/routes_arena.js index 4507f3b..8726109 100644 --- a/routes/routes_arena.js +++ b/routes/routes_arena.js @@ -1,44 +1,139 @@ const express = require("express"); const router = express.Router(); +const db = require("../database/database"); + +/* ======================== + Login Middleware (lokal) +======================== */ + +function requireLogin(req, res, next) { + if (!req.session.user) { + return res.status(401).json({ error: "Nicht eingeloggt" }); + } + next(); +} /* ================================ Arena Übersicht GET /arena ================================ */ + router.get("/", (req, res) => { res.render("arena", { title: "Kampfarena", }); }); +/* ======================== + GET /arena/me + Eigene Spielerdaten fΓΌr Matchmaking +======================== */ + +router.get("/me", requireLogin, async (req, res) => { + const userId = req.session.user.id; + try { + const [[account]] = await db.query( + "SELECT id, ingame_name, level FROM accounts WHERE id = ?", + [userId], + ); + + if (!account) { + return res.status(404).json({ error: "Spieler nicht gefunden" }); + } + + res.json({ + id: account.id, + name: account.ingame_name || "Unbekannter Held", + level: account.level || 1, + }); + } catch (err) { + console.error("[Arena /me]", err); + res.status(500).json({ error: "Datenbankfehler" }); + } +}); + /* ================================ 1v1 Spielfeld GET /arena/1v1 ================================ */ -router.get("/1v1", (req, res) => { - res.render("1v1_spielfeld", { - title: "1v1 Kampf", - player1: req.session?.character?.name || "Spieler 1", - player2: "Gegner", - player1hp: 20, - player2hp: 20, - player1mana: 3, - player2mana: 3, - }); + +router.get("/1v1", requireLogin, async (req, res) => { + const userId = req.session.user.id; + const { match: matchId, slot } = req.query; + + // Kein matchId β†’ direkte Vorschau ohne Matchmaking (z.B. fΓΌr Tests) + if (!matchId) { + return res.render("1v1_spielfeld", { + title: "1v1 Kampf", + matchId: null, + mySlot: "player1", + player1: req.session?.character?.name || req.session?.user?.ingame_name || "Spieler 1", + player2: "Gegner", + player1hp: 20, + player1mana: 3, + player2hp: 20, + player2mana: 3, + }); + } + + try { + // Eigene Spielerdaten laden + const [[me]] = await db.query( + "SELECT ingame_name, level FROM accounts WHERE id = ?", + [userId], + ); + + // HP + Mana aus Charakter-Tabelle (falls vorhanden, sonst Defaults) + let myHp = 20; + let myMana = 3; + + try { + const [[charStats]] = await db.query( + "SELECT hp, mana FROM characters WHERE account_id = ?", + [userId], + ); + if (charStats) { + myHp = charStats.hp || 20; + myMana = charStats.mana || 3; + } + } catch { + // Tabelle existiert evtl. noch nicht – Defaults verwenden + } + + const isPlayer1 = slot === "player1"; + + res.render("1v1_spielfeld", { + title: "βš”οΈ 1v1 Kampf", + matchId, + mySlot: slot || "player1", + player1: isPlayer1 ? (me?.ingame_name || "Du") : "Gegner", + player2: isPlayer1 ? "Gegner" : (me?.ingame_name || "Du"), + player1hp: isPlayer1 ? myHp : 20, + player1mana: isPlayer1 ? myMana : 3, + player2hp: isPlayer1 ? 20 : myHp, + player2mana: isPlayer1 ? 3 : myMana, + }); + } catch (err) { + console.error("[Arena /1v1]", err); + res.status(500).send("Fehler beim Laden des Spielfelds."); + } }); /* ================================ 2v2 Spielfeld (Platzhalter) GET /arena/2v2 ================================ */ + router.get("/2v2", (req, res) => { res.render("1v1_spielfeld", { title: "2v2 Kampf", + matchId: null, + mySlot: "player1", player1: req.session?.character?.name || "Spieler 1", player2: "Gegner", player1hp: 20, - player2hp: 20, player1mana: 3, + player2hp: 20, player2mana: 3, }); }); @@ -47,14 +142,17 @@ router.get("/2v2", (req, res) => { 4v4 Spielfeld (Platzhalter) GET /arena/4v4 ================================ */ + router.get("/4v4", (req, res) => { res.render("1v1_spielfeld", { title: "4v4 Kampf", + matchId: null, + mySlot: "player1", player1: req.session?.character?.name || "Spieler 1", player2: "Gegner", player1hp: 20, - player2hp: 20, player1mana: 3, + player2hp: 20, player2mana: 3, }); }); diff --git a/views/1v1_spielfeld.ejs b/views/1v1_spielfeld.ejs index 9d070f0..82699bf 100644 --- a/views/1v1_spielfeld.ejs +++ b/views/1v1_spielfeld.ejs @@ -11,9 +11,94 @@ /> + + + + +
+ βš”οΈ 1v1 KAMPFARENA +
+
+ Verbinde… +
+ Match: <%= matchId || "β€”" %> +
+ + +
+
+
Warte auf Gegner…
+

Verbindung wird hergestellt

+
+
@@ -29,7 +114,7 @@
- +
βš”
-
<%= player1 || "Spieler 1" %>
+
<%= player1 || "Spieler 1" %>
❀ - <%= player1hp || 20 %> + <%= player1hp || 20 %>
πŸ’§ - <%= player1mana || 3 %> + <%= player1mana || 3 %>
-
<%= player1hp || 15 %>
+
<%= player1hp || 15 %>
- +
πŸ›‘
-
<%= player2 || "Spieler 2" %>
+
<%= player2 || "Spieler 2" %>
❀ - <%= player2hp || 20 %> + <%= player2hp || 20 %>
πŸ’§ - <%= player2mana || 3 %> + <%= player2mana || 3 %>
-
<%= player2hp || 15 %>
+
<%= player2hp || 15 %>
@@ -120,7 +205,11 @@ + + +