diff --git a/app.js b/app.js index b30782c..552ed11 100644 --- a/app.js +++ b/app.js @@ -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() diff --git a/sockets/arena.js b/sockets/arena.js deleted file mode 100644 index 2f70808..0000000 --- a/sockets/arena.js +++ /dev/null @@ -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 }; diff --git a/sockets/arena_socket.js b/sockets/arena.socket.js similarity index 91% rename from sockets/arena_socket.js rename to sockets/arena.socket.js index 889cda4..eea576b 100644 --- a/sockets/arena_socket.js +++ b/sockets/arena.socket.js @@ -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 ── */ diff --git a/views/1v1-battlefield.ejs b/views/1v1-battlefield.ejs index f4bca54..aa867eb 100644 --- a/views/1v1-battlefield.ejs +++ b/views/1v1-battlefield.ejs @@ -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 = ``; - const SVG_RACE = ``; + const SVG_RACE = ``; /* ── 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 = `
@@ -450,12 +453,13 @@ : "" } ${rngVal != null ? `${SVG_RANGE} ${rngVal}` : ""} - ${rceVal != null ? `${SVG_RACE} ${rceVal}` : ""} + ${rceVal != null ? `${SVG_RACE} ${rceVal}` : ""}
`; - const readyBadge = (isReady && isMyTurn) - ? `
SPIELEN
` - : ""; + const readyBadge = + isReady && isMyTurn + ? `
SPIELEN
` + : ""; slot.innerHTML = card.image ? ` { 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 @@
${hasAtk ? `${atkVal}` : ""} ${hasDef ? `${defVal}` : ""} - ${hasCd ? `${cdVal}` : ""} + ${hasCd ? `${cdVal}` : ""} ${hasRng ? `${SVG_RANGE} ${rngVal}` : ""} - ${hasRce ? `${SVG_RACE} ${rceVal}` : ""} + ${hasRce ? `${SVG_RACE} ${rceVal}` : ""}
` : ""; // 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 1–3 | Rechter Spieler: Slots 9–11 + 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 = `
${atkVal != null ? `${atkVal}` : ""} ${defVal != null ? `${defVal}` : ""} - ${cdVal != null ? `${cdVal}` : ""} + ${cdVal != null ? `${cdVal}` : ""} ${rngVal != null ? `${SVG_RANGE} ${rngVal}` : ""} - ${rceVal != null ? `${SVG_RACE} ${rceVal}` : ""} + ${rceVal != null ? `${SVG_RACE} ${rceVal}` : ""}
`; 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")