/* ═══════════════════════════════════════════════════════════════ 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; if (myTurn) enableBoardDrag(); else disableBoardDrag(); 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}`); }); }); /* ═══════════════════════════════════════════════════════════ ABLAGESTAPEL – DISCARD SYSTEM Spieler zieht Karte vom Board → auf Discard-Zone droppen → Karte weg vom Board, ausgegraut auf Stapel, nicht mehr spielbar ═══════════════════════════════════════════════════════════ */ const discardPile = []; // abgeworfene Karten (lokal) /* ── Discard-Zone Drag-Events ────────────────────────────── */ const discardZone = document.getElementById('discard-zone'); discardZone.addEventListener('dragover', e => { if (!isMyTurn) return; if (!draggedBoardSlotId) return; // nur Board-Karten e.preventDefault(); discardZone.classList.add('drag-over'); }); discardZone.addEventListener('dragleave', () => { discardZone.classList.remove('drag-over'); }); discardZone.addEventListener('drop', e => { e.preventDefault(); discardZone.classList.remove('drag-over'); if (!draggedBoardSlotId || !isMyTurn) return; discardBoardCard(draggedBoardSlotId); draggedBoardSlotId = null; }); /* ── Board-Karten draggable machen / entfernen ───────────── */ let draggedBoardSlotId = null; function enableBoardDrag() { document.querySelectorAll('.card-slot').forEach(slot => { const idx = Number(slot.dataset.slotIndex); if (!isMyZone(idx)) return; if (!boardState[slot.id]) return; if (boardState[slot.id].owner !== mySlot) return; slot.classList.add('board-slot-draggable'); slot.setAttribute('draggable', 'true'); // Event-Listener nur einmal anhängen if (!slot.dataset.discardListenerAttached) { slot.dataset.discardListenerAttached = '1'; slot.addEventListener('dragstart', e => { if (!isMyTurn) { e.preventDefault(); return; } draggedBoardSlotId = slot.id; slot.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/board', slot.id); }); slot.addEventListener('dragend', () => { slot.classList.remove('dragging'); draggedBoardSlotId = null; discardZone.classList.remove('drag-over'); }); } }); } function disableBoardDrag() { document.querySelectorAll('.board-slot-draggable').forEach(slot => { slot.classList.remove('board-slot-draggable'); slot.setAttribute('draggable', 'false'); }); } /* ── Karte abwerfen ──────────────────────────────────────── */ function discardBoardCard(slotId) { const entry = boardState[slotId]; if (!entry || entry.owner !== mySlot) return; const card = entry.card; discardPile.push(card); delete boardState[slotId]; // Slot leeren const slotEl = document.getElementById(slotId); if (slotEl) { slotEl.innerHTML = ''; slotEl.classList.remove('board-slot-draggable', 'dragging'); slotEl.setAttribute('draggable', 'false'); delete slotEl.dataset.discardListenerAttached; } updateDiscardZone(); socket.emit('discard_card', { matchId, slotId, slot: mySlot, cardId: card.id }); console.log(`[Discard] Karte abgeworfen: ${card.name} von ${slotId}`); } /* ── Discard-Zone visuell updaten ────────────────────────── */ function updateDiscardZone() { const stack = document.getElementById('discard-stack'); const count = document.getElementById('discard-count'); if (!stack) return; const shown = Math.min(discardPile.length, 3); stack.innerHTML = Array.from({ length: shown }).map(() => `Abgeworfen` ).join(''); if (count) { count.textContent = discardPile.length > 0 ? discardPile.length : ''; count.dataset.count = discardPile.length; } } /* ── Gegner wirft Karte ab → Slot leeren ─────────────────── */ socket.on('card_discarded', data => { const { slotId } = data; if (!slotId) return; const slotEl = document.getElementById(slotId); if (slotEl) slotEl.innerHTML = ''; delete boardState[slotId]; console.log(`[Discard] Gegner hat Karte abgeworfen von ${slotId}`); }); /* ── Board-Drag bei Zugwechsel aktivieren/deaktivieren ────── */ // Wird von setTurnState aufgerufen (siehe unten) 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`; } } 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, row } = data; updateHpDisplay(slot, remainingHp, maxHp); console.log(`[HP] ${slot} (${row}) -${damage} → ${remainingHp}/${maxHp}`); /* Schadens-Zahl einblenden – Reihe 1 oben, Reihe 2 etwas tiefer */ const isLeft = slot === (window._leftSlot || 'player1'); const avEl = document.getElementById(isLeft ? 'avLeft' : 'avRight'); const topPct = row === 'row2' ? '38%' : '14%'; if (avEl) { const dmg = document.createElement('div'); dmg.textContent = `-${damage}`; dmg.style.cssText = ` position:absolute;top:${topPct};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 2.5s ease forwards;`; avEl.appendChild(dmg); setTimeout(() => dmg.remove(), 2500); /* Schüttelanimation + roter Flash NUR bei Treffer */ 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); } }); /* ═══════════════════════════════════════════════════════════ KI-MATCH SETUP (ht_ai_setup) Setzt Links/Rechts explizit ohne seed-basierten Flip. Nur für Daily-KI-Matches – umgeht den ready_status Flip. ═══════════════════════════════════════════════════════════ */ socket.on('ht_ai_setup', data => { const { playerSlot, leftSlot, playerName, aiName, hp, maxHp } = data; /* Links/Rechts festlegen */ window._leftSlot = leftSlot || 'player1'; amILeftPlayer = mySlot === window._leftSlot; /* Namen setzen */ const nameLeftEl = document.getElementById('nameLeft'); const nameRightEl = document.getElementById('nameRight'); if (nameLeftEl) nameLeftEl.textContent = amILeftPlayer ? (playerName || 'Du') : (aiName || 'Wächter'); if (nameRightEl) nameRightEl.textContent = amILeftPlayer ? (aiName || 'Wächter') : (playerName || 'Du'); /* Avatar-Platzhalter */ ['avLeft', 'avRight'].forEach(avId => { const av = document.getElementById(avId); const ph = av?.querySelector('.av-placeholder'); const name = avId === 'avLeft' ? nameLeftEl?.textContent : nameRightEl?.textContent; if (ph) ph.innerHTML = `
${name}
`; }); /* Board-Seite festlegen */ 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'); } /* Avatar-Farben: Spieler=blau, KI=rot */ const avL = document.getElementById('avLeft'); const avR = document.getElementById('avRight'); if (avL) { avL.style.borderColor = 'rgba(60,140,255,.95)'; avL.style.boxShadow = '0 0 28px rgba(60,140,255,.55),inset 0 0 20px rgba(0,0,0,.5)'; } if (avR) { avR.style.borderColor = 'rgba(220,60,60,.95)'; avR.style.boxShadow = '0 0 28px rgba(220,60,60,.55),inset 0 0 20px rgba(0,0,0,.5)'; } /* HP initialisieren */ if (hp && maxHp) applyHpFromEvent({ hp, maxHp }); /* Board-Lock entfernen */ document.getElementById('board-lock-overlay')?.remove(); console.log(`[KI] Setup: ich=${mySlot}, links=${window._leftSlot}, amILeft=${amILeftPlayer}`); }); /* ═══════════════════════════════════════════════════════════ AUFGEBEN ═══════════════════════════════════════════════════════════ */ function closeToArena() { if (window.parent && window.parent !== window) { const pd = window.parent.document; /* Alle möglichen Popup-Varianten entfernen */ ['arena-backdrop', 'arena-popup', 'ht-daily-backdrop', 'ht-daily-popup', 'ht-backdrop', 'ht-popup'].forEach(id => pd.getElementById(id)?.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.jpeg'; 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 5 Sekunden: Overlay ausblenden und zur Arena weiterleiten */ setTimeout(() => { const el = document.getElementById('match-end-overlay'); if (el) { el.style.transition = 'opacity 0.6s ease'; el.style.opacity = '0'; setTimeout(() => { el.remove(); closeToArena(); }, 650); } else { closeToArena(); } }, 5000); } 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'); });