require("dotenv").config(); const express = require("express"); const path = require("path"); const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); const http = require("http"); const { Server } = require("socket.io"); const db = require("./database/database"); const serverRoutes = require("./routes/servers"); const registerRoutes = require("./routes/register"); const verifyRoutes = require("./routes/verify"); const characterRoutes = require("./routes/character"); const session = require("express-session"); const loginRoutes = require("./routes/login"); const launcherRoutes = require("./routes/launcher"); const buildingRoutes = require("./routes/buildings"); const inventory = require("./routes/inventory"); const avatar = require("./routes/avatar"); const equip = require("./routes/equip"); const equipment = require("./routes/equipment"); const blackmarket = require("./routes/blackmarket"); const mineRoute = require("./routes/mine_route"); const arenaRoutes = require("./routes/routes_arena"); const compression = require("compression"); const app = express(); app.set("trust proxy", 1); const PORT = process.env.PORT || 3000; /* ======================== Chatserver ======================== */ const server = http.createServer(app); const io = new Server(server); /* ======================== Compression ======================== */ app.use(compression()); /* ======================== Security Middleware ======================== */ app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:"], connectSrc: ["'self'", "ws:", "wss:"], }, }, }), ); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5000, }); app.use(limiter); app.use( session({ secret: process.env.SESSION_SECRET || "dynastyofknights_secret", resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: 1000 * 60 * 60 * 24, }, }), ); /* ======================== Express Settings ======================== */ app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); /* ======================== Login Middleware ======================== */ function requireLogin(req, res, next) { if (!req.session.user) { return res.status(401).json({ error: "Nicht eingeloggt" }); } next(); } /* ======================== Route für Ajax für Gebäude ======================== */ app.get("/api/building/:id", requireLogin, async (req, res) => { const buildingId = req.params.id; const userId = req.session.user.id; try { const [userBuilding] = await db.query( "SELECT level, points FROM user_buildings WHERE user_id=? AND building_id=?", [userId, buildingId], ); let building; if (!userBuilding.length) { await db.query( "INSERT INTO user_buildings (user_id,building_id,level,points) VALUES (?,?,1,0)", [userId, buildingId], ); building = { level: 1, points: 0 }; } else { building = userBuilding[0]; } const [nextLevel] = await db.query( "SELECT required_points, wood, stone, gold FROM building_levels WHERE building_id=? AND level=?", [buildingId, building.level + 1], ); const [info] = await db.query( "SELECT name,description,history FROM buildings WHERE id=?", [buildingId], ); const buildingInfo = info[0] || {}; res.json({ name: buildingInfo.name || "Gebäude", type: buildingId, level: building.level, points: building.points, nextLevelPoints: nextLevel[0]?.required_points || null, description: info[0].description, history: info[0].history, upgradeCost: `${nextLevel[0]?.wood} Holz, ${nextLevel[0]?.stone} Stein, ${nextLevel[0]?.gold} Gold`, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ======================== HUD API ======================== */ app.get("/api/hud", requireLogin, async (req, res) => { const userId = req.session.user.id; try { const [[account]] = await db.query( "SELECT ingame_name FROM accounts WHERE id = ?", [userId], ); const [[currency]] = await db.query( "SELECT silver, gold, gems, wood, stone FROM account_currency WHERE account_id = ?", [userId], ); res.json({ name: account?.ingame_name || "Held", silver: currency?.silver || 0, gold: currency?.gold || 0, gems: currency?.gems || 0, wood: currency?.wood || 0, stone: currency?.stone || 0, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); app.get("/api/buildings", requireLogin, async (req, res) => { const userId = req.session.user.id; try { const [rows] = await db.query( ` SELECT b.id, b.name, b.description, b.history, ub.level, ub.points FROM buildings b LEFT JOIN user_buildings ub ON ub.building_id = b.id AND ub.user_id = ? `, [userId], ); res.json(rows); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ======================== Body Parser ======================== */ app.use(express.json()); app.use(express.urlencoded({ extended: true })); /* ======================== Static Files ======================== */ app.use(express.static(path.join(__dirname, "public"))); /* ======================== Routes ======================== */ app.use("/", serverRoutes); app.use("/register", registerRoutes); app.use("/verify", verifyRoutes); app.use("/create-character", characterRoutes); app.use("/login", loginRoutes); app.use("/launcher", launcherRoutes); app.use("/", buildingRoutes); app.use("/api/inventory", inventory); app.use("/api/avatar", avatar); app.use("/api/equip", equip); app.use("/api/equipment", equipment); app.use("/api/blackmarket", blackmarket); app.use("/api/mine", mineRoute); app.use("/arena", arenaRoutes); /* ======================== 404 Handler ======================== */ app.use((req, res) => { res.status(404).send("Seite nicht gefunden"); }); /* ======================== Chat + 1v1 Matchmaking System ======================== */ 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.`); } }); }); /* ======================== Server Start ======================== */ server.listen(PORT, () => { console.log(`Dynasty of Knights Server läuft auf http://localhost:${PORT}`); });