/* ═══════════════════════════════════════════════════════════════ public/js/buildings/1v1.js Vollstaendige Spielfeld-Logik fuer das 1v1-Battlefield. EJS-Variablen kommen aus window.GAME_CONFIG (in der EJS gesetzt). ═══════════════════════════════════════════════════════════════ */ /* ── Konfiguration aus EJS-Bridge ────────────────────────── */ const matchId = new URLSearchParams(window.location.search).get("match") || window.GAME_CONFIG.matchId; const mySlot = new URLSearchParams(window.location.search).get("slot") || window.GAME_CONFIG.mySlot; const amIPlayer1 = mySlot === "player1"; /* ── Gegner-Name aus URL setzen ──────────────────────────── */ const _urlParams = new URLSearchParams(window.location.search); const opponentNameFromUrl = _urlParams.get("opponent"); if (opponentNameFromUrl) { const oppEl = document.getElementById(amIPlayer1 ? "nameRight" : "nameLeft"); if (oppEl) oppEl.textContent = decodeURIComponent(opponentNameFromUrl); } /* ═══════════════════════════════════════════════════════════ SPIELFELD AUFBAUEN ═══════════════════════════════════════════════════════════ */ ["row1", "row2"].forEach((rowId) => { const row = document.getElementById(rowId); for (let i = 1; i <= 11; i++) { const s = document.createElement("div"); s.className = "card-slot"; s.dataset.row = rowId; s.dataset.slotIndex = i; s.id = `${rowId}-slot-${i}`; s.innerHTML = '\u2736' + i + ""; row.appendChild(s); } }); const hand = document.getElementById("handArea"); /* ── Deck-Stapel ─────────────────────────────────────────── */ const deckSlot = document.createElement("div"); deckSlot.className = "hand-slot hand-slot-deck"; deckSlot.id = "deck-stack"; deckSlot.title = "Dein Deck"; deckSlot.innerHTML = `
\u2014`; hand.appendChild(deckSlot); /* ── Hand-Karten-Slots ───────────────────────────────────── */ const handCardIds = [ "hand-card-1", "hand-card-2", "hand-card-3", "hand-card-4", "hand-card-5", "hand-card-6", "hand-card-7", ]; handCardIds.forEach((id) => { const s = document.createElement("div"); s.className = "hand-slot hand-slot-card"; s.id = id; s.innerHTML = '\uD83C\uDCCF'; hand.appendChild(s); }); /* ═══════════════════════════════════════════════════════════ HAND & DECK SYSTEM ═══════════════════════════════════════════════════════════ */ let deckQueue = []; const handSlotState = {}; handCardIds.forEach((id) => { handSlotState[id] = null; }); /* ── SVG-Icons ───────────────────────────────────────────── */ const SVG_RANGE = ``; const SVG_RACE = ``; /* ── Hand-Slot rendern ───────────────────────────────────── */ function renderHandSlot(id) { const slot = document.getElementById(id); const state = handSlotState[id]; if (!slot) return; slot.innerHTML = ""; slot.style.backgroundImage = "none"; slot.style.opacity = "1"; slot.style.filter = "none"; slot.style.backgroundColor = "#0a0805"; slot.classList.remove("hand-slot-ready", "hand-slot--filled"); if (!state) { slot.innerHTML = '\uD83C\uDCCF'; slot.draggable = false; delete slot.dataset.cardSlotId; return; } const { card, currentCd } = state; const isReady = currentCd <= 0; const atkVal = card.attack ?? null; const defVal = card.defends ?? null; const rngVal = card.range ?? null; const rceVal = card.race ?? null; const statsHtml = `
${atkVal != null ? `${atkVal}` : ""} ${defVal != null ? `${defVal}` : ""} ${card.cooldown != null ? `${isReady ? "\u2713" : currentCd}` : ""} ${rngVal != null ? `${SVG_RANGE} ${rngVal}` : ""} ${rceVal != null ? `${SVG_RACE} ${rceVal}` : ""}
`; const readyBadge = isReady && isMyTurn ? '
SPIELEN
' : ""; slot.innerHTML = card.image ? `${statsHtml}${readyBadge}` : `
\u2694\uFE0F ${card.name}
${statsHtml}${readyBadge}`; slot.classList.toggle("hand-slot-ready", isReady); slot.classList.add("hand-slot--filled"); if (isReady && isMyTurn) { slot.draggable = true; slot.dataset.cardSlotId = id; } else { slot.draggable = false; delete slot.dataset.cardSlotId; } } function setHandSlot(id, card) { handSlotState[id] = { card, currentCd: card.cooldown ?? 0, wasReduced: false, }; renderHandSlot(id); } function drawNextCard() { if (deckQueue.length === 0) return; const freeSlot = handCardIds.find((id) => handSlotState[id] === null); if (!freeSlot) return; const card = deckQueue.shift(); setHandSlot(freeSlot, card); const countEl = document.getElementById("deck-count"); if (countEl) countEl.textContent = deckQueue.length; } function tickHandCooldowns() { handCardIds.forEach((id) => { const state = handSlotState[id]; if (!state) return; const baseCd = Number(state.card.cooldown || 0); if (state.currentCd <= 0 && !state.wasReduced) { state.currentCd = baseCd > 0 ? Math.max(1, Math.floor(baseCd / 2)) : 0; state.wasReduced = true; } else if (state.currentCd > 0) { state.currentCd--; } renderHandSlot(id); }); } /* ── Deck laden ──────────────────────────────────────────── */ (async () => { try { const urlP = new URLSearchParams(window.location.search); const deckId = urlP.get("deck") || sessionStorage.getItem("selectedDeckId"); if (!deckId) return; const res = await fetch("/api/decks/" + deckId + "/cards"); if (!res.ok) return; const cards = await res.json(); const expanded = []; cards.forEach((c) => { for (let i = 0; i < (c.amount ?? 1); i++) expanded.push(c); }); for (let i = expanded.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [expanded[i], expanded[j]] = [expanded[j], expanded[i]]; } for (let i = 0; i < 3; i++) { if (expanded[i]) setHandSlot(handCardIds[i], expanded[i]); } deckQueue = expanded.slice(3); const countEl = document.getElementById("deck-count"); if (countEl) countEl.textContent = deckQueue.length; } catch (e) { console.error("[Battlefield] Deck laden:", e); } })(); /* ═══════════════════════════════════════════════════════════ ZUG-TIMER ═══════════════════════════════════════════════════════════ */ const TURN_SECONDS = 30; const TT_CIRCUM = 2 * Math.PI * 18; let turnTimerInt = null; let turnSecsLeft = TURN_SECONDS; let amILeftPlayer = null; let isMyTurn = false; function startTurnTimer(activeName) { clearInterval(turnTimerInt); turnSecsLeft = TURN_SECONDS; updateTimerUI(turnSecsLeft, activeName); const wrap = document.getElementById("turn-timer-wrap"); if (wrap) wrap.style.display = "flex"; turnTimerInt = setInterval(() => { turnSecsLeft--; updateTimerUI(turnSecsLeft, activeName); if (turnSecsLeft <= 0) { clearInterval(turnTimerInt); if (isMyTurn) endMyTurn(); } }, 1000); } function stopTurnTimer() { clearInterval(turnTimerInt); const wrap = document.getElementById("turn-timer-wrap"); if (wrap) wrap.style.display = "none"; } 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 > 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 { num.style.color = "#fff"; num.style.animation = "none"; } } } /* ═══════════════════════════════════════════════════════════ ZUG-SYSTEM ═══════════════════════════════════════════════════════════ */ function setTurnState(myTurn, activeName) { isMyTurn = myTurn; const btn = document.getElementById("end-turn-btn"); if (!btn) return; handCardIds.forEach((id) => renderHandSlot(id)); if (myTurn) { btn.disabled = false; btn.textContent = "Zug beenden"; btn.style.opacity = "1"; document.getElementById("turn-indicator")?.remove(); startTurnTimer(activeName || "Du"); } else { btn.disabled = true; btn.style.opacity = "0.4"; showTurnIndicator(activeName); startTurnTimer(activeName || "Gegner"); } } function showTurnIndicator(activeName) { document.getElementById("turn-indicator")?.remove(); const ind = document.createElement("div"); ind.id = "turn-indicator"; ind.style.cssText = ` position:fixed;bottom:calc(var(--s)*200);left:50%;transform:translateX(-50%); background:linear-gradient(135deg,rgba(20,20,40,0.95),rgba(10,10,25,0.95)); border:1px solid rgba(200,160,60,0.5);border-radius:calc(var(--s)*8); color:rgba(255,215,80,0.75);font-family:'Cinzel',serif; font-size:calc(var(--s)*11);letter-spacing:calc(var(--s)*3); padding:calc(var(--s)*8) calc(var(--s)*20);z-index:100;pointer-events:none; text-transform:uppercase;box-shadow:0 4px 20px rgba(0,0,0,0.6);`; ind.textContent = activeName ? `\u23F3 ${activeName} ist am Zug` : "\u23F3 Gegner ist am Zug"; document.body.appendChild(ind); } function endMyTurn() { if (!isMyTurn) return; clearInterval(turnTimerInt); stopTurnTimer(); setTurnState(false); tickHandCooldowns(); drawNextCard(); socket.emit("end_turn", { matchId, slot: mySlot }); } document.getElementById("end-turn-btn")?.addEventListener("click", endMyTurn); /* ═══════════════════════════════════════════════════════════ KARTE AUF BOARD RENDERN ═══════════════════════════════════════════════════════════ */ /* * boardState speichert jetzt: { [slotId]: { card, owner } } * card = Karten-Objekt mit aktuellen Werten (defends wird live verändert) * owner = 'player1' | 'player2' */ const boardState = {}; /* ── Kampf + HP Animationen ────────────────────────────── */ (function () { const s = document.createElement("style"); s.textContent = ` @keyframes dmg-float { 0% { opacity:1; transform:translateX(-50%) translateY(0); } 100% { opacity:0; transform:translateX(-50%) translateY(-44px); } } @keyframes combat-fade { from { opacity:0; transform:translateX(-50%) scale(0.9); } to { opacity:1; transform:translateX(-50%) scale(1); } } `; document.head.appendChild(s); })(); /* ── Spieler-Farbe auf Slot oder Avatar anwenden ────────────────── */ function applyOwnerStyle(el, owner) { if (!el || !owner) return; const isLeft = owner === (window._leftSlot || "player1"); if (isLeft) { el.style.borderColor = "rgba(60, 140, 255, 0.95)"; el.style.boxShadow = "0 0 14px rgba(60, 140, 255, 0.5), inset 0 0 10px rgba(60,140,255,0.08)"; } else { el.style.borderColor = "rgba(220, 60, 60, 0.95)"; el.style.boxShadow = "0 0 14px rgba(220, 60, 60, 0.5), inset 0 0 10px rgba(220,60,60,0.08)"; } } function buildStatsHtml(card) { const atk = card.attack ?? null; const def = card.defends ?? null; const cd = card.cooldown ?? null; const rng = card.range ?? null; const rce = card.race ?? null; return `
${atk != null ? `${atk}` : ""} ${def != null ? `${def}` : ""} ${cd != null ? `${cd}` : ""} ${rng != null ? `${SVG_RANGE} ${rng}` : ""} ${rce != null ? `${SVG_RACE} ${rce}` : ""}
`; } function renderCardOnBoard(slotEl, card, owner) { const statsHtml = buildStatsHtml(card); slotEl.classList.add("slot-occupied"); applyOwnerStyle(slotEl, owner); slotEl.innerHTML = card.image ? `${statsHtml}` : `
\u2694\uFE0F${card.name}
${statsHtml}`; } function renderCardInSlot(slot, card) { if (!slot || !card) return; slot.classList.add("slot-occupied"); slot.innerHTML = card.image ? `${buildStatsHtml(card)}` : `
\u2694\uFE0F${card.name}
${buildStatsHtml(card)}`; } /* Slot leeren (nach Tod oder Bewegung) */ function clearBoardSlot(slotId) { delete boardState[slotId]; const el = document.getElementById(slotId); if (!el) return; el.classList.remove("slot-occupied"); el.style.borderColor = ""; el.style.boxShadow = ""; el.innerHTML = '\u2736' + slotId.split("-slot-")[1] + ""; } /* ═══════════════════════════════════════════════════════════ SOCKET & ARENA-JOIN ═══════════════════════════════════════════════════════════ */ const socket = io(); let myIngameName = null; function emitArenaJoin(me) { myIngameName = (me && (me.ingame_name || me.username || me.name || String(me.id))) || "Spieler"; const myNameEl = document.getElementById( amIPlayer1 ? "nameLeft" : "nameRight", ); if (myNameEl) myNameEl.textContent = myIngameName; console.log("[1v1] emitArenaJoin:", { matchId, slot: mySlot, name: myIngameName, }); socket.emit("arena_join", { matchId, slot: mySlot, accountId: me?.id ?? null, playerName: myIngameName, }); } function fetchAndJoin() { fetch("/arena/me") .then((r) => r.json()) .then((me) => emitArenaJoin(me)) .catch(() => { console.warn("[1v1] /arena/me fehlgeschlagen"); emitArenaJoin(null); }); } fetchAndJoin(); socket.on("connect", () => { console.log("[1v1] Socket connected:", socket.id); if (myIngameName && myIngameName !== "Spieler") { socket.emit("arena_join", { matchId, slot: mySlot, accountId: null, playerName: myIngameName, }); } else { fetchAndJoin(); } }); socket.on("connect_error", (err) => console.error("[1v1] connect_error:", err.message), ); const readyFallbackTimer = setTimeout(() => { console.warn("[1v1] Kein arena-Event nach 10s"); document.getElementById("connecting-overlay")?.remove(); const lo = document.getElementById("board-lock-overlay"); if (lo) lo.style.display = "flex"; }, 10000); /* ── Board-Sync ──────────────────────────────────────────── */ /* * boardSync-Einträge müssen vom Server nun { boardSlot, card, owner } enthalten. * Ältere Sync-Daten ohne owner werden als gegnerisch behandelt (Fallback). */ function applyBoardSync(cards) { if (!cards || !cards.length) return; cards.forEach((cd) => { const slotEl = document.getElementById(cd.boardSlot); if (!slotEl || boardState[cd.boardSlot]) return; const owner = cd.owner ?? cd.slot ?? "unknown"; boardState[cd.boardSlot] = { card: cd.card, owner }; renderCardOnBoard(slotEl, cd.card, cd.owner); }); console.log("[1v1] Board sync:", cards.length, "Karten"); } /* ── Socket Events ───────────────────────────────────────── */ socket.on("arena_opponent_joined", (data) => { console.log("[Arena] arena_opponent_joined:", data); clearTimeout(readyFallbackTimer); document.getElementById(amIPlayer1 ? "nameRight" : "nameLeft").textContent = data.name || "Gegner"; document.getElementById("connecting-overlay")?.remove(); const lo = document.getElementById("board-lock-overlay"); if (lo) lo.style.display = "flex"; }); socket.on("arena_ready", (data) => { console.log("[Arena] arena_ready:", data); clearTimeout(readyFallbackTimer); document.getElementById("connecting-overlay")?.remove(); if (data.boardSync) applyBoardSync(data.boardSync); if (data.hp && data.maxHp) applyHpFromEvent(data); const oppName = amIPlayer1 ? data.player2 : data.player1; const oppEl = document.getElementById(amIPlayer1 ? "nameRight" : "nameLeft"); const urlOppName = _urlParams.get("opponent"); if ( oppEl && oppName && !["Spieler", "Spieler 1", "Spieler 2", "Gegner"].includes(oppName) ) { oppEl.textContent = oppName; } else if ( oppEl && urlOppName && (!oppEl.textContent || oppEl.textContent === "Gegner") ) { oppEl.textContent = decodeURIComponent(urlOppName); } const myEl = document.getElementById(amIPlayer1 ? "nameLeft" : "nameRight"); if (myEl && myIngameName) myEl.textContent = myIngameName; const lo = document.getElementById("board-lock-overlay"); if (lo) lo.style.display = "flex"; }); socket.on("board_sync", (data) => { if (data.cards) applyBoardSync(data.cards); }); socket.on("turn_change", (data) => { document.getElementById("board-lock-overlay")?.remove(); document.getElementById("connecting-overlay")?.remove(); if (data.boardSync) applyBoardSync(data.boardSync); const activeSlot = data.activeSlot; const nowMyTurn = activeSlot === mySlot; const activeNameEl = document.getElementById( activeSlot === (window._leftSlot || "player1") ? "nameLeft" : "nameRight", ); const activeName = activeNameEl?.textContent || (nowMyTurn ? "Du" : "Gegner"); console.log(`[1v1] turn_change: ${activeSlot} | meinZug: ${nowMyTurn}`); setTurnState(nowMyTurn, activeName); if (data.hp && data.maxHp) applyHpFromEvent(data); }); socket.on("turn_started", (data) => { const myT = data.slot === mySlot; const nameEl = document.getElementById( data.slot === (window._leftSlot || "player1") ? "nameLeft" : "nameRight", ); setTurnState(myT, nameEl?.textContent || (myT ? "Du" : "Gegner")); }); /* ═══════════════════════════════════════════════════════════ BEREIT-SYSTEM ═══════════════════════════════════════════════════════════ */ let myReady = false; function handleBereit() { if (myReady) return; myReady = true; const btn = document.getElementById("bereit-btn"); btn.textContent = "\u2714 BEREIT"; btn.classList.add("bereit-clicked"); btn.disabled = true; socket.emit("player_ready", { matchId, slot: mySlot }); } const CIRCUMFERENCE = 2 * Math.PI * 34; const timerCircle = document.getElementById("timer-circle"); if (timerCircle) timerCircle.style.strokeDasharray = CIRCUMFERENCE; socket.on("ready_timer", (data) => { const num = document.getElementById("ready-timer-number"); if (num) num.textContent = data.remaining; if (timerCircle) { timerCircle.style.strokeDashoffset = CIRCUMFERENCE * (1 - data.remaining / 30); timerCircle.style.stroke = data.remaining > 15 ? "#27ae60" : data.remaining > 7 ? "#f39c12" : "#e74c3c"; } }); socket.on("ready_status", (data) => { console.log("[1v1] ready_status:", data); const pip1 = document.getElementById("pip-player1"); const pip2 = document.getElementById("pip-player2"); if (data.readySlots) { if (data.readySlots.includes("player1") && pip1) pip1.textContent = "\u2705 " + (document.getElementById("nameLeft")?.textContent || "Spieler 1"); if (data.readySlots.includes("player2") && pip2) pip2.textContent = "\u2705 " + (document.getElementById("nameRight")?.textContent || "Spieler 2"); } if (data.readyCount === 2) { const seed = matchId.split("").reduce((a, c) => a + c.charCodeAt(0), 0); const flip = seed % 2 === 1; const myName = myIngameName || "Spieler"; const oppEl = document.getElementById( amIPlayer1 ? "nameRight" : "nameLeft", ); const oppName = oppEl?.textContent || "Gegner"; const p1Name = amIPlayer1 ? myName : oppName; const p2Name = amIPlayer1 ? oppName : myName; const leftName = flip ? p2Name : p1Name; const rightName = flip ? p1Name : p2Name; document.getElementById("nameLeft").textContent = leftName; document.getElementById("nameRight").textContent = rightName; ["avLeft", "avRight"].forEach((avId) => { const av = document.getElementById(avId); const ph = av?.querySelector(".av-placeholder"); const name = avId === "avLeft" ? leftName : rightName; if (ph) ph.innerHTML = `
${name}
`; }); document.getElementById("board-lock-overlay")?.remove(); amILeftPlayer = flip ? mySlot === "player2" : mySlot === "player1"; // Avatare farbig umranden: links=blau, rechts=rot const _avL = document.getElementById("avLeft"); const _avR = document.getElementById("avRight"); if (_avL) { _avL.style.borderColor = "rgba(60, 140, 255, 0.95)"; _avL.style.boxShadow = "0 0 28px rgba(60, 140, 255, 0.55), inset 0 0 20px rgba(0,0,0,0.5)"; } if (_avR) { _avR.style.borderColor = "rgba(220, 60, 60, 0.95)"; _avR.style.boxShadow = "0 0 28px rgba(220, 60, 60, 0.55), inset 0 0 20px rgba(0,0,0,0.5)"; } 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"); } const leftSlot = flip ? "player2" : "player1"; window._leftSlot = leftSlot; console.log(`[1v1] linker Slot: ${leftSlot} | ich: ${mySlot}`); // Bereits auf dem Board liegende Karten jetzt korrekt einfärben // (boardSync kann vor _leftSlot ankommen) Object.entries(boardState).forEach(([slotId, entry]) => { const el = document.getElementById(slotId); if (el && entry?.owner) applyOwnerStyle(el, entry.owner); }); if (mySlot === "player1") { socket.emit("start_turn_request", { matchId, starterSlot: leftSlot }); } } }); /* ═══════════════════════════════════════════════════════════ DRAG & DROP ═══════════════════════════════════════════════════════════ */ function isMyZone(slotIndex) { const idx = Number(slotIndex); const iAmLeft = amILeftPlayer !== null ? amILeftPlayer : amIPlayer1; return iAmLeft ? idx <= 3 : idx >= 9; } function setDropZones(active) { document.querySelectorAll(".card-slot").forEach((slot) => { const idx = Number(slot.dataset.slotIndex); if (!isMyZone(idx)) return; if (active && !boardState[slot.id]) slot.classList.add("drop-zone-active"); else slot.classList.remove("drop-zone-active", "drop-zone-hover"); }); } let draggedCardSlotId = null; document.getElementById("handArea").addEventListener("dragstart", (e) => { const slot = e.target.closest("[data-card-slot-id]"); if (!slot || !isMyTurn) { e.preventDefault(); return; } draggedCardSlotId = slot.dataset.cardSlotId; slot.classList.add("dragging"); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", draggedCardSlotId); setTimeout(() => setDropZones(true), 0); }); document.getElementById("handArea").addEventListener("dragend", (e) => { const slot = e.target.closest("[data-card-slot-id]"); if (slot) slot.classList.remove("dragging"); setDropZones(false); draggedCardSlotId = null; }); ["row1", "row2"].forEach((rowId) => { const row = document.getElementById(rowId); row.addEventListener("dragover", (e) => { const slot = e.target.closest(".card-slot"); if (!slot) return; const idx = Number(slot.dataset.slotIndex); if (!isMyZone(idx) || boardState[slot.id]) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; row .querySelectorAll(".drop-zone-hover") .forEach((s) => s.classList.remove("drop-zone-hover")); slot.classList.add("drop-zone-hover"); }); row.addEventListener("dragleave", (e) => { const slot = e.target.closest(".card-slot"); if (slot) slot.classList.remove("drop-zone-hover"); }); row.addEventListener("drop", (e) => { e.preventDefault(); const slot = e.target.closest(".card-slot"); if (!slot) return; const idx = Number(slot.dataset.slotIndex); if (!isMyZone(idx) || boardState[slot.id]) return; const sourceId = draggedCardSlotId || e.dataTransfer.getData("text/plain"); if (!sourceId) return; const cardState = handSlotState[sourceId]; if (!cardState || cardState.currentCd > 0) return; handSlotState[sourceId] = null; renderHandSlot(sourceId); /* ── boardState mit owner speichern ─────────────────── */ boardState[slot.id] = { card: cardState.card, owner: mySlot }; renderCardOnBoard(slot, cardState.card, mySlot); slot.classList.remove("drop-zone-active", "drop-zone-hover"); socket.emit("card_played", { matchId, slot: mySlot, boardSlot: slot.id, row: slot.dataset.row, slotIndex: idx, card: cardState.card, }); console.log(`[1v1] Karte gespielt: ${cardState.card.name} -> ${slot.id}`); }); }); socket.on("card_played", (data) => { if (data.slot === mySlot) return; if (boardState[data.boardSlot]) return; const slotEl = document.getElementById(data.boardSlot); if (!slotEl) { console.warn("[1v1] card_played: Slot fehlt:", data.boardSlot); return; } /* ── boardState mit owner des Gegners speichern ─────── */ boardState[data.boardSlot] = { card: data.card, owner: data.slot }; renderCardOnBoard(slotEl, data.card, data.slot); console.log("[1v1] Gegner Karte:", data.card?.name, "->", data.boardSlot); }); /* ═══════════════════════════════════════════════════════════ KAMPFPHASE – CLIENT-SEITIGE VERARBEITUNG Der Server sendet nach end_turn ein 'combat_phase'-Event mit: { events: [...], finalBoard: [...] } ═══════════════════════════════════════════════════════════ */ /* Timing (ms) zwischen einzelnen Kampf-Events */ const COMBAT_DELAY_MOVE = 350; const COMBAT_DELAY_ATTACK = 450; const COMBAT_DELAY_DIE = 300; /* Kampf-Log-Banner (erscheint kurz über dem Board) */ function showCombatBanner(text, color = "#f0d060") { const old = document.getElementById("combat-banner"); if (old) old.remove(); const el = document.createElement("div"); el.id = "combat-banner"; el.style.cssText = ` position:fixed;top:18%;left:50%;transform:translateX(-50%); background:rgba(10,8,5,0.92);border:1px solid ${color}; border-radius:8px;padding:7px 22px; color:${color};font-family:'Cinzel',serif;font-size:12px; letter-spacing:2px;z-index:500;pointer-events:none; animation:combat-fade 0.25s ease;`; el.textContent = text; document.body.appendChild(el); setTimeout(() => el.remove(), 1600); } /* Kurzes Aufleuchten eines Slots (Angriff / Tod) */ function flashSlot(slotId, color, durationMs = 300) { const el = document.getElementById(slotId); if (!el) return; el.style.transition = `box-shadow ${durationMs / 2}ms ease`; el.style.boxShadow = `0 0 18px 6px ${color}`; setTimeout(() => { el.style.boxShadow = ""; }, durationMs); } /* Karte bewegen: Slot A → Slot B */ function applyMoveEvent(ev) { const entry = boardState[ev.from]; if (!entry) return; const fromEl = document.getElementById(ev.from); const toEl = document.getElementById(ev.to); if (!fromEl || !toEl) return; /* boardState aktualisieren */ delete boardState[ev.from]; boardState[ev.to] = entry; /* DOM aktualisieren */ fromEl.classList.remove("slot-occupied"); fromEl.innerHTML = '\u2736' + ev.from.split("-slot-")[1] + ""; renderCardOnBoard(toEl, entry.card, entry.owner); console.log(`[Combat] Bewegt: ${entry.card.name} ${ev.from} → ${ev.to}`); } /* Angriff: Verteidigung des Ziels aktualisieren */ function applyAttackEvent(ev) { const attackerEntry = boardState[ev.from]; const targetEntry = boardState[ev.to]; if (!attackerEntry || !targetEntry) return; /* Verteidigung im boardState aktualisieren */ targetEntry.card = { ...targetEntry.card, defends: ev.remainingDef }; /* Visuelles Feedback */ flashSlot(ev.from, "rgba(255,200,50,0.7)", 250); // Angreifer leuchtet gold flashSlot(ev.to, "rgba(220,50,50,0.85)", 350); // Ziel leuchtet rot /* Verteidigungswert im DOM sofort aktualisieren */ const targetEl = document.getElementById(ev.to); if (targetEl) { const defEl = targetEl.querySelector(".cs-def"); if (defEl) { defEl.textContent = ev.remainingDef; defEl.style.color = ev.remainingDef <= 2 ? "#e74c3c" : ""; } } console.log( `[Combat] Angriff: ${attackerEntry.card.name} → ${targetEntry.card.name} (${ev.damage} Schaden, verbleibend: ${ev.remainingDef})`, ); } /* Karte stirbt */ function applyDieEvent(ev) { const entry = boardState[ev.slotId]; const name = entry?.card?.name ?? "???"; flashSlot(ev.slotId, "rgba(200,50,50,0.9)", 500); setTimeout(() => { clearBoardSlot(ev.slotId); console.log(`[Combat] Gestorben: ${name} auf ${ev.slotId}`); }, 200); } /* Finales Board nach Kampfphase vollständig anwenden (Sicherheits-Sync) */ function applyFinalBoard(finalBoard) { /* Erst alles leeren */ Object.keys(boardState).forEach((sid) => clearBoardSlot(sid)); /* Dann neuen Zustand setzen */ if (!finalBoard || !finalBoard.length) return; finalBoard.forEach((entry) => { const slotEl = document.getElementById(entry.boardSlot); if (!slotEl) return; boardState[entry.boardSlot] = { card: entry.card, owner: entry.owner }; renderCardOnBoard(slotEl, entry.card, entry.owner); }); } /* Haupt-Handler für combat_phase */ socket.on("combat_phase", (data) => { const events = data.events ?? []; const finalBoard = data.finalBoard ?? []; console.log(`[Combat] Kampfphase startet: ${events.length} Events`); showCombatBanner("\u2694\uFE0F KAMPFPHASE"); if (events.length === 0) { /* Keine Karten im Spiel → direkt final sync */ applyFinalBoard(finalBoard); return; } /* Events sequenziell abarbeiten */ let delay = 600; // kurze Pause nach Banner events.forEach((ev) => { const thisDelay = delay; if (ev.type === "move") { delay += COMBAT_DELAY_MOVE; setTimeout(() => applyMoveEvent(ev), thisDelay); } else if (ev.type === "attack") { delay += COMBAT_DELAY_ATTACK; setTimeout(() => applyAttackEvent(ev), thisDelay); } else if (ev.type === "die") { delay += COMBAT_DELAY_DIE; setTimeout(() => applyDieEvent(ev), thisDelay); } }); /* Nach allen Events → finaler Sync (stellt sicher dass alles stimmt) */ delay += 500; setTimeout(() => { applyFinalBoard(finalBoard); console.log("[Combat] Kampfphase abgeschlossen, Board synchronisiert."); }, delay); }); /* ═══════════════════════════════════════════════════════════ AVATAR HP – Anzeige & Animationen ═══════════════════════════════════════════════════════════ */ function updateHpDisplay(slot, currentHp, maxHp) { const isLeft = slot === (window._leftSlot || "player1"); const orbEl = document.getElementById(isLeft ? "orbLeft" : "orbRight"); const hpEl = document.getElementById(isLeft ? "hpLeft" : "hpRight"); const avEl = document.getElementById(isLeft ? "avLeft" : "avRight"); if (orbEl) orbEl.textContent = currentHp; if (hpEl) hpEl.textContent = currentHp; /* Farbe je nach HP-Prozent */ const pct = maxHp > 0 ? currentHp / maxHp : 0; const color = pct > 0.5 ? "#e74c3c" : pct > 0.25 ? "#e67e22" : "#8b0000"; if (orbEl) { orbEl.style.background = `radial-gradient(circle at 40% 35%, ${color}, #3a0000)`; orbEl.style.boxShadow = `0 0 12px ${color}cc`; } /* Avatar-Schüttelanimation + roter Flash */ if (avEl) { avEl.style.transition = "transform 0.07s ease"; avEl.style.transform = "scale(1.07)"; setTimeout(() => { avEl.style.transform = "scale(0.96)"; }, 70); setTimeout(() => { avEl.style.transform = ""; }, 150); const flash = document.createElement("div"); flash.style.cssText = "position:absolute;inset:0;border-radius:inherit;background:rgba(220,50,50,0.4);z-index:20;pointer-events:none;"; avEl.appendChild(flash); setTimeout(() => flash.remove(), 320); } } function applyHpFromEvent(data) { if (!data.hp || !data.maxHp) return; ["player1", "player2"].forEach((slot) => { if (data.hp[slot] != null) updateHpDisplay(slot, data.hp[slot], data.maxHp[slot] ?? data.hp[slot]); }); } /* Initiale HP beim Spielstart */ socket.on("hp_init", (data) => { applyHpFromEvent(data); console.log("[HP] Init:", data.hp); }); /* Avatar getroffen */ socket.on("avatar_damaged", (data) => { const { slot, damage, remainingHp, maxHp } = data; updateHpDisplay(slot, remainingHp, maxHp); console.log(`[HP] ${slot} -${damage} → ${remainingHp}/${maxHp}`); /* Schadens-Zahl einblenden */ const isLeft = slot === (window._leftSlot || "player1"); const avEl = document.getElementById(isLeft ? "avLeft" : "avRight"); if (avEl) { const dmg = document.createElement("div"); dmg.textContent = `-${damage}`; dmg.style.cssText = ` position:absolute;top:15%;left:50%;transform:translateX(-50%); font-family:'Cinzel',serif;font-size:calc(var(--s)*24);font-weight:700; color:#e74c3c;text-shadow:0 2px 10px rgba(0,0,0,0.95); pointer-events:none;z-index:30; animation:dmg-float 1s ease forwards;`; avEl.appendChild(dmg); setTimeout(() => dmg.remove(), 1000); } }); /* ═══════════════════════════════════════════════════════════ AUFGEBEN ═══════════════════════════════════════════════════════════ */ function closeToArena() { if (window.parent && window.parent !== window) { window.parent.document.getElementById("arena-backdrop")?.remove(); window.parent.document.getElementById("arena-popup")?.remove(); } else { window.location.href = "/launcher"; } } function handleAufgeben() { const modal = document.createElement("div"); modal.id = "surrender-modal"; modal.style.cssText = "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;"; modal.innerHTML = `
\uD83C\uDFF3\uFE0F
AUFGEBEN?

Willst du wirklich aufgeben?
Du erh\u00E4ltst keine Punkte f\u00FCr dieses Match.

`; document.body.appendChild(modal); document .getElementById("surrender-no") .addEventListener("click", () => modal.remove()); document.getElementById("surrender-yes").addEventListener("click", () => { modal.remove(); socket.emit("player_surrender", { matchId, slot: mySlot }); showSurrenderMessage(false); }); } function showSurrenderMessage(iWon) { const overlay = document.createElement("div"); overlay.id = "surrender-result-overlay"; overlay.style.cssText = "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.92);display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'Cinzel',serif;animation:fadeIn 0.4s ease;"; overlay.innerHTML = iWon ? `
\uD83C\uDFC6
SIEG!
Dein Gegner hat aufgegeben.
Du erh\u00E4ltst die Arena-Punkte.
` : `
\uD83C\uDFF3\uFE0F
AUFGEGEBEN
Du hast das Match aufgegeben.
Keine Punkte f\u00FCr dieses Match.
`; document.body.appendChild(overlay); document .getElementById("surrender-close-btn") .addEventListener("click", closeToArena); setTimeout(closeToArena, 8000); } socket.on("player_surrendered", (data) => { if (data.slot !== mySlot) showSurrenderMessage(true); }); /* ═══════════════════════════════════════════════════════════ MATCH-ERGEBNIS OVERLAY ═══════════════════════════════════════════════════════════ */ function showResultOverlay(won, data) { const overlay = document.getElementById("match-result-overlay"); const titleEl = document.getElementById("result-title"); const pointsEl = document.getElementById("result-points"); titleEl.textContent = won ? "\u2694\uFE0F SIEG!" : "\uD83D\uDC80 NIEDERLAGE"; titleEl.className = "result-title " + (won ? "win" : "lose"); pointsEl.textContent = "Punkte werden berechnet\u2026"; overlay.classList.add("show"); if (data) updateResultWithPoints(data); } function updateResultWithPoints(data) { /* Verhindert doppeltes Aufrufen */ if (document.getElementById("match-end-overlay")) return; const img = data.won ? "/images/victory.jpeg" : "/images/defeat.jpg"; const awarded = data.awarded ?? 0; /* Fade-in Keyframe einmalig anlegen */ if (!document.getElementById("_matchEndStyle")) { const st = document.createElement("style"); st.id = "_matchEndStyle"; st.textContent = ` @keyframes matchEndFadeIn { from{opacity:0} to{opacity:1} } @keyframes matchPtsSlideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }`; document.head.appendChild(st); } /* Punkte-Text aufbauen */ const ptsLine = awarded > 0 ? "+" + awarded + " Arena-Punkte" : "Keine Punkte"; const lvlLine = data.level_up ? "⬆ LEVEL UP! → Level " + data.new_level : ""; const overlay = document.createElement("div"); overlay.id = "match-end-overlay"; overlay.style.cssText = "position:fixed;inset:0;z-index:9999;background:#000;animation:matchEndFadeIn 0.5s ease forwards;"; overlay.innerHTML = `
${ptsLine}
${lvlLine ? `
${lvlLine}
` : ""}
`; document.body.appendChild(overlay); /* Aktuellen Punktestand nachladen und anzeigen */ fetch("/api/points/me") .then((r) => r.json()) .then((me) => { const info = overlay.querySelector("div > div:first-child"); if (info && awarded > 0) { info.insertAdjacentHTML( "afterend", `
Gesamt: ${me.arena_points} Pts • Level ${me.level}
`, ); } }) .catch(() => {}); /* Nach 3 Sekunden zur Arena weiterleiten */ setTimeout(() => closeToArena(), 3000); } function closePopup() { closeToArena(); } socket.on("match_result", (data) => updateResultWithPoints(data)); socket.on("match_cancelled", () => closePopup()); document .getElementById("result-close-btn") .addEventListener("click", closePopup); /* ═══════════════════════════════════════════════════════════ AVATAR ═══════════════════════════════════════════════════════════ */ 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"; document .getElementById(parentId) ?.querySelector(".av-placeholder") ?.style.setProperty("display", "none"); }; r.readAsDataURL(file); } /* ═══════════════════════════════════════════════════════════ EVENT-LISTENER ═══════════════════════════════════════════════════════════ */ document.getElementById("bereit-btn")?.addEventListener("click", handleBereit); document .getElementById("aufgeben-btn") ?.addEventListener("click", handleAufgeben); document .getElementById("fileInputLeft") ?.addEventListener("change", function () { loadAvatar(this, "avImgL", "avLeft"); }); document .getElementById("fileInputRight") ?.addEventListener("change", function () { loadAvatar(this, "avImgR", "avRight"); });