dok/app.js
2026-03-18 11:36:52 +00:00

495 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}`);
});