This commit is contained in:
Cay 2026-03-18 11:10:00 +00:00
parent a1625ba23f
commit 19b2acd434
4 changed files with 675 additions and 81 deletions

149
app.js
View File

@ -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.`);
}
});
});
/* ========================

View File

@ -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} &nbsp;vs&nbsp; ${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 &nbsp;·&nbsp; vs ${opponentName}`
: "⚔️ Arena &nbsp;·&nbsp; 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 &nbsp;·&nbsp; 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);
}

View File

@ -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,
});
});

View File

@ -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>