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" %>
- - - - +
- - - + + + + + - + \ No newline at end of file