461 lines
12 KiB
JavaScript
461 lines
12 KiB
JavaScript
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: 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}`);
|
||
});
|