Arena1
This commit is contained in:
parent
a1625ba23f
commit
19b2acd434
149
app.js
149
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.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ========================
|
||||
|
||||
@ -29,15 +29,18 @@ export async function loadArena() {
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Status-Anzeige (erscheint nur während Suche) -->
|
||||
<div id="arena-queue-status" style="display:none;"></div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
injectArenaPopupStyles();
|
||||
injectArenaStyles();
|
||||
initArenaModes();
|
||||
}
|
||||
|
||||
/* ── Styles einmalig ins <head> 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 = `
|
||||
<div class="mfo-title">⚔️ Match gefunden!</div>
|
||||
<div class="mfo-vs">${myName} vs ${opponentName}</div>
|
||||
<div class="mfo-bar"><div class="mfo-bar-fill" id="mfBar"></div></div>
|
||||
`;
|
||||
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 = `
|
||||
<div id="arena-popup-titlebar">
|
||||
<div class="ap-left">
|
||||
@ -156,9 +367,9 @@ function openArenaPopup(src) {
|
||||
<div class="ap-dot min"></div>
|
||||
<div class="ap-dot expand" id="arena-fullscreen-btn"></div>
|
||||
</div>
|
||||
<span class="ap-title">⚔️ Arena · 1v1</span>
|
||||
<span class="ap-title">${title}</span>
|
||||
</div>
|
||||
<span class="ap-url">${src}</span>
|
||||
<span class="ap-url">${matchId || src}</span>
|
||||
</div>
|
||||
<iframe src="${src}" allowfullscreen></iframe>
|
||||
`;
|
||||
@ -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}
|
||||
<br>
|
||||
<span class="qs-cancel" id="qs-cancel-btn">Suche abbrechen</span>
|
||||
`;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,9 +11,94 @@
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="/css/1v1.css" />
|
||||
|
||||
<style>
|
||||
/* ── Match-Info Banner ── */
|
||||
#match-banner {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(10, 8, 5, 0.92);
|
||||
border-bottom: 1px solid rgba(255, 215, 80, 0.3);
|
||||
padding: 6px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: "Cinzel", serif;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 215, 80, 0.7);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
#match-banner .mb-id {
|
||||
color: rgba(255,255,255,0.25);
|
||||
font-size: 10px;
|
||||
}
|
||||
#match-banner .mb-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
#match-banner .mb-dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #2ecc71;
|
||||
box-shadow: 0 0 6px #2ecc71;
|
||||
}
|
||||
|
||||
/* ── Verbindungs-Overlay ── */
|
||||
#connecting-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0,0,0,0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Cinzel", serif;
|
||||
color: rgba(255,215,80,0.85);
|
||||
font-size: 18px;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
#connecting-overlay p {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
#connecting-overlay .spinner {
|
||||
width: 40px; height: 40px;
|
||||
border: 3px solid rgba(255,215,80,0.2);
|
||||
border-top-color: #ffd750;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Match-Info oben -->
|
||||
<div id="match-banner">
|
||||
<span>⚔️ 1v1 KAMPFARENA</span>
|
||||
<div class="mb-status">
|
||||
<div class="mb-dot"></div>
|
||||
<span id="mb-status-text">Verbinde…</span>
|
||||
</div>
|
||||
<span class="mb-id">Match: <%= matchId || "—" %></span>
|
||||
</div>
|
||||
|
||||
<!-- Warte-Overlay bis Gegner verbunden ist -->
|
||||
<div id="connecting-overlay">
|
||||
<div class="spinner"></div>
|
||||
<div>Warte auf Gegner…</div>
|
||||
<p>Verbindung wird hergestellt</p>
|
||||
</div>
|
||||
|
||||
<div class="board">
|
||||
<!-- TOP BAR -->
|
||||
<div class="top-bar">
|
||||
@ -29,7 +114,7 @@
|
||||
<button class="end-turn-btn">Zug beenden</button>
|
||||
</div>
|
||||
|
||||
<!-- LEFT AVATAR -->
|
||||
<!-- LEFT AVATAR (Spieler 1) -->
|
||||
<div class="avatar avatar-left" id="avLeft">
|
||||
<input
|
||||
type="file"
|
||||
@ -41,25 +126,25 @@
|
||||
|
||||
<div class="av-placeholder" id="avPhL">
|
||||
<div class="av-icon">⚔</div>
|
||||
<div class="av-lbl"><%= player1 || "Spieler 1" %></div>
|
||||
<div class="av-lbl" id="nameLeft"><%= player1 || "Spieler 1" %></div>
|
||||
</div>
|
||||
|
||||
<div class="av-stats">
|
||||
<div class="stat hp">
|
||||
<span class="s-icon">❤</span>
|
||||
<span class="s-val"><%= player1hp || 20 %></span>
|
||||
<span class="s-val" id="hpLeft"><%= player1hp || 20 %></span>
|
||||
</div>
|
||||
|
||||
<div class="stat mana">
|
||||
<span class="s-icon">💧</span>
|
||||
<span class="s-val"><%= player1mana || 3 %></span>
|
||||
<span class="s-val" id="manaLeft"><%= player1mana || 3 %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hp-orb"><%= player1hp || 15 %></div>
|
||||
<div class="hp-orb" id="orbLeft"><%= player1hp || 15 %></div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT AVATAR -->
|
||||
<!-- RIGHT AVATAR (Spieler 2) -->
|
||||
<div class="avatar avatar-right" id="avRight">
|
||||
<input
|
||||
type="file"
|
||||
@ -71,22 +156,22 @@
|
||||
|
||||
<div class="av-placeholder" id="avPhR">
|
||||
<div class="av-icon">🛡</div>
|
||||
<div class="av-lbl"><%= player2 || "Spieler 2" %></div>
|
||||
<div class="av-lbl" id="nameRight"><%= player2 || "Spieler 2" %></div>
|
||||
</div>
|
||||
|
||||
<div class="av-stats">
|
||||
<div class="stat hp">
|
||||
<span class="s-icon">❤</span>
|
||||
<span class="s-val"><%= player2hp || 20 %></span>
|
||||
<span class="s-val" id="hpRight"><%= player2hp || 20 %></span>
|
||||
</div>
|
||||
|
||||
<div class="stat mana">
|
||||
<span class="s-icon">💧</span>
|
||||
<span class="s-val"><%= player2mana || 3 %></span>
|
||||
<span class="s-val" id="manaRight"><%= player2mana || 3 %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hp-orb"><%= player2hp || 15 %></div>
|
||||
<div class="hp-orb" id="orbRight"><%= player2hp || 15 %></div>
|
||||
</div>
|
||||
|
||||
<!-- CENTER CARD ROWS -->
|
||||
@ -120,7 +205,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Socket.io (wird vom Parent-Fenster geerbt oder eigenständig geladen) -->
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
|
||||
<script>
|
||||
// ── Spielfeld-Karten aufbauen ──────────────────────────────────────────
|
||||
["row1", "row2"].forEach((id) => {
|
||||
const row = document.getElementById(id);
|
||||
|
||||
@ -128,7 +217,6 @@
|
||||
const s = document.createElement("div");
|
||||
s.className = "card-slot";
|
||||
|
||||
// Erster Slot in Reihe 1: Silberklinge hardcodiert
|
||||
if (id === "row1" && i === 1) {
|
||||
s.innerHTML = `
|
||||
<img
|
||||
@ -148,9 +236,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ── Hand-Karten aufbauen ───────────────────────────────────────────────
|
||||
const hand = document.getElementById("handArea");
|
||||
|
||||
// Erster Slot: Silberklinge (hardcodiert)
|
||||
const silberklinge = document.createElement("div");
|
||||
silberklinge.className = "hand-slot";
|
||||
silberklinge.innerHTML = `
|
||||
@ -162,7 +250,6 @@
|
||||
`;
|
||||
hand.appendChild(silberklinge);
|
||||
|
||||
// Restliche leere Slots
|
||||
for (let i = 1; i < 8; i++) {
|
||||
const s = document.createElement("div");
|
||||
s.className = "hand-slot";
|
||||
@ -170,24 +257,61 @@
|
||||
hand.appendChild(s);
|
||||
}
|
||||
|
||||
// ── Match-Daten aus URL ────────────────────────────────────────────────
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const matchId = urlParams.get("match") || "<%= matchId || '' %>";
|
||||
const mySlot = urlParams.get("slot") || "<%= mySlot || 'player1' %>";
|
||||
const amIPlayer1 = mySlot === "player1";
|
||||
|
||||
// ── Socket.io: Gegner-Namen live empfangen ─────────────────────────────
|
||||
const socket = io();
|
||||
|
||||
socket.emit("arena_join", { matchId, slot: mySlot });
|
||||
|
||||
socket.on("arena_opponent_joined", (data) => {
|
||||
// Gegner-Name anzeigen
|
||||
const opponentName = data.name || "Gegner";
|
||||
|
||||
if (amIPlayer1) {
|
||||
document.getElementById("nameRight").textContent = opponentName;
|
||||
} else {
|
||||
document.getElementById("nameLeft").textContent = opponentName;
|
||||
}
|
||||
|
||||
// Overlay entfernen
|
||||
const overlay = document.getElementById("connecting-overlay");
|
||||
if (overlay) overlay.remove();
|
||||
|
||||
// Banner aktualisieren
|
||||
document.getElementById("mb-status-text").textContent = "Verbunden · Kampf läuft";
|
||||
});
|
||||
|
||||
// Beide Spieler sind verbunden → beide benachrichtigen
|
||||
socket.on("arena_ready", (data) => {
|
||||
const overlay = document.getElementById("connecting-overlay");
|
||||
if (overlay) overlay.remove();
|
||||
document.getElementById("mb-status-text").textContent = "Verbunden · Kampf läuft";
|
||||
|
||||
// Namen setzen
|
||||
document.getElementById("nameLeft").textContent = data.player1 || "Spieler 1";
|
||||
document.getElementById("nameRight").textContent = data.player2 || "Spieler 2";
|
||||
});
|
||||
|
||||
// ── Avatar laden (lokal) ──────────────────────────────────────────────
|
||||
function loadAvatar(input, imgId, parentId) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const r = new FileReader();
|
||||
|
||||
r.onload = (e) => {
|
||||
const img = document.getElementById(imgId);
|
||||
|
||||
img.src = e.target.result;
|
||||
img.style.display = "block";
|
||||
|
||||
const parent = document.getElementById(parentId);
|
||||
const ph = parent.querySelector(".av-placeholder");
|
||||
|
||||
if (ph) ph.style.display = "none";
|
||||
};
|
||||
|
||||
r.readAsDataURL(file);
|
||||
}
|
||||
|
||||
@ -196,16 +320,12 @@
|
||||
if (!file) return;
|
||||
|
||||
const r = new FileReader();
|
||||
|
||||
r.onload = (e) => {
|
||||
const img = document.getElementById(imgId);
|
||||
|
||||
img.src = e.target.result;
|
||||
img.style.display = "block";
|
||||
|
||||
img.parentElement.querySelector("span").style.display = "none";
|
||||
};
|
||||
|
||||
r.readAsDataURL(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user