diff --git a/public/css/1v1.css b/public/css/1v1.css
index eb1dd4c..48850a3 100644
--- a/public/css/1v1.css
+++ b/public/css/1v1.css
@@ -417,11 +417,13 @@ body {
align-items: center;
justify-content: center;
cursor: pointer;
- transition: border-color 0.2s ease, transform 0.2s ease;
+ transition:
+ border-color 0.2s ease,
+ transform 0.2s ease;
position: relative;
overflow: hidden;
flex-shrink: 0;
- box-shadow: 0 calc(var(--s) * 4) calc(var(--s) * 14) rgba(0,0,0,.35);
+ box-shadow: 0 calc(var(--s) * 4) calc(var(--s) * 14) rgba(0, 0, 0, 0.35);
}
.hand-slot:hover {
border-color: rgba(255, 215, 80, 0.9);
@@ -432,7 +434,6 @@ body {
opacity: 0.2;
}
-
/* Right side: action icons */
.action-hud {
display: flex;
@@ -954,7 +955,202 @@ body {
cursor: grabbing;
}
-
.hand-slot img.hand-card-img {
- width:100%;height:100%;object-fit:cover;display:block;border-radius:calc(var(--s) * 7);
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ border-radius: calc(var(--s) * 7);
+}
+
+/* ══════════════════════════════════════════════════════
+ MATCH-RESULT OVERLAY
+══════════════════════════════════════════════════════ */
+#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);
+}
+
+/* ══════════════════════════════════════════════════════
+ 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;
}
diff --git a/public/js/buildings/1v1.js b/public/js/buildings/1v1.js
new file mode 100644
index 0000000..6511a5c
--- /dev/null
+++ b/public/js/buildings/1v1.js
@@ -0,0 +1,724 @@
+/* ═══════════════════════════════════════════════════════════════
+ 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
+═══════════════════════════════════════════════════════════ */
+const boardState = {};
+
+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) {
+ const statsHtml = buildStatsHtml(card);
+ slotEl.classList.add('slot-occupied');
+ 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)}`;
+}
+
+
+/* ═══════════════════════════════════════════════════════════
+ 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 ──────────────────────────────────────────── */
+function applyBoardSync(cards) {
+ if (!cards || !cards.length) return;
+ cards.forEach(cd => {
+ const slotEl = document.getElementById(cd.boardSlot);
+ if (!slotEl || boardState[cd.boardSlot]) return;
+ boardState[cd.boardSlot] = cd.card;
+ renderCardOnBoard(slotEl, cd.card);
+ });
+ 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);
+
+ 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 === 'player1' ? 'nameLeft' : 'nameRight');
+ const activeName = activeNameEl?.textContent || (nowMyTurn ? 'Du' : 'Gegner');
+ console.log(`[1v1] turn_change: ${activeSlot} | meinZug: ${nowMyTurn}`);
+ 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 = '\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';
+
+ 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}`);
+ 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[slot.id] = cardState.card;
+ renderCardOnBoard(slot, cardState.card);
+ 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[data.boardSlot] = data.card;
+ renderCardOnBoard(slotEl, data.card);
+ console.log('[1v1] Gegner Karte:', data.card?.name, '->', data.boardSlot);
+});
+
+/* ═══════════════════════════════════════════════════════════
+ 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) {
+ const overlay = document.getElementById('match-result-overlay');
+ 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');
+
+ if (!overlay.classList.contains('show')) {
+ document.getElementById('result-title').textContent = data.won ? '\u2694\uFE0F SIEG!' : '\uD83D\uDC80 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\u00FCh oder Tageslimit)';
+
+ if (data.level_up) {
+ levelupEl.style.display = 'block';
+ levelupEl.textContent = '\u2B06 LEVEL UP! \u2192 Level ' + data.new_level;
+ }
+
+ fetch('/api/points/me').then(r => r.json()).then(me => {
+ progressEl.style.display = 'block';
+ lvlLbl.textContent = 'Level ' + me.current_level + (me.next_level ? ' \u2192 ' + me.next_level : ' (MAX)');
+ ptsLbl.textContent = me.points_this_level + ' / ' + me.points_for_next + ' Pts';
+ requestAnimationFrame(() => { fillEl.style.width = me.progress_percent + '%'; });
+ }).catch(() => {});
+}
+
+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'); });
diff --git a/views/1v1-battlefield.ejs b/views/1v1-battlefield.ejs
index 8d65fbd..e8ea391 100644
--- a/views/1v1-battlefield.ejs
+++ b/views/1v1-battlefield.ejs
@@ -4,202 +4,11 @@
<%= title || "Spielfeld" %>
-
+
-
-
-
+
Warte auf Gegner…
@@ -207,18 +16,16 @@
+
<%= title || "Spielfeld" %>
-
-
-
+
-
⚙
🗺
@@ -226,26 +33,18 @@
🏆
-
+
-
+
Bereit machen
@@ -261,19 +60,11 @@
![]()
-
+
<%= player1 || "Spieler 1" %>
-
- ❤<%= player1hp || 20 %>
-
-
- 💧<%= player1mana || 3 %>
-
+
❤<%= player1hp || 20 %>
+
💧<%= player1mana || 3 %>
<%= player1hp || 15 %>
@@ -281,19 +72,11 @@
![]()
-
+
<%= player2 || "Gegner" %>
-
- ❤<%= player2hp || 20 %>
-
-
- 💧<%= player2mana || 3 %>
-
+
❤<%= player2hp || 20 %>
+
💧<%= player2mana || 3 %>
<%= player2hp || 15 %>
@@ -320,1177 +103,37 @@
+