This commit is contained in:
Cay 2026-03-18 11:43:30 +00:00
parent 9dde216cbf
commit 67a02cc646
5 changed files with 424 additions and 232 deletions

235
app.js
View File

@ -23,6 +23,8 @@ const equipment = require("./routes/equipment");
const blackmarket = require("./routes/blackmarket");
const mineRoute = require("./routes/mine_route");
const arenaRoutes = require("./routes/routes_arena");
const { registerArenaHandlers } = require("./sockets/arena");
const { registerChatHandlers } = require("./sockets/chat");
const compression = require("compression");
@ -249,240 +251,13 @@ app.use((req, res) => {
});
/* ========================
Chat + 1v1 Matchmaking System
Socket.io Handler
======================== */
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.`);
}
});
registerChatHandlers(io, socket);
registerArenaHandlers(io, socket);
});
/* ========================

View File

@ -593,3 +593,77 @@ body {
backdrop-filter: blur(2px);
cursor: not-allowed;
}
/* ── Bereit-Timer Box (zentriert im Lock-Overlay) ── */
#board-lock-overlay {
display: flex;
align-items: center;
justify-content: center;
}
#ready-timer-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
background: rgba(10, 8, 5, 0.92);
border: 1px solid rgba(255, 215, 80, 0.35);
border-radius: 16px;
padding: 32px 40px;
box-shadow: 0 8px 40px rgba(0,0,0,0.8);
min-width: 260px;
}
#ready-timer-label {
font-family: "Cinzel", serif;
font-size: 18px;
letter-spacing: 4px;
color: rgba(255, 215, 80, 0.9);
text-transform: uppercase;
}
#ready-timer-ring {
position: relative;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.timer-track {
fill: none;
stroke: rgba(255,255,255,0.1);
stroke-width: 6;
}
.timer-fill {
fill: none;
stroke: #27ae60;
stroke-width: 6;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 0.9s linear, stroke 0.3s ease;
}
#ready-timer-number {
position: absolute;
font-family: "Cinzel", serif;
font-size: 26px;
font-weight: 700;
color: #fff;
text-shadow: 0 0 10px rgba(0,0,0,0.8);
}
#ready-timer-sub {
font-family: "Cinzel", serif;
font-size: 11px;
color: rgba(255,255,255,0.4);
letter-spacing: 1px;
text-align: center;
}
#ready-status-row {
display: flex;
gap: 20px;
}
.ready-pip {
font-family: "Cinzel", serif;
font-size: 12px;
color: rgba(255,255,255,0.5);
letter-spacing: 1px;
transition: color 0.3s;
}

199
sockets/arena_socket.js Normal file
View File

@ -0,0 +1,199 @@
/* ============================================================
sockets/arena.js
Alle Socket-Events rund um 1v1 Matchmaking, Spielfeld & Bereit-System
============================================================ */
const waitingPool = new Map(); // socketId → { socket, player }
const LEVEL_RANGE = 5;
// Werden beim ersten Event lazy initialisiert (auf io gespeichert)
// io._arenaRooms → matchId → { sockets, names }
// io._arenaReady → matchId → Set of ready slots
// io._arenaTimers → matchId → intervalId
const READY_TIMEOUT = 30; // Sekunden bis Match abgebrochen wird
function startReadyTimer(io, matchId) {
if (!io._arenaTimers) io._arenaTimers = new Map();
if (io._arenaTimers.has(matchId)) return; // läuft bereits
let remaining = READY_TIMEOUT;
// Sofort ersten Tick senden
io.to("arena_" + matchId).emit("ready_timer", { remaining });
const interval = setInterval(() => {
remaining--;
io.to("arena_" + matchId).emit("ready_timer", { remaining });
if (remaining <= 0) {
clearInterval(interval);
io._arenaTimers.delete(matchId);
// Match abbrechen Funktion noch offen
console.log(`[1v1] Match ${matchId} abgebrochen Zeit abgelaufen.`);
io.to("arena_" + matchId).emit("match_cancelled", {
reason: "timeout",
message: "Zeit abgelaufen Match wird abgebrochen.",
});
}
}, 1000);
io._arenaTimers.set(matchId, interval);
}
function stopReadyTimer(io, matchId) {
if (!io._arenaTimers) return;
const interval = io._arenaTimers.get(matchId);
if (interval) {
clearInterval(interval);
io._arenaTimers.delete(matchId);
console.log(`[1v1] Timer für Match ${matchId} gestoppt (beide bereit).`);
}
}
function tryMatchmaking(io, 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) {
waitingPool.delete(newSocketId);
waitingPool.delete(id);
const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
challenger.socket.emit("match_found", {
matchId,
opponent: entry.player,
mySlot: "player1",
});
entry.socket.emit("match_found", {
matchId,
opponent: challenger.player,
mySlot: "player2",
});
console.log(
`[1v1] Match: ${challenger.player.name} (Lvl ${challenger.player.level})` +
` vs ${entry.player.name} (Lvl ${entry.player.level}) | ${matchId}`
);
return;
}
}
}
function registerArenaHandlers(io, socket) {
/* ── Queue beitreten ── */
socket.on("join_1v1", (playerData) => {
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}) im Pool. Größe: ${waitingPool.size}`);
tryMatchmaking(io, socket.id);
});
/* ── Queue verlassen ── */
socket.on("leave_1v1", () => {
if (waitingPool.delete(socket.id)) {
socket.emit("queue_status", { status: "left" });
console.log(`[1v1] ${socket.id} hat Pool verlassen.`);
}
});
/* ── Spielfeld: Spieler betritt Arena-Room ── */
socket.on("arena_join", (data) => {
const { matchId, slot } = data;
if (!matchId || !slot) return;
if (!io._arenaRooms) io._arenaRooms = new Map();
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]) {
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"]}`);
// 30-Sekunden Bereit-Timer starten
startReadyTimer(io, matchId);
} else {
socket.to("arena_" + matchId).emit("arena_opponent_joined", {
name: room.names[slot],
slot,
});
}
});
/* ── Bereit-System ── */
socket.on("player_ready", (data) => {
const { matchId, slot } = data;
if (!matchId || !slot) return;
if (!io._arenaReady) io._arenaReady = new Map();
if (!io._arenaReady.has(matchId)) {
io._arenaReady.set(matchId, new Set());
}
const readySet = io._arenaReady.get(matchId);
readySet.add(slot);
io.to("arena_" + matchId).emit("ready_status", {
readyCount: readySet.size,
readySlots: Array.from(readySet),
});
console.log(`[1v1] ${slot} bereit in ${matchId} (${readySet.size}/2)`);
if (readySet.size >= 2) {
stopReadyTimer(io, matchId);
io._arenaReady.delete(matchId);
}
});
/* ── Aufgeben ── */
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 });
});
/* ── Disconnect: aus Pool entfernen ── */
socket.on("disconnect", () => {
if (waitingPool.delete(socket.id)) {
console.log(`[1v1] ${socket.id} disconnected aus Pool entfernt.`);
}
});
}
module.exports = { registerArenaHandlers };

89
sockets/chat_socket.js Normal file
View File

@ -0,0 +1,89 @@
/* ============================================================
sockets/chat.js
Alle Socket-Events rund um Chat, Whisper & Online-Status
============================================================ */
const db = require("../database/database");
const onlineUsers = {}; // ingameName → socketId
function registerChatHandlers(io, socket) {
/* ── 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));
});
/* ── 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",
});
}
});
/* ── Flüstern ── */
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",
});
});
/* ── Private Nachricht ── */
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: aus Online-Liste entfernen ── */
socket.on("disconnect", () => {
if (socket.user) {
delete onlineUsers[socket.user];
io.emit("onlineUsers", Object.keys(onlineUsers));
}
});
}
module.exports = { registerChatHandlers };

View File

@ -42,7 +42,20 @@
</div>
<!-- Spielfeld-Sperre bis beide Spieler bereit sind -->
<div id="board-lock-overlay"></div>
<div id="board-lock-overlay">
<div id="ready-timer-box">
<div id="ready-timer-label">Bereit machen</div>
<div id="ready-timer-ring">
<svg viewBox="0 0 80 80" width="80" height="80"><circle cx="40" cy="40" r="34" class="timer-track"/><circle cx="40" cy="40" r="34" class="timer-fill" id="timer-circle"/></svg>
<div id="ready-timer-number">30</div>
</div>
<div id="ready-timer-sub">Beide Spieler müssen BEREIT klicken</div>
<div id="ready-status-row">
<div class="ready-pip" id="pip-player1">⬜ Spieler 1</div>
<div class="ready-pip" id="pip-player2">⬜ Spieler 2</div>
</div>
</div>
</div>
<!-- LEFT AVATAR (Spieler 1) -->
<div class="avatar avatar-left" id="avLeft">
@ -240,8 +253,37 @@
socket.emit("player_ready", { matchId, slot: mySlot });
}
// Timer-Kreis: Umfang des Kreises (r=34 → 2*π*34 ≈ 213.6)
const CIRCUMFERENCE = 2 * Math.PI * 34;
const timerCircle = document.getElementById("timer-circle");
if (timerCircle) timerCircle.style.strokeDasharray = CIRCUMFERENCE;
socket.on("ready_timer", (data) => {
const { remaining } = data;
const num = document.getElementById("ready-timer-number");
if (num) num.textContent = remaining;
// Kreis-Fortschritt aktualisieren
if (timerCircle) {
const progress = remaining / 30;
const offset = CIRCUMFERENCE * (1 - progress);
timerCircle.style.strokeDashoffset = offset;
// Farbe: grün → gelb → rot
if (remaining > 15) timerCircle.style.stroke = "#27ae60";
else if (remaining > 7) timerCircle.style.stroke = "#f39c12";
else timerCircle.style.stroke = "#e74c3c";
}
});
socket.on("ready_status", (data) => {
// data.readyCount = wie viele Spieler bereits bereit sind
// Pips aktualisieren
const pip1 = document.getElementById("pip-player1");
const pip2 = document.getElementById("pip-player2");
if (data.readySlots && pip1 && pip2) {
if (data.readySlots.includes("player1")) pip1.textContent = "✅ " + (document.getElementById("nameLeft")?.textContent || "Spieler 1");
if (data.readySlots.includes("player2")) pip2.textContent = "✅ " + (document.getElementById("nameRight")?.textContent || "Spieler 2");
}
if (data.readyCount === 2) {
// Beide bereit → Sperre aufheben
const lock = document.getElementById("board-lock-overlay");
@ -255,6 +297,19 @@
}
});
socket.on("match_cancelled", (data) => {
const lock = document.getElementById("board-lock-overlay");
if (lock) {
lock.innerHTML = `
<div id="ready-timer-box">
<div id="ready-timer-label" style="color:#e74c3c;">⏰ Zeit abgelaufen</div>
<div id="ready-timer-sub">${data.message || "Match abgebrochen."}</div>
</div>
`;
}
// Aufgabe-Logik kommt hier rein
});
function handleAufgeben() {
// Funktion offen hier kann später die Aufgabe-Logik rein
socket.emit("player_surrender", { matchId, slot: mySlot });