/* ═══════════════════════════════════════════════════════════════ public/js/buildings/1v1.js Vollstaendige Spielfeld-Logik fuer das 1v1-Battlefield. EJS-Variablen kommen aus window.GAME_CONFIG (in der EJS gesetzt). ═══════════════════════════════════════════════════════════════ */ /* ── Konfiguration aus EJS-Bridge ────────────────────────── */ const matchId = new URLSearchParams(window.location.search).get('match') || window.GAME_CONFIG.matchId; const mySlot = new URLSearchParams(window.location.search).get('slot') || window.GAME_CONFIG.mySlot; const amIPlayer1 = mySlot === 'player1'; /* ── Gegner-Name aus URL setzen ──────────────────────────── */ const _urlParams = new URLSearchParams(window.location.search); const opponentNameFromUrl = _urlParams.get('opponent'); if (opponentNameFromUrl) { const oppEl = document.getElementById(amIPlayer1 ? 'nameRight' : 'nameLeft'); if (oppEl) oppEl.textContent = decodeURIComponent(opponentNameFromUrl); } /* ═══════════════════════════════════════════════════════════ SPIELFELD AUFBAUEN ═══════════════════════════════════════════════════════════ */ ['row1', 'row2'].forEach(rowId => { const row = document.getElementById(rowId); for (let i = 1; i <= 11; i++) { const s = document.createElement('div'); s.className = 'card-slot'; s.dataset.row = rowId; s.dataset.slotIndex = i; s.id = `${rowId}-slot-${i}`; s.innerHTML = '\u2736' + i + ''; row.appendChild(s); } }); const hand = document.getElementById('handArea'); /* ── Deck-Stapel ─────────────────────────────────────────── */ const deckSlot = document.createElement('div'); deckSlot.className = 'hand-slot hand-slot-deck'; deckSlot.id = 'deck-stack'; deckSlot.title = 'Dein Deck'; deckSlot.innerHTML = `
\u2014`; hand.appendChild(deckSlot); /* ── Hand-Karten-Slots ───────────────────────────────────── */ const handCardIds = ['hand-card-1','hand-card-2','hand-card-3','hand-card-4','hand-card-5','hand-card-6','hand-card-7']; handCardIds.forEach(id => { const s = document.createElement('div'); s.className = 'hand-slot hand-slot-card'; s.id = id; s.innerHTML = '\uD83C\uDCCF'; hand.appendChild(s); }); /* ═══════════════════════════════════════════════════════════ HAND & DECK SYSTEM ═══════════════════════════════════════════════════════════ */ let deckQueue = []; const handSlotState = {}; handCardIds.forEach(id => { handSlotState[id] = null; }); /* ── SVG-Icons ───────────────────────────────────────────── */ const SVG_RANGE = ``; const SVG_RACE = ``; /* ── Hand-Slot rendern ───────────────────────────────────── */ function renderHandSlot(id) { const slot = document.getElementById(id); const state = handSlotState[id]; if (!slot) return; slot.innerHTML = ''; slot.style.backgroundImage = 'none'; slot.style.opacity = '1'; slot.style.filter = 'none'; slot.style.backgroundColor = '#0a0805'; slot.classList.remove('hand-slot-ready', 'hand-slot--filled'); if (!state) { slot.innerHTML = '\uD83C\uDCCF'; slot.draggable = false; delete slot.dataset.cardSlotId; return; } const { card, currentCd } = state; const isReady = currentCd <= 0; const atkVal = card.attack ?? null; const defVal = card.defends ?? null; const rngVal = card.range ?? null; const rceVal = card.race ?? null; const statsHtml = `
${atkVal != null ? `${atkVal}` : ''} ${defVal != null ? `${defVal}` : ''} ${card.cooldown != null ? `${isReady ? '\u2713' : currentCd}` : ''} ${rngVal != null ? `${SVG_RANGE} ${rngVal}` : ''} ${rceVal != null ? `${SVG_RACE} ${rceVal}` : ''}
`; const readyBadge = (isReady && isMyTurn) ? '
SPIELEN
' : ''; slot.innerHTML = card.image ? `${statsHtml}${readyBadge}` : `
\u2694\uFE0F ${card.name}
${statsHtml}${readyBadge}`; slot.classList.toggle('hand-slot-ready', isReady); slot.classList.add('hand-slot--filled'); if (isReady && isMyTurn) { slot.draggable = true; slot.dataset.cardSlotId = id; } else { slot.draggable = false; delete slot.dataset.cardSlotId; } } function setHandSlot(id, card) { handSlotState[id] = { card, currentCd: card.cooldown ?? 0, wasReduced: false }; renderHandSlot(id); } function drawNextCard() { if (deckQueue.length === 0) return; const freeSlot = handCardIds.find(id => handSlotState[id] === null); if (!freeSlot) return; const card = deckQueue.shift(); setHandSlot(freeSlot, card); const countEl = document.getElementById('deck-count'); if (countEl) countEl.textContent = deckQueue.length; } function tickHandCooldowns() { handCardIds.forEach(id => { const state = handSlotState[id]; if (!state) return; const baseCd = Number(state.card.cooldown || 0); if (state.currentCd <= 0 && !state.wasReduced) { state.currentCd = baseCd > 0 ? Math.max(1, Math.floor(baseCd / 2)) : 0; state.wasReduced = true; } else if (state.currentCd > 0) { state.currentCd--; } renderHandSlot(id); }); } /* ── Deck laden ──────────────────────────────────────────── */ (async () => { try { const urlP = new URLSearchParams(window.location.search); const deckId = urlP.get('deck') || sessionStorage.getItem('selectedDeckId'); if (!deckId) return; const res = await fetch('/api/decks/' + deckId + '/cards'); if (!res.ok) return; const cards = await res.json(); const expanded = []; cards.forEach(c => { for (let i = 0; i < (c.amount ?? 1); i++) expanded.push(c); }); for (let i = expanded.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [expanded[i], expanded[j]] = [expanded[j], expanded[i]]; } for (let i = 0; i < 3; i++) { if (expanded[i]) setHandSlot(handCardIds[i], expanded[i]); } deckQueue = expanded.slice(3); const countEl = document.getElementById('deck-count'); if (countEl) countEl.textContent = deckQueue.length; } catch (e) { console.error('[Battlefield] Deck laden:', e); } })(); /* ═══════════════════════════════════════════════════════════ ZUG-TIMER ═══════════════════════════════════════════════════════════ */ const TURN_SECONDS = 30; const TT_CIRCUM = 2 * Math.PI * 18; let turnTimerInt = null; let turnSecsLeft = TURN_SECONDS; let amILeftPlayer = null; let isMyTurn = false; function startTurnTimer(activeName) { clearInterval(turnTimerInt); turnSecsLeft = TURN_SECONDS; updateTimerUI(turnSecsLeft, activeName); const wrap = document.getElementById('turn-timer-wrap'); if (wrap) wrap.style.display = 'flex'; turnTimerInt = setInterval(() => { turnSecsLeft--; updateTimerUI(turnSecsLeft, activeName); if (turnSecsLeft <= 0) { clearInterval(turnTimerInt); if (isMyTurn) endMyTurn(); } }, 1000); } function stopTurnTimer() { clearInterval(turnTimerInt); const wrap = document.getElementById('turn-timer-wrap'); if (wrap) wrap.style.display = 'none'; } function updateTimerUI(secs, activeName) { const num = document.getElementById('turn-timer-num'); const circle = document.getElementById('tt-circle'); const wrap = document.getElementById('turn-timer-wrap'); if (num) num.textContent = secs; if (wrap && activeName) wrap.title = activeName + ' ist am Zug'; if (circle) { circle.style.strokeDashoffset = TT_CIRCUM * (1 - secs / TURN_SECONDS); circle.style.stroke = secs > 15 ? '#27ae60' : secs > 8 ? '#f39c12' : '#e74c3c'; if (secs <= 8) { num.style.color = '#e74c3c'; num.style.animation = 'tt-pulse 0.5s ease-in-out infinite'; } else { num.style.color = '#fff'; num.style.animation = 'none'; } } } /* ═══════════════════════════════════════════════════════════ ZUG-SYSTEM ═══════════════════════════════════════════════════════════ */ function setTurnState(myTurn, activeName) { isMyTurn = myTurn; const btn = document.getElementById('end-turn-btn'); if (!btn) return; handCardIds.forEach(id => renderHandSlot(id)); if (myTurn) { btn.disabled = false; btn.textContent = 'Zug beenden'; btn.style.opacity = '1'; document.getElementById('turn-indicator')?.remove(); startTurnTimer(activeName || 'Du'); } else { btn.disabled = true; btn.style.opacity = '0.4'; showTurnIndicator(activeName); startTurnTimer(activeName || 'Gegner'); } } function showTurnIndicator(activeName) { document.getElementById('turn-indicator')?.remove(); const ind = document.createElement('div'); ind.id = 'turn-indicator'; ind.style.cssText = ` position:fixed;bottom:calc(var(--s)*200);left:50%;transform:translateX(-50%); background:linear-gradient(135deg,rgba(20,20,40,0.95),rgba(10,10,25,0.95)); border:1px solid rgba(200,160,60,0.5);border-radius:calc(var(--s)*8); color:rgba(255,215,80,0.75);font-family:'Cinzel',serif; font-size:calc(var(--s)*11);letter-spacing:calc(var(--s)*3); padding:calc(var(--s)*8) calc(var(--s)*20);z-index:100;pointer-events:none; text-transform:uppercase;box-shadow:0 4px 20px rgba(0,0,0,0.6);`; ind.textContent = activeName ? `\u23F3 ${activeName} ist am Zug` : '\u23F3 Gegner ist am Zug'; document.body.appendChild(ind); } function endMyTurn() { if (!isMyTurn) return; clearInterval(turnTimerInt); stopTurnTimer(); setTurnState(false); tickHandCooldowns(); drawNextCard(); socket.emit('end_turn', { matchId, slot: mySlot }); } document.getElementById('end-turn-btn')?.addEventListener('click', endMyTurn); /* ═══════════════════════════════════════════════════════════ KARTE AUF BOARD RENDERN ═══════════════════════════════════════════════════════════ */ /* * boardState speichert jetzt: { [slotId]: { card, owner } } * card = Karten-Objekt mit aktuellen Werten (defends wird live verändert) * owner = 'player1' | 'player2' */ const boardState = {}; 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)}`; } /* 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.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); }); 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 === (window._leftSlot || '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 === (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'; 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 mit owner speichern ─────────────────── */ boardState[slot.id] = { card: cardState.card, owner: mySlot }; 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 mit owner des Gegners speichern ─────── */ boardState[data.boardSlot] = { card: data.card, owner: data.slot }; renderCardOnBoard(slotEl, data.card); 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); 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); }); } /* 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); }); /* ═══════════════════════════════════════════════════════════ 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'); });