/* ═══════════════════════════════════════════════════════════════
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(() =>
`
`
).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'); });