This commit is contained in:
cay 2026-04-12 11:48:04 +01:00
parent a766fc7fe9
commit 65b0d29595
4 changed files with 210 additions and 326 deletions

23
app.js
View File

@ -24,12 +24,12 @@ const blackmarket = require("./routes/blackmarket.route");
const mineRoute = require("./routes/mine.route");
const carddeckRoutes = require("./routes/carddeck.route");
const arenaRoutes = require("./routes/arena.route");
const { registerArenaHandlers } = require("./sockets/arena");
const { registerArenaHandlers } = require("./sockets/arena.socket");
const { registerChatHandlers } = require("./sockets/chat");
const boosterRoutes = require("./routes/booster.route");
const pointsRoutes = require("./routes/points.route");
const pointsRoutes = require("./routes/points.route");
const combineRoutes = require("./routes/combine.route");
const bazaarRoutes = require("./routes/bazaar.route");
const bazaarRoutes = require("./routes/bazaar.route");
const compression = require("compression");
@ -61,11 +61,20 @@ app.use(
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
scriptSrcAttr: ["'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
styleSrc: [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com",
"https://cdnjs.cloudflare.com",
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com",
"https://cdnjs.cloudflare.com",
],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'", "ws:", "wss:"],
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
},
},
}),
@ -105,7 +114,7 @@ app.use(
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
const shopRoutes = require("./routes/shop.route");
const shopRoutes = require("./routes/shop.route");
/* ========================
WICHTIG: Shop/Webhook VOR express.json()

View File

@ -1,199 +0,0 @@
/* ============================================================
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 };

View File

@ -367,7 +367,10 @@ function getRoom(io, matchId) {
// Sendet an beide Spieler über ihre gespeicherten Socket-IDs
function emitToMatch(io, matchId, event, data) {
const room = getRoom(io, matchId);
if (!room) { console.warn(`[emitToMatch] Kein Room für ${matchId}`); return; }
if (!room) {
console.warn(`[emitToMatch] Kein Room für ${matchId}`);
return;
}
["player1", "player2"].forEach((slot) => {
if (room.sockets[slot]) {
io.to(room.sockets[slot]).emit(event, data);
@ -442,7 +445,9 @@ function registerArenaHandlers(io, socket) {
}
// Sonst: bereits guter Name gespeichert → nicht überschreiben
console.log(`[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`);
console.log(
`[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`,
);
socket.join("arena_" + matchId);
@ -479,26 +484,23 @@ function registerArenaHandlers(io, socket) {
if (!io._arenaReady.has(matchId)) io._arenaReady.set(matchId, new Set());
const readySet = io._arenaReady.get(matchId);
readySet.add(slot);
const readyData = { readyCount: readySet.size, readySlots: Array.from(readySet) };
const readyData = {
readyCount: readySet.size,
readySlots: Array.from(readySet),
};
emitToMatch(io, matchId, "ready_status", readyData);
console.log(`[1v1] ready_status: ${readySet.size}/2 bereit | Match ${matchId}`);
console.log(
`[1v1] ready_status: ${readySet.size}/2 bereit | Match ${matchId}`,
);
if (readySet.size >= 2) {
stopReadyTimer(io, matchId);
io._arenaReady.delete(matchId);
// Server startet Zug direkt kein end_turn_init vom Client nötig
// Startspieler: player1 (deterministisch, kein Flip nötig auf Server-Seite)
const starterSlot = "player1";
if (!io._turnInit) io._turnInit = new Set();
if (!io._turnInit.has(matchId)) {
io._turnInit.add(matchId);
setTimeout(() => io._turnInit?.delete(matchId), 60000);
const room = io._arenaRooms?.get(matchId);
const boardCards = room?.boardCards || [];
emitToMatch(io, matchId, "turn_change", { activeSlot: starterSlot, boardSync: boardCards });
console.log(`[1v1] Spiel startet → ${starterSlot} beginnt | Match ${matchId}`);
}
// Startspieler wird vom Client per start_turn_request gesendet
// (Client kennt durch seed-basiertes Flip wer links = wer anfängt)
console.log(
`[1v1] Beide bereit warte auf start_turn_request | Match ${matchId}`,
);
}
});
@ -522,7 +524,9 @@ function registerArenaHandlers(io, socket) {
boardSync: boardCards,
});
console.log(`[1v1] Zug: ${slot}${nextSlot} | boardCards=${boardCards.length} | Match ${matchId}`);
console.log(
`[1v1] Zug: ${slot}${nextSlot} | boardCards=${boardCards.length} | Match ${matchId}`,
);
});
/* ── Karte gespielt → direkt an Gegner senden ── */
@ -540,20 +544,29 @@ function registerArenaHandlers(io, socket) {
roomState.boardCards.push(data);
}
console.log(`[1v1] card_played: ${data.card?.name}${data.boardSlot} | Match ${matchId}`);
console.log(
`[1v1] card_played: ${data.card?.name}${data.boardSlot} | Match ${matchId}`,
);
});
socket.on("end_turn_init", (data) => {
// Client sendet nach ready_status den leftSlot (wer anfängt)
socket.on("start_turn_request", (data) => {
const { matchId, starterSlot } = data;
if (!matchId || !starterSlot) return;
// Nur einmal senden (erster Empfang gewinnt)
if (!io._turnInit) io._turnInit = new Set();
// Nur einmal pro Match ausführen
if (io._turnInit.has(matchId)) return;
io._turnInit.add(matchId);
emitToMatch(io, matchId, "turn_change", { activeSlot: starterSlot });
console.log(`[1v1] Startzug: ${starterSlot} | Match ${matchId}`);
// Cleanup nach 60s (lang genug damit doppelte end_turn_init geblockt bleiben)
setTimeout(() => io._turnInit?.delete(matchId), 60000);
const room = io._arenaRooms?.get(matchId);
const boardCards = room?.boardCards || [];
emitToMatch(io, matchId, "turn_change", {
activeSlot: starterSlot,
boardSync: boardCards,
});
console.log(
`[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`,
);
});
/* ── 2v2 & 4v4 ── */

View File

@ -167,7 +167,8 @@
}
/* ── Reichweite & Laufen Badges (Hand + Board) ── */
.cs-range, .cs-race {
.cs-range,
.cs-race {
position: absolute;
display: flex;
align-items: center;
@ -182,14 +183,16 @@
pointer-events: none;
}
.cs-range {
bottom: 22px; left: 3px;
background: rgba(30,20,0,0.85);
bottom: 22px;
left: 3px;
background: rgba(30, 20, 0, 0.85);
border: 1px solid #e8b84b;
color: #e8b84b;
}
.cs-race {
bottom: 6px; left: 3px;
background: rgba(0,25,0,0.85);
bottom: 6px;
left: 3px;
background: rgba(0, 25, 0, 0.85);
border: 1px solid #7de87d;
color: #7de87d;
}
@ -416,7 +419,7 @@
/* ── Stat-Icon SVGs ─────────────────────────────────── */
const SVG_RANGE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#e8b84b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2 Q1 8 4 14"/><line x1="4" y1="2" x2="4" y2="14" stroke-width="0.7" stroke-dasharray="2,1.5"/><line x1="4" y1="8" x2="13" y2="8"/><polyline points="11,6 13,8 11,10"/><line x1="5" y1="7" x2="4" y2="8"/><line x1="5" y1="9" x2="4" y2="8"/></svg>`;
const SVG_RACE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/><line x1="9" y1="4" x2="8" y2="9"/><line x1="8" y1="9" x2="10" y2="14"/><line x1="8" y1="9" x2="6" y2="13"/><line x1="8.5" y1="5.5" x2="11" y2="8"/><line x1="8.5" y1="5.5" x2="6" y2="7"/></svg>`;
const SVG_RACE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/><line x1="9" y1="4" x2="8" y2="9"/><line x1="8" y1="9" x2="10" y2="14"/><line x1="8" y1="9" x2="6" y2="13"/><line x1="8.5" y1="5.5" x2="11" y2="8"/><line x1="8.5" y1="5.5" x2="6" y2="7"/></svg>`;
/* ── Slot rendern ──────────────────────────────────── */
function renderHandSlot(id) {
@ -437,8 +440,8 @@
const atkVal = card.attack ?? null;
const defVal = card.defends ?? null;
const rngVal = card.range ?? null;
const rceVal = card.race ?? null;
const rngVal = card.range ?? null;
const rceVal = card.race ?? null;
const statsHtml = `
<div class="card-stat-overlay">
@ -450,12 +453,13 @@
: ""
}
${rngVal != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ""}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
</div>`;
const readyBadge = (isReady && isMyTurn)
? `<div class="hand-slot-ready-badge">SPIELEN</div>`
: "";
const readyBadge =
isReady && isMyTurn
? `<div class="hand-slot-ready-badge">SPIELEN</div>`
: "";
slot.innerHTML = card.image
? `<img src="/images/cards/${card.image}"
@ -547,41 +551,33 @@
})();
/* ══════════════════════════════════════════════════════
ZUG-TIMER (20 Sekunden)
ZUG-TIMER (30 Sekunden) läuft für BEIDE Spieler sichtbar
══════════════════════════════════════════════════════ */
const TURN_SECONDS = 20;
const TURN_SECONDS = 30;
const TT_CIRCUM = 2 * Math.PI * 18; // r=18
let turnTimerInt = null;
let turnSecsLeft = TURN_SECONDS;
function startTurnTimer() {
// activeName: Name des Spielers der gerade dran ist
function startTurnTimer(activeName) {
clearInterval(turnTimerInt);
turnSecsLeft = TURN_SECONDS;
updateTimerUI(turnSecsLeft);
updateTimerUI(turnSecsLeft, activeName);
const wrap = document.getElementById("turn-timer-wrap");
if (wrap) wrap.style.display = "flex";
turnTimerInt = setInterval(() => {
turnSecsLeft--;
updateTimerUI(turnSecsLeft);
updateTimerUI(turnSecsLeft, activeName);
if (turnSecsLeft <= 0) {
clearInterval(turnTimerInt);
// Nur der aktive Spieler sendet end_turn
if (isMyTurn) {
tickHandCooldowns();
drawNextCard(); // eine Karte aus dem Deck nachziehen
drawNextCard();
setTurnState(false);
socket.emit("end_turn", { matchId, slot: mySlot });
// Watchdog: falls turn_change nach 5s nicht ankommt → selbst freischalten
setTimeout(() => {
if (!isMyTurn) {
console.warn(
"[1v1] Kein turn_change nach 5s Zug lokal übergeben",
);
setTurnState(true);
}
}, 5000);
}
}
}, 1000);
@ -593,15 +589,17 @@
if (wrap) wrap.style.display = "none";
}
function updateTimerUI(secs) {
function updateTimerUI(secs, activeName) {
const num = document.getElementById("turn-timer-num");
const circle = document.getElementById("tt-circle");
const wrap = document.getElementById("turn-timer-wrap");
if (num) num.textContent = secs;
if (wrap && activeName) wrap.title = activeName + " ist am Zug";
if (circle) {
circle.style.strokeDashoffset = TT_CIRCUM * (1 - secs / TURN_SECONDS);
circle.style.stroke =
secs > 10 ? "#27ae60" : secs > 5 ? "#f39c12" : "#e74c3c";
if (secs <= 5) {
secs > 15 ? "#27ae60" : secs > 8 ? "#f39c12" : "#e74c3c";
if (secs <= 8) {
num.style.color = "#e74c3c";
num.style.animation = "tt-pulse 0.5s ease-in-out infinite";
} else {
@ -619,7 +617,8 @@
let amILeftPlayer = null;
let isMyTurn = false;
function setTurnState(myTurn) {
// activeName = wer gerade dran ist (für Timer-Text + Indicator)
function setTurnState(myTurn, activeName) {
isMyTurn = myTurn;
const btn = document.getElementById("end-turn-btn");
if (!btn) return;
@ -628,36 +627,29 @@
btn.disabled = false;
btn.textContent = "Zug beenden";
btn.style.opacity = "1";
// Draggable für bereite Karten aktivieren
setTimeout(() => handCardIds.forEach(id => renderHandSlot(id)), 50);
document.getElementById("turn-indicator")?.remove();
startTurnTimer();
// Watchdog für wartenden Spieler abbrechen falls vorhanden
if (window._waitWatchdog) {
clearTimeout(window._waitWatchdog);
window._waitWatchdog = null;
}
// Draggable für bereite Karten aktivieren
setTimeout(() => handCardIds.forEach((id) => renderHandSlot(id)), 50);
// Timer für beide Spieler sichtbar starten
startTurnTimer(activeName || "Du");
} else {
btn.disabled = true;
btn.style.opacity = "0.4";
stopTurnTimer();
// Draggable deaktivieren
handCardIds.forEach(id => { const s = document.getElementById(id); if(s){s.draggable=false;delete s.dataset.cardSlotId;} });
showTurnIndicator();
// Watchdog: falls nach 26s kein turn_change → eigenen Zug erzwingen
if (window._waitWatchdog) clearTimeout(window._waitWatchdog);
window._waitWatchdog = setTimeout(() => {
if (!isMyTurn) {
console.warn(
"[1v1] Kein turn_change nach 26s Zug lokal übernommen",
);
setTurnState(true);
// Draggable sofort deaktivieren
handCardIds.forEach((id) => {
const s = document.getElementById(id);
if (s) {
s.draggable = false;
delete s.dataset.cardSlotId;
}
}, 26000);
});
showTurnIndicator(activeName);
// Timer für Gegner-Countdown auch anzeigen
startTurnTimer(activeName || "Gegner");
}
}
function showTurnIndicator() {
function showTurnIndicator(activeName) {
document.getElementById("turn-indicator")?.remove();
const ind = document.createElement("div");
ind.id = "turn-indicator";
@ -677,34 +669,35 @@
text-transform:uppercase;
box-shadow:0 4px 20px rgba(0,0,0,0.6);
`;
ind.textContent = "⏳ Gegner ist am Zug";
ind.textContent = activeName
? `⏳ ${activeName} ist am Zug`
: "⏳ Gegner ist am Zug";
document.body.appendChild(ind);
}
/* ── Zug beenden: CD ticken + Karte ziehen + Server informieren ── */
/* ── Zug beenden: per Button oder Timer-Ablauf ── */
document.getElementById("end-turn-btn")?.addEventListener("click", () => {
if (!isMyTurn) return;
// Sofort Timer stoppen und Zug abgeben nicht auf turn_change warten
clearInterval(turnTimerInt);
stopTurnTimer();
tickHandCooldowns();
// Eine Karte aus dem Deck nachziehen
drawNextCard();
setTurnState(false);
socket.emit("end_turn", { matchId, slot: mySlot });
// Server antwortet mit turn_change → dann wird setTurnState erneut aufgerufen
});
/* ── Hilfsfunktion: Karte mit Stats in einen Slot rendern ── */
function renderCardInSlot(slot, card) {
if (!slot || !card) return;
const atkVal = card.attack ?? null;
const defVal = card.defends ?? null;
const cdVal = card.cooldown ?? null;
const rngVal = card.range ?? null;
const rceVal = card.race ?? null;
const cdVal = card.cooldown ?? null;
const rngVal = card.range ?? null;
const rceVal = card.race ?? null;
const hasAtk = atkVal != null;
const hasDef = defVal != null;
const hasCd = cdVal != null;
const hasCd = cdVal != null;
const hasRng = rngVal != null;
const hasRce = rceVal != null;
const statsHtml =
@ -713,9 +706,9 @@
<div class="card-stat-overlay">
${hasAtk ? `<span class="cs-atk">${atkVal}</span>` : ""}
${hasDef ? `<span class="cs-def">${defVal}</span>` : ""}
${hasCd ? `<span class="cs-cd">${cdVal}</span>` : ""}
${hasCd ? `<span class="cs-cd">${cdVal}</span>` : ""}
${hasRng ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ""}
${hasRce ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
${hasRce ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
</div>`
: "";
// note: SVG_RACE is defined as the walking icon above
@ -741,15 +734,14 @@
const mySlot = urlParams.get("slot") || "<%= mySlot || 'player1' %>";
const amIPlayer1 = mySlot === "player1";
// Board-Klasse setzen damit CSS die richtigen Zonen einfärbt
document.querySelector(".board")?.classList.add(
amIPlayer1 ? "my-side-left" : "my-side-right"
);
// Board-Klasse wird erst nach ready_status gesetzt wenn amILeftPlayer bekannt ist
// Gegner-Name direkt aus URL setzen (kommt aus match_found, immer korrekt)
const opponentNameFromUrl = urlParams.get("opponent");
if (opponentNameFromUrl) {
const oppEl = document.getElementById(amIPlayer1 ? "nameRight" : "nameLeft");
const oppEl = document.getElementById(
amIPlayer1 ? "nameRight" : "nameLeft",
);
if (oppEl) oppEl.textContent = decodeURIComponent(opponentNameFromUrl);
}
@ -851,9 +843,17 @@
const currentOppName = oppEl?.textContent || "";
const urlOppName = urlParams.get("opponent");
// Nur überschreiben wenn Server einen echten Namen hat (nicht "Spieler"/"Spieler 1"/"Spieler 2")
if (oppEl && oppName && !["Spieler", "Spieler 1", "Spieler 2", "Gegner"].includes(oppName)) {
if (
oppEl &&
oppName &&
!["Spieler", "Spieler 1", "Spieler 2", "Gegner"].includes(oppName)
) {
oppEl.textContent = oppName;
} else if (oppEl && urlOppName && (!currentOppName || currentOppName === "Gegner")) {
} else if (
oppEl &&
urlOppName &&
(!currentOppName || currentOppName === "Gegner")
) {
oppEl.textContent = decodeURIComponent(urlOppName);
}
// Eigenen Namen sichern falls noch nicht gesetzt
@ -884,18 +884,36 @@
/* ── Server: Zugwechsel ──────────────────────────────── */
socket.on("turn_change", (data) => {
// Sicherheits-Fallback: Overlay entfernen falls noch vorhanden
document.getElementById("board-lock-overlay")?.remove();
document.getElementById("connecting-overlay")?.remove();
if (data.boardSync) applyBoardSync(data.boardSync);
const nowMyTurn = data.activeSlot === mySlot;
console.log("[1v1] turn_change:", data.activeSlot, "| ich:", mySlot, "| meinZug:", nowMyTurn);
setTurnState(nowMyTurn);
// Aktiver Spieler: Slot aus Server-Daten
// Linker Spieler = window._leftSlot (gesetzt nach ready_status)
// Der linke Spieler fängt immer an.
const activeSlot = data.activeSlot;
const nowMyTurn = activeSlot === mySlot;
// Namen des aktiven Spielers für Timer + Indicator
const activeNameEl = document.getElementById(
activeSlot === "player1" ? "nameLeft" : "nameRight",
);
const activeName =
activeNameEl?.textContent || (nowMyTurn ? "Du" : "Gegner");
console.log(
`[1v1] turn_change: ${activeSlot} | ich: ${mySlot} | meinZug: ${nowMyTurn} | name: ${activeName}`,
);
setTurnState(nowMyTurn, activeName);
});
socket.on("turn_started", (data) => {
setTurnState(data.slot === mySlot);
const myT = data.slot === mySlot;
const nameEl = document.getElementById(
data.slot === "player1" ? "nameLeft" : "nameRight",
);
setTurnState(myT, nameEl?.textContent || (myT ? "Du" : "Gegner"));
});
/* ── Bereit-System ──────────────────────────────────── */
@ -994,9 +1012,38 @@
// Festlegen ob ich der linke Spieler bin
amILeftPlayer = flip ? mySlot === "player2" : mySlot === "player1";
// Server startet Zug automatisch wenn beide bereit sind
// turn_change kommt vom Server → kein end_turn_init nötig
console.log("[1v1] Beide bereit warte auf turn_change vom Server...");
// Board-CSS-Klasse jetzt setzen da amILeftPlayer bekannt ist
const board = document.querySelector(".board");
if (board) {
board.classList.remove("my-side-left", "my-side-right");
board.classList.add(
amILeftPlayer ? "my-side-left" : "my-side-right",
);
}
// Der LINKE Spieler fängt an → amILeftPlayer bestimmt wer turn_change bekommt
// Server schickt turn_change mit activeSlot=player1 → wir mappen das auf links
// Wenn ich der linke Spieler bin UND player1 bin → ich bin aktiv
// Wenn ich der linke Spieler bin aber player2 bin → ich bin auch aktiv (flip)
// → setTurnState direkt setzen basierend auf amILeftPlayer
const leftSlot = flip ? "player2" : "player1";
const iStartLeft = mySlot === leftSlot;
console.log(
`[1v1] Beide bereit | linker Spieler-Slot: ${leftSlot} | ich (${mySlot}) starte: ${iStartLeft}`,
);
window._leftSlot = leftSlot;
// Nur EINER der beiden Clients sendet start_turn_request
// Wir nehmen immer player1 als Absender (deterministisch, kein Doppel-Fire)
if (mySlot === "player1") {
socket.emit("start_turn_request", {
matchId,
starterSlot: leftSlot,
});
console.log(
`[1v1] start_turn_request gesendet: starterSlot=${leftSlot}`,
);
}
}
});
@ -1219,7 +1266,6 @@
r.readAsDataURL(file);
}
/* ══════════════════════════════════════════════════════
DRAG & DROP Karte aus Hand auf Board legen
══════════════════════════════════════════════════════ */
@ -1230,7 +1276,10 @@
/* Welche Slot-Indizes darf ich bespielen? */
function isMyZone(slotIndex) {
const idx = Number(slotIndex);
return amIPlayer1 ? idx <= 3 : idx >= 9;
// amILeftPlayer nach ready_status bekannt; Fallback: amIPlayer1
const iAmLeft = amILeftPlayer !== null ? amILeftPlayer : amIPlayer1;
// Linker Spieler: Slots 13 | Rechter Spieler: Slots 911
return iAmLeft ? idx <= 3 : idx >= 9;
}
/* Drop-Zones aktivieren / deaktivieren */
@ -1250,16 +1299,16 @@
function renderCardOnBoard(slotEl, card) {
const atkVal = card.attack ?? null;
const defVal = card.defends ?? null;
const cdVal = card.cooldown ?? null;
const rngVal = card.range ?? null;
const rceVal = card.race ?? null;
const cdVal = card.cooldown ?? null;
const rngVal = card.range ?? null;
const rceVal = card.race ?? null;
const statsHtml = `
<div class="card-stat-overlay">
${atkVal != null ? `<span class="cs-atk">${atkVal}</span>` : ""}
${defVal != null ? `<span class="cs-def">${defVal}</span>` : ""}
${cdVal != null ? `<span class="cs-cd">${cdVal}</span>` : ""}
${cdVal != null ? `<span class="cs-cd">${cdVal}</span>` : ""}
${rngVal != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ""}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
</div>`;
slotEl.classList.add("slot-occupied");
slotEl.innerHTML = card.image
@ -1280,7 +1329,10 @@
document.getElementById("handArea").addEventListener("dragstart", (e) => {
const slot = e.target.closest("[data-card-slot-id]");
if (!slot || !isMyTurn) { e.preventDefault(); return; }
if (!slot || !isMyTurn) {
e.preventDefault();
return;
}
draggedCardSlotId = slot.dataset.cardSlotId;
slot.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
@ -1308,7 +1360,9 @@
e.preventDefault();
e.dataTransfer.dropEffect = "move";
// Hover-Highlight
row.querySelectorAll(".drop-zone-hover").forEach(s => s.classList.remove("drop-zone-hover"));
row
.querySelectorAll(".drop-zone-hover")
.forEach((s) => s.classList.remove("drop-zone-hover"));
slot.classList.add("drop-zone-hover");
});
@ -1324,7 +1378,8 @@
const idx = Number(slot.dataset.slotIndex);
if (!isMyZone(idx) || boardState[slot.id]) return;
// Fallback: manche Browser liefern draggedCardSlotId über dataTransfer
const sourceId = draggedCardSlotId || e.dataTransfer.getData("text/plain");
const sourceId =
draggedCardSlotId || e.dataTransfer.getData("text/plain");
if (!sourceId) return;
const cardState = handSlotState[sourceId];
@ -1350,7 +1405,9 @@
card: cardState.card,
});
console.log(`[1v1] Karte gespielt: ${cardState.card.name} → ${slot.id} (aus ${sourceId})`);
console.log(
`[1v1] Karte gespielt: ${cardState.card.name} → ${slot.id} (aus ${sourceId})`,
);
});
});
@ -1364,18 +1421,22 @@
const slotEl = document.getElementById(data.boardSlot);
if (!slotEl) {
console.warn("[1v1] card_played: Slot nicht gefunden:", data.boardSlot);
console.warn(
"[1v1] card_played: Slot nicht gefunden:",
data.boardSlot,
);
return;
}
boardState[data.boardSlot] = data.card;
renderCardOnBoard(slotEl, data.card);
console.log("[1v1] Gegner Karte:", data.card?.name, "→", data.boardSlot);
console.log(
"[1v1] Gegner Karte:",
data.card?.name,
"→",
data.boardSlot,
);
});
/* Wenn Zug wechselt: draggable aktualisieren */
const _origSetTurnState = setTurnState;
// setTurnState ist bereits definiert, wir patchen es nach
/* ── Event-Listener ─────────────────────────────────── */
document
.getElementById("bereit-btn")