dok/views/1v1-battlefield.ejs
2026-04-13 10:17:35 +01:00

1460 lines
57 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title || "Spielfeld" %></title>
<link
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/css/1v1.css" />
<style>
#match-result-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.88);
z-index: 9999;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Cinzel", serif;
animation: fadeIn 0.4s ease;
}
#match-result-overlay.show {
display: flex;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.result-title {
font-size: 48px;
letter-spacing: 6px;
margin-bottom: 12px;
text-shadow: 0 0 30px currentColor;
}
.result-title.win {
color: #f0d060;
}
.result-title.lose {
color: #c84040;
}
.result-points {
font-size: 22px;
color: #c8a860;
margin-bottom: 6px;
letter-spacing: 2px;
}
.result-levelup {
font-size: 18px;
color: #60e060;
margin-bottom: 20px;
letter-spacing: 2px;
animation: pulse 1s ease-in-out infinite;
}
.result-progress-wrap {
width: 320px;
margin-bottom: 24px;
}
.result-progress-label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #a08060;
margin-bottom: 6px;
}
.result-progress-track {
height: 10px;
background: rgba(255, 255, 255, 0.08);
border-radius: 5px;
overflow: hidden;
}
.result-progress-fill {
height: 100%;
background: linear-gradient(90deg, #c8960c, #f0d060);
border-radius: 5px;
transition: width 1s ease;
}
.result-close-btn {
background: linear-gradient(#4a3010, #2a1a08);
border: 2px solid #c8960c;
color: #f0d9a6;
font-family: "Cinzel", serif;
font-size: 14px;
padding: 10px 30px;
border-radius: 8px;
cursor: pointer;
letter-spacing: 2px;
margin-top: 10px;
transition: 0.2s;
}
.result-close-btn:hover {
border-color: #f0d060;
filter: brightness(1.2);
}
</style>
<style>
/* ── Deck-Stapel ────────────────────────── */
.hand-slot-deck {
position: relative;
cursor: pointer;
}
.deck-stack-wrap {
position: relative;
width: 100%;
height: calc(100% - 18px);
}
.deck-card-back,
.deck-card-top {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 7px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
.deck-shadow-3 {
transform: translate(-4px, -4px);
}
.deck-shadow-2 {
transform: translate(-2px, -2px);
}
.deck-shadow-1 {
transform: translate(-1px, -1px);
}
.deck-card-top {
transform: translate(0, 0);
border: 1px solid rgba(200, 150, 42, 0.5);
}
.deck-count {
position: absolute;
bottom: 2px;
left: 0;
right: 0;
text-align: center;
font-family: "Cinzel", serif;
font-size: 10px;
color: #f0d060;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.hand-slot-card {
border: 1px solid rgba(200, 150, 42, 0.4) !important;
}
.hand-slot-card > img {
border-radius: 0;
overflow: hidden;
}
.hand-slot-card:hover {
border-color: rgba(200, 150, 42, 0.9) !important;
transform: translateY(-4px);
transition: transform 0.15s;
}
/* ── Reichweite & Laufen Badges (Hand + Board) ── */
.cs-range,
.cs-race {
position: absolute;
display: flex;
align-items: center;
gap: 2px;
padding: 1px 4px;
border-radius: 20px;
font-family: "Cinzel", serif;
font-size: 8px;
font-weight: bold;
line-height: 1;
z-index: 6;
pointer-events: none;
}
.cs-range {
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);
border: 1px solid #7de87d;
color: #7de87d;
}
</style>
</head>
<body>
<div id="connecting-overlay">
<div class="spinner"></div>
<div>Warte auf Gegner…</div>
<p>Verbindung wird hergestellt</p>
</div>
<div class="board">
<div class="top-bar">
<div class="game-title"><%= title || "Spielfeld" %></div>
<!-- Zug-Timer mittig -->
<div id="turn-timer-wrap" style="display: none">
<svg id="turn-timer-svg" viewBox="0 0 44 44" width="44" height="44">
<circle cx="22" cy="22" r="18" class="tt-track" />
<circle cx="22" cy="22" r="18" class="tt-fill" id="tt-circle" />
</svg>
<div id="turn-timer-num">20</div>
</div>
<div class="top-icons">
<div class="top-icon">⚙</div>
<div class="top-icon">🗺</div>
<div class="top-icon">📖</div>
<div class="top-icon">🏆</div>
</div>
<div class="top-bar-actions">
<button class="end-turn-btn" id="end-turn-btn" disabled>
Zug beenden
</button>
<button class="aufgeben-btn" id="aufgeben-btn">🏳 Aufgeben</button>
</div>
</div>
<div id="board-lock-overlay" style="display: none">
<div id="ready-timer-box">
<div id="ready-timer-label">Bereit machen</div>
<div id="ready-timer-ring">
<svg viewBox="0 0 80 80" width="80" height="80">
<circle cx="40" cy="40" r="34" class="timer-track" />
<circle
cx="40"
cy="40"
r="34"
class="timer-fill"
id="timer-circle"
/>
</svg>
<div id="ready-timer-number">30</div>
</div>
<div id="ready-timer-sub">Beide Spieler müssen BEREIT klicken</div>
<div id="ready-status-row">
<div class="ready-pip" id="pip-player1">⬜ Spieler 1</div>
<div class="ready-pip" id="pip-player2">⬜ Spieler 2</div>
</div>
<button class="bereit-btn" id="bereit-btn">✔ BEREIT</button>
</div>
</div>
<div class="avatar avatar-left" id="avLeft">
<input type="file" accept="image/*" id="fileInputLeft" />
<img id="avImgL" class="av-img" />
<div class="av-placeholder" id="avPhL">
<div class="av-icon">⚔</div>
</div>
<div class="av-name" id="nameLeft"><%= player1 || "Spieler 1" %></div>
<div class="av-stats">
<div class="stat hp">
<span class="s-icon">❤</span
><span class="s-val" id="hpLeft"><%= player1hp || 20 %></span>
</div>
<div class="stat mana">
<span class="s-icon">💧</span
><span class="s-val" id="manaLeft"><%= player1mana || 3 %></span>
</div>
</div>
<div class="hp-orb" id="orbLeft"><%= player1hp || 15 %></div>
</div>
<div class="avatar avatar-right" id="avRight">
<input type="file" accept="image/*" id="fileInputRight" />
<img id="avImgR" class="av-img" />
<div class="av-placeholder" id="avPhR">
<div class="av-icon">🛡</div>
</div>
<div class="av-name" id="nameRight"><%= player2 || "Gegner" %></div>
<div class="av-stats">
<div class="stat hp">
<span class="s-icon">❤</span
><span class="s-val" id="hpRight"><%= player2hp || 20 %></span>
</div>
<div class="stat mana">
<span class="s-icon">💧</span
><span class="s-val" id="manaRight"><%= player2mana || 3 %></span>
</div>
</div>
<div class="hp-orb" id="orbRight"><%= player2hp || 15 %></div>
</div>
<div class="card-area">
<div class="row-label"></div>
<div class="card-row" id="row1"></div>
<div class="row-label"></div>
<div class="card-row" id="row2"></div>
</div>
<div class="bottom-bar">
<div class="hand-area" id="handArea"></div>
<div class="action-hud">
<div class="action-row">
<div class="action-btn" title="Angriff">⚔</div>
<div class="action-btn" title="Magie">✨</div>
<div class="action-btn" title="Verteidigung">🛡</div>
</div>
<div class="action-row">
<div class="action-btn" title="Heilen">💊</div>
<div class="action-btn" title="Karte ziehen">🃏</div>
<div class="action-btn" title="Einstellungen">⚙</div>
</div>
</div>
</div>
</div>
<!-- Match-Ergebnis Overlay -->
<div id="match-result-overlay">
<div class="result-title" id="result-title"></div>
<div class="result-points" id="result-points"></div>
<div
class="result-levelup"
id="result-levelup"
style="display: none"
></div>
<div
class="result-progress-wrap"
id="result-progress-wrap"
style="display: none"
>
<div class="result-progress-label">
<span id="result-level-label">Level</span>
<span id="result-pts-label"></span>
</div>
<div class="result-progress-track">
<div
class="result-progress-fill"
id="result-progress-fill"
style="width: 0%"
></div>
</div>
</div>
<button class="result-close-btn" id="result-close-btn">Schließen</button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
/* ── 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 =
'<span class="slot-icon">✦</span><span class="slot-num">' +
i +
"</span>";
row.appendChild(s);
}
});
const hand = document.getElementById("handArea");
// ── Slot 1: Deck-Stapel (Rückseite oben) ─────────────
const deckSlot = document.createElement("div");
deckSlot.className = "hand-slot hand-slot-deck";
deckSlot.id = "deck-stack";
deckSlot.title = "Dein Deck";
deckSlot.innerHTML = `
<div class="deck-stack-wrap">
<img class="deck-card-back deck-shadow-3" src="/images/items/rueckseite.png">
<img class="deck-card-back deck-shadow-2" src="/images/items/rueckseite.png">
<img class="deck-card-back deck-shadow-1" src="/images/items/rueckseite.png">
<img class="deck-card-top" src="/images/items/rueckseite.png">
</div>
<span class="deck-count" id="deck-count">—</span>`;
hand.appendChild(deckSlot);
// ── Slots 24: aufgedeckte Handkarten (leer bis API lädt) ───
const handCardIds = ["hand-card-1", "hand-card-2", "hand-card-3"];
handCardIds.forEach((id) => {
const s = document.createElement("div");
s.className = "hand-slot hand-slot-card";
s.id = id;
s.innerHTML = '<span class="hs-icon">🃏</span>';
hand.appendChild(s);
});
// ── Slots 58: leere Slots ───────────────────────────
for (let i = 0; i < 4; i++) {
const s = document.createElement("div");
s.className = "hand-slot";
s.innerHTML = '<span class="hs-icon">🃏</span>';
hand.appendChild(s);
}
// ══════════════════════════════════════════════════════
// HAND & DECK SYSTEM
// ══════════════════════════════════════════════════════
let deckQueue = [];
// State pro Slot: { card, currentCd } oder null = leer
const handSlotState = {};
handCardIds.forEach((id) => {
handSlotState[id] = null;
});
/* ── 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>`;
/* ── Slot rendern ──────────────────────────────────── */
function renderHandSlot(id) {
const slot = document.getElementById(id);
const state = handSlotState[id];
if (!slot) return;
if (!state) {
slot.style.backgroundImage = '';
slot.innerHTML = '<span class="hs-icon">🃏</span>';
slot.classList.remove("hand-slot-ready", "hand-slot--filled");
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 = `
<div class="card-stat-overlay">
${atkVal != null ? `<span class="cs-atk">${atkVal}</span>` : ""}
${defVal != null ? `<span class="cs-def">${defVal}</span>` : ""}
${
card.cooldown != null
? `<span class="cs-cd ${isReady ? "cs-cd-ready" : ""}">${isReady ? "✓" : currentCd}</span>`
: ""
}
${rngVal != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</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>`
: "";
// ── Karte als CSS background-image auf dem Slot ──────────────────────
// Kein <img>-Element → keine Flex-Höhen-Probleme, kein z-index-Konflikt.
// background-color (#0a0805) deckt transparente PNG-Bereiche solid ab.
if (card.image) {
slot.style.backgroundImage = 'none';
} else {
slot.style.backgroundImage = 'none';
}
slot.innerHTML = card.image
? `<img class="hand-card-img" src="/images/cards/${card.image}" onerror="this.src='/images/items/rueckseite.png'" title="${card.name}">${statsHtml}${readyBadge}`
: `<div style="width:100%;height:100%;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:4px;font-family:Cinzel,serif;">
<span style="font-size:18px;">⚔️</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>${statsHtml}${readyBadge}`;
slot.classList.toggle("hand-slot-ready", isReady);
slot.classList.add("hand-slot--filled");
// Drag & Drop: nur wenn Karte bereit UND mein Zug
if (isReady && isMyTurn) {
slot.draggable = true;
slot.dataset.cardSlotId = id;
} else {
slot.draggable = false;
delete slot.dataset.cardSlotId;
}
}
/* ── Karte in Hand-Slot legen ──────────────────────── */
function setHandSlot(id, card) {
handSlotState[id] = { card, currentCd: card.cooldown ?? 0 };
renderHandSlot(id);
}
/* ── Karte vom Deck ziehen ─────────────────────────── */
function drawNextCard() {
if (deckQueue.length === 0) return;
const freeSlot = handCardIds.find((id) => handSlotState[id] === null);
if (!freeSlot) return; // alle Slots belegt → Karte bleibt im Deck
const card = deckQueue.shift();
setHandSlot(freeSlot, card);
const countEl = document.getElementById("deck-count");
if (countEl) countEl.textContent = deckQueue.length;
}
/* ── Cooldowns beim Zug-Ende reduzieren ────────────── */
function tickHandCooldowns() {
handCardIds.forEach((id) => {
const state = handSlotState[id];
if (!state) return;
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 cardsRes = await fetch("/api/decks/" + deckId + "/cards");
if (!cardsRes.ok) return;
const cards = await cardsRes.json();
// Karten nach amount auffalten
const expanded = [];
cards.forEach((card) => {
for (let i = 0; i < (card.amount ?? 1); i++) expanded.push(card);
});
// Mischen (Fisher-Yates)
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]];
}
// Erste 3 in die Hand
handCardIds.forEach((id, i) => {
if (expanded[i]) setHandSlot(id, expanded[i]);
});
// Rest als Deck-Queue
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 (30 Sekunden) läuft für BEIDE Spieler sichtbar
══════════════════════════════════════════════════════ */
const TURN_SECONDS = 30;
const TT_CIRCUM = 2 * Math.PI * 18; // r=18
let turnTimerInt = null;
let turnSecsLeft = TURN_SECONDS;
// activeName: Name des Spielers der gerade dran ist
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);
// Nur der aktive Spieler beendet den Zug
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
══════════════════════════════════════════════════════ */
// Bin ich der linke Spieler? Wird nach Seitenzuweisung gesetzt.
let amILeftPlayer = null;
let isMyTurn = false;
// 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;
// Alle Handkarten sofort neu rendern damit Draggable + Badge korrekt sind
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
? `⏳ ${activeName} ist am Zug`
: "⏳ Gegner ist am Zug";
document.body.appendChild(ind);
}
/* ── Zug beenden: per Button oder Timer-Ablauf ── */
function endMyTurn() {
if (!isMyTurn) return;
clearInterval(turnTimerInt);
stopTurnTimer();
// ERST isMyTurn=false setzen, DANN Karten updaten
// so werden die neuen/aktualisierten Karten gleich korrekt (ohne Badge/Draggable) gerendert
setTurnState(false);
tickHandCooldowns(); // CDs runtercountent und Slots neu rendern (isMyTurn ist jetzt false)
drawNextCard(); // neue Karte ziehen und Slot rendern (isMyTurn ist jetzt false)
socket.emit("end_turn", { matchId, slot: mySlot });
}
document.getElementById("end-turn-btn")?.addEventListener("click", () => {
endMyTurn();
});
/* ── 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 hasAtk = atkVal != null;
const hasDef = defVal != null;
const hasCd = cdVal != null;
const hasRng = rngVal != null;
const hasRce = rceVal != null;
const statsHtml =
hasAtk || hasDef || hasCd || hasRng || hasRce
? `
<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>` : ""}
${hasRng ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ""}
${hasRce ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
</div>`
: "";
// note: SVG_RACE is defined as the walking icon above
slot.classList.add("slot-occupied");
slot.innerHTML = card.image
? `<img src="/images/cards/${card.image}"
onerror="this.src='/images/items/rueckseite.png'"
title="${card.name}"
style="width:100%;height:100%;object-fit:cover;border-radius:7px;display:block;">
${statsHtml}`
: `<div style="display:flex;flex-direction:column;align-items:center;
justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;">
<span style="font-size:18px;">⚔️</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>
${statsHtml}`;
}
/* ── Match-Daten ────────────────────────────────────── */
const urlParams = new URLSearchParams(window.location.search);
const matchId = urlParams.get("match") || "<%= matchId || '' %>";
const mySlot = urlParams.get("slot") || "<%= mySlot || 'player1' %>";
const amIPlayer1 = mySlot === "player1";
// 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",
);
if (oppEl) oppEl.textContent = decodeURIComponent(opponentNameFromUrl);
}
/* ── Socket ─────────────────────────────────────────── */
const socket = io();
/* Account-ID laden dann arena_join senden */
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 join ohne Account");
emitArenaJoin(null);
});
}
// Sofort emittieren (socket.io puffert bis verbunden)
fetchAndJoin();
// Sicherheits-Retry: nur erneut joinen wenn Name schon bekannt ist
// (verhindert dass ein fehlgeschlagener /arena/me-Fetch den Namen auf "Spieler" überschreibt)
socket.on("connect", () => {
console.log("[1v1] Socket connected:", socket.id);
if (myIngameName && myIngameName !== "Spieler") {
// Name bereits bekannt → direkt arena_join senden, kein erneuter Fetch nötig
socket.emit("arena_join", {
matchId,
slot: mySlot,
accountId: null,
playerName: myIngameName,
});
} else {
// Name noch unbekannt (erster Connect oder Fetch fehlgeschlagen) → fetch wiederholen
fetchAndJoin();
}
});
socket.on("connect_error", (err) => {
console.error("[1v1] Socket connect_error:", err.message);
});
// Fallback: Falls nach 10s kein arena-Event → Bereit-Box erzwingen
const readyFallbackTimer = setTimeout(() => {
console.warn("[1v1] Kein arena-Event nach 10s Bereit-Box erzwingen");
document.getElementById("connecting-overlay")?.remove();
const lockOverlay = document.getElementById("board-lock-overlay");
if (lockOverlay) lockOverlay.style.display = "flex";
}, 10000);
/* ── Gegner verbunden ───────────────────────────────── */
socket.on("arena_opponent_joined", (data) => {
console.log("[Arena] arena_opponent_joined:", data);
clearTimeout(readyFallbackTimer);
const name = data.name || "Gegner";
document.getElementById(
amIPlayer1 ? "nameRight" : "nameLeft",
).textContent = name;
document.getElementById("connecting-overlay")?.remove();
const lockOverlay = document.getElementById("board-lock-overlay");
if (lockOverlay) lockOverlay.style.display = "flex";
});
socket.on("arena_ready", (data) => {
console.log("[Arena] arena_ready:", data);
clearTimeout(readyFallbackTimer);
document.getElementById("connecting-overlay")?.remove();
// Board sync falls Karten bereits gespielt wurden
if (data.boardSync) applyBoardSync(data.boardSync);
// Gegner-Name: URL-Parameter hat Vorrang (aus match_found, 100% korrekt)
// Server-Daten nur als Fallback wenn URL-Name fehlt
const oppName = amIPlayer1 ? data.player2 : data.player1;
const oppEl = document.getElementById(
amIPlayer1 ? "nameRight" : "nameLeft",
);
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)
) {
oppEl.textContent = oppName;
} else if (
oppEl &&
urlOppName &&
(!currentOppName || currentOppName === "Gegner")
) {
oppEl.textContent = decodeURIComponent(urlOppName);
}
// Eigenen Namen sichern falls noch nicht gesetzt
const myEl = document.getElementById(
amIPlayer1 ? "nameLeft" : "nameRight",
);
if (myEl && myIngameName) myEl.textContent = myIngameName;
const lockOverlay = document.getElementById("board-lock-overlay");
if (lockOverlay) lockOverlay.style.display = "flex";
});
/* ── Board synchronisieren (aus boardSync Array) ───────── */
function applyBoardSync(cards) {
if (!cards || !cards.length) return;
cards.forEach((cardData) => {
const slotEl = document.getElementById(cardData.boardSlot);
if (!slotEl || boardState[cardData.boardSlot]) return;
boardState[cardData.boardSlot] = cardData.card;
renderCardOnBoard(slotEl, cardData.card);
});
console.log("[1v1] Board sync:", cards.length, "Karten");
}
/* ── Board-Sync bei Reconnect ──────────────────────────── */
socket.on("board_sync", (data) => {
if (data.cards) applyBoardSync(data.cards);
});
/* ── Server: Zugwechsel ──────────────────────────────── */
socket.on("turn_change", (data) => {
document.getElementById("board-lock-overlay")?.remove();
document.getElementById("connecting-overlay")?.remove();
if (data.boardSync) applyBoardSync(data.boardSync);
// 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) => {
const myT = data.slot === mySlot;
const nameEl = document.getElementById(
data.slot === "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 = "✔ 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 empfangen:", data);
const pip1 = document.getElementById("pip-player1");
const pip2 = document.getElementById("pip-player2");
if (data.readySlots) {
if (data.readySlots.includes("player1") && pip1)
pip1.textContent =
"✅ " +
(document.getElementById("nameLeft")?.textContent || "Spieler 1");
if (data.readySlots.includes("player2") && pip2)
pip2.textContent =
"✅ " +
(document.getElementById("nameRight")?.textContent ||
"Spieler 2");
}
if (data.readyCount === 2) {
// ── Zufällige Seitenzuweisung ──────────────────────
const seed = matchId
.split("")
.reduce((a, c) => a + c.charCodeAt(0), 0);
const flip = seed % 2 === 1;
// Eigenen Namen immer aus myIngameName, Gegner aus DOM
const myName = myIngameName || "Spieler";
const oppEl = document.getElementById(
amIPlayer1 ? "nameRight" : "nameLeft",
);
const oppName = oppEl?.textContent || "Gegner";
// player1-Name und player2-Name bestimmen
const p1Name = amIPlayer1 ? myName : oppName;
const p2Name = amIPlayer1 ? oppName : myName;
const leftName = flip ? p2Name : p1Name;
const rightName = flip ? p1Name : p2Name;
// Namen in Avatar-Slots schreiben
document.getElementById("nameLeft").textContent = leftName;
document.getElementById("nameRight").textContent = rightName;
// Platzhalter: Ingame-Namen anzeigen
["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 = `
<div style="
font-family:'Cinzel',serif;
font-size:calc(var(--s)*13);
font-weight:700;
color:#ffd750;
text-align:center;
padding:0 8px;
word-break:break-word;
line-height:1.4;
text-shadow:0 2px 8px rgba(0,0,0,0.9),0 0 20px rgba(0,0,0,0.8);
">${name}</div>`;
});
document.getElementById("board-lock-overlay")?.remove();
// Festlegen ob ich der linke Spieler bin
amILeftPlayer = flip ? mySlot === "player2" : mySlot === "player1";
// 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}`,
);
}
}
});
/* ── Aufgeben Modal ─────────────────────────────────── */
function handleAufgeben() {
// Eigenes Modal statt browser confirm()
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 = `
<div style="background:linear-gradient(#2a1a08,#1a0f04);border:2px solid #c8960c;
border-radius:14px;padding:32px 40px;text-align:center;max-width:360px;
box-shadow:0 20px 60px rgba(0,0,0,0.9);font-family:'Cinzel',serif;">
<div style="font-size:40px;margin-bottom:12px;">🏳️</div>
<div style="font-size:18px;color:#f0d060;letter-spacing:3px;margin-bottom:10px;">AUFGEBEN?</div>
<p style="color:#a08060;font-size:12px;line-height:1.7;margin-bottom:24px;">
Willst du wirklich aufgeben?<br>
Du erhältst <strong style="color:#e74c3c;">keine Punkte</strong> für dieses Match.
</p>
<div style="display:flex;gap:12px;justify-content:center;">
<button id="surrender-no" style="padding:10px 24px;background:linear-gradient(#1a4a18,#0f2a0e);
border:2px solid #4a8a3c;border-radius:8px;color:#a0e090;font-family:'Cinzel',serif;
font-size:12px;cursor:pointer;">✖ Weiterkämpfen</button>
<button id="surrender-yes" style="padding:10px 24px;background:linear-gradient(#4a1010,#2a0808);
border:2px solid #8a3030;border-radius:8px;color:#e07070;font-family:'Cinzel',serif;
font-size:12px;cursor:pointer;">🏳 Aufgeben</button>
</div>
</div>`;
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 });
// Eigene Weiterleitung nach kurzer Verzögerung
showSurrenderMessage(false);
});
}
/* ── Zurück zum Arena-Popup (nicht zum Launcher) ─────── */
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";
}
}
/* ── Nachricht anzeigen + weiterleiten ──────────────── */
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;`;
if (iWon) {
overlay.innerHTML = `
<div style="font-size:64px;margin-bottom:20px;filter:drop-shadow(0 0 20px rgba(255,215,0,0.6));">🏆</div>
<div style="font-size:34px;color:#f0d060;letter-spacing:6px;margin-bottom:12px;
text-shadow:0 0 30px rgba(255,215,0,0.5);">SIEG!</div>
<div style="font-size:16px;color:#a0d090;letter-spacing:2px;margin-bottom:6px;">
Dein Gegner hat aufgegeben.</div>
<div style="font-size:12px;color:#606060;margin-bottom:32px;letter-spacing:1px;">
Du erhältst die Arena-Punkte für dieses Match.</div>
<button id="surrender-close-btn" style="
background:linear-gradient(135deg,#1a5a18,#27ae60);
border:2px solid rgba(100,220,100,0.6);
border-radius:10px;color:#fff;
font-family:'Cinzel',serif;font-size:14px;
letter-spacing:3px;padding:12px 36px;
cursor:pointer;transition:0.2s;
box-shadow:0 4px 20px rgba(0,150,0,0.4);">
✔ WEITER
</button>`;
} else {
overlay.innerHTML = `
<div style="font-size:64px;margin-bottom:20px;opacity:0.7;">🏳️</div>
<div style="font-size:34px;color:#e74c3c;letter-spacing:6px;margin-bottom:12px;">AUFGEGEBEN</div>
<div style="font-size:16px;color:#a08060;letter-spacing:2px;margin-bottom:6px;">
Du hast das Match aufgegeben.</div>
<div style="font-size:12px;color:#606060;margin-bottom:32px;letter-spacing:1px;">
Keine Punkte für dieses Match.</div>
<button id="surrender-close-btn" style="
background:linear-gradient(135deg,#3a2810,#2a1a08);
border:2px solid rgba(200,150,60,0.5);
border-radius:10px;color:#c8a860;
font-family:'Cinzel',serif;font-size:14px;
letter-spacing:3px;padding:12px 36px;
cursor:pointer;transition:0.2s;">
← ZURÜCK ZUR ARENA
</button>`;
}
document.body.appendChild(overlay);
// Button-Klick → zurück zum Arena-Popup
document
.getElementById("surrender-close-btn")
.addEventListener("click", closeToArena);
// Auto-Close nach 8s
setTimeout(closeToArena, 8000);
}
/* ── Spielende Events ───────────────────────────────── */
socket.on("player_surrendered", (data) => {
const iLost = data.slot === mySlot;
if (!iLost) {
// Ich habe gewonnen Gegner hat aufgegeben
showSurrenderMessage(true);
}
// Wenn ich selbst aufgegeben habe, läuft showSurrenderMessage(false) schon
});
/* Punkte-Ergebnis vom Server (kommt kurz nach player_surrendered) */
socket.on("match_result", (data) => {
updateResultWithPoints(data);
});
socket.on("match_cancelled", () => {
closePopup();
});
/* ── 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 ? "⚔️ SIEG!" : "💀 NIEDERLAGE";
titleEl.className = "result-title " + (won ? "win" : "lose");
pointsEl.textContent = "Punkte werden berechnet…";
overlay.classList.add("show");
if (data) updateResultWithPoints(data);
}
function updateResultWithPoints(data) {
const pointsEl = document.getElementById("result-points");
const levelupEl = document.getElementById("result-levelup");
const progressEl = document.getElementById("result-progress-wrap");
const fillEl = document.getElementById("result-progress-fill");
const lvlLbl = document.getElementById("result-level-label");
const ptsLbl = document.getElementById("result-pts-label");
/* Overlay anzeigen falls noch nicht sichtbar */
const overlay = document.getElementById("match-result-overlay");
if (!overlay.classList.contains("show")) {
document.getElementById("result-title").textContent = data.won
? "⚔️ SIEG!"
: "💀 NIEDERLAGE";
document.getElementById("result-title").className =
"result-title " + (data.won ? "win" : "lose");
overlay.classList.add("show");
}
pointsEl.textContent =
data.awarded > 0
? "+" + data.awarded + " Arena-Punkte"
: "Keine Punkte (Aufgabe zu früh oder Tageslimit erreicht)";
if (data.level_up) {
levelupEl.style.display = "block";
levelupEl.textContent = "⬆ LEVEL UP! → Level " + data.new_level;
}
/* Fortschrittsbalken vom Server laden */
fetch("/api/points/me")
.then((r) => r.json())
.then((me) => {
progressEl.style.display = "block";
lvlLbl.textContent =
"Level " +
me.current_level +
(me.next_level ? " → " + me.next_level : " (MAX)");
ptsLbl.textContent =
me.points_this_level + " / " + me.points_for_next + " Pts";
requestAnimationFrame(() => {
fillEl.style.width = me.progress_percent + "%";
});
})
.catch(() => {});
}
/* ── Popup schließen → zurück zum Arena-Popup ────────── */
function closePopup() {
closeToArena();
}
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);
}
/* ══════════════════════════════════════════════════════
DRAG & DROP Karte aus Hand auf Board legen
══════════════════════════════════════════════════════ */
// Board-State: welche Karten liegen auf welchem Slot
const boardState = {}; // key: "row1-slot-3", value: card object
/* Welche Slot-Indizes darf ich bespielen? */
function isMyZone(slotIndex) {
const idx = Number(slotIndex);
// 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 */
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");
}
});
}
/* Karte auf Board rendern */
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 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>` : ""}
${rngVal != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ""}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
</div>`;
slotEl.classList.add("slot-occupied");
slotEl.innerHTML = card.image
? `<img src="/images/cards/${card.image}"
onerror="this.src='/images/items/rueckseite.png'"
title="${card.name}"
style="width:100%;height:100%;object-fit:cover;border-radius:7px;display:block;">
${statsHtml}`
: `<div style="display:flex;flex-direction:column;align-items:center;
justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;">
<span style="font-size:18px;">⚔️</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>${statsHtml}`;
}
/* ── Drag-Events auf Hand-Slots (delegiert) ── */
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);
// Drop-Zones einblenden
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;
});
/* ── Drop-Events auf Board-Slots (delegiert) ── */
["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";
// Hover-Highlight
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;
// Fallback: manche Browser liefern draggedCardSlotId über dataTransfer
const sourceId =
draggedCardSlotId || e.dataTransfer.getData("text/plain");
if (!sourceId) return;
const cardState = handSlotState[sourceId];
if (!cardState || cardState.currentCd > 0) return;
// Karte vom Hand-Slot entfernen
handSlotState[sourceId] = null;
renderHandSlot(sourceId);
// Karte auf Board-Slot legen
boardState[slot.id] = cardState.card;
renderCardOnBoard(slot, cardState.card);
slot.classList.remove("drop-zone-active", "drop-zone-hover");
// Gegner & Server informieren
// (Nachziehen erst am Zugende, nicht sofort)
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} (aus ${sourceId})`,
);
});
});
/* ── Gegner hat Karte gespielt → auf Board anzeigen ── */
socket.on("card_played", (data) => {
// Eigene Aktionen ignorieren (lokal bereits gesetzt)
if (data.slot === mySlot) return;
// Slot bereits belegt → ignorieren
if (boardState[data.boardSlot]) return;
const slotEl = document.getElementById(data.boardSlot);
if (!slotEl) {
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,
);
});
/* ── 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");
});
</script>
</body>
</html>