dok/public/js/buildings/1v1.js
2026-04-14 12:01:05 +01:00

1241 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ═══════════════════════════════════════════════════════════════
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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + i + '</span>';
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 = `
<div class="deck-stack-wrap">
<img class="deck-card-back deck-shadow-3" src="/images/items/rueckseite.png">
<img class="deck-card-back deck-shadow-2" src="/images/items/rueckseite.png">
<img class="deck-card-back deck-shadow-1" src="/images/items/rueckseite.png">
<img class="deck-card-top" src="/images/items/rueckseite.png">
</div>
<span class="deck-count" id="deck-count">\u2014</span>`;
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 = '<span class="hs-icon">\uD83C\uDCCF</span>';
hand.appendChild(s);
});
/* ═══════════════════════════════════════════════════════════
HAND & DECK SYSTEM
═══════════════════════════════════════════════════════════ */
let deckQueue = [];
const handSlotState = {};
handCardIds.forEach(id => { handSlotState[id] = null; });
/* ── SVG-Icons ───────────────────────────────────────────── */
const SVG_RANGE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#e8b84b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2 Q1 8 4 14"/><line x1="4" y1="2" x2="4" y2="14" stroke-width="0.7" stroke-dasharray="2,1.5"/><line x1="4" y1="8" x2="13" y2="8"/><polyline points="11,6 13,8 11,10"/><line x1="5" y1="7" x2="4" y2="8"/><line x1="5" y1="9" x2="4" y2="8"/></svg>`;
const SVG_RACE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/><line x1="9" y1="4" x2="8" y2="9"/><line x1="8" y1="9" x2="10" y2="14"/><line x1="8" y1="9" x2="6" y2="13"/><line x1="8.5" y1="5.5" x2="11" y2="8"/><line x1="8.5" y1="5.5" x2="6" y2="7"/></svg>`;
/* ── 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 = '<span class="hs-icon">\uD83C\uDCCF</span>';
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 = `<div class="card-stat-overlay">
${atkVal != null ? `<span class="cs-atk">${atkVal}</span>` : ''}
${defVal != null ? `<span class="cs-def">${defVal}</span>` : ''}
${card.cooldown != null ? `<span class="cs-cd ${isReady ? 'cs-cd-ready' : ''}">${isReady ? '\u2713' : currentCd}</span>` : ''}
${rngVal != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ''}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ''}
</div>`;
const readyBadge = (isReady && isMyTurn) ? '<div class="hand-slot-ready-badge">SPIELEN</div>' : '';
slot.innerHTML = card.image
? `<img class="hand-card-img" src="/images/cards/${card.image}" onerror="this.src='/images/items/rueckseite.png'" title="${card.name}">${statsHtml}${readyBadge}`
: `<div style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:Cinzel,serif;">
<span style="font-size:18px;">\u2694\uFE0F</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>${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 `<div class="card-stat-overlay">
${atk != null ? `<span class="cs-atk">${atk}</span>` : ''}
${def != null ? `<span class="cs-def">${def}</span>` : ''}
${cd != null ? `<span class="cs-cd">${cd}</span>` : ''}
${rng != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rng}</span>` : ''}
${rce != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rce}</span>` : ''}
</div>`;
}
function renderCardOnBoard(slotEl, card, owner) {
const statsHtml = buildStatsHtml(card);
slotEl.classList.add('slot-occupied');
applyOwnerStyle(slotEl, owner);
slotEl.innerHTML = card.image
? `<img draggable="false" src="/images/cards/${card.image}" onerror="this.src='/images/items/rueckseite.png'" title="${card.name}" style="width:100%;height:100%;object-fit:cover;border-radius:7px;display:block;pointer-events:none;">${statsHtml}`
: `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;"><span style="font-size:18px;">\u2694\uFE0F</span><span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span></div>${statsHtml}`;
}
function renderCardInSlot(slot, card) {
if (!slot || !card) return;
slot.classList.add('slot-occupied');
slot.innerHTML = card.image
? `<img src="/images/cards/${card.image}" onerror="this.src='/images/items/rueckseite.png'" title="${card.name}" style="width:100%;height:100%;object-fit:cover;border-radius:7px;display:block;">${buildStatsHtml(card)}`
: `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;"><span style="font-size:18px;">\u2694\uFE0F</span><span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span></div>${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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + slotId.split('-slot-')[1] + '</span>';
}
/* ═══════════════════════════════════════════════════════════
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 = `<div style="font-family:'Cinzel',serif;font-size:calc(var(--s)*13);font-weight:700;color:#ffd750;text-align:center;padding:0 8px;word-break:break-word;line-height:1.4;text-shadow:0 2px 8px rgba(0,0,0,0.9)">${name}</div>`;
});
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 => {
// Nur eigene Karten sind abwerfbar egal auf welchem Slot sie stehen
if (boardState[slot.id]?.owner !== mySlot) return;
slot.classList.add('board-slot-draggable');
slot.setAttribute('draggable', 'true');
});
}
function disableBoardDrag() {
document.querySelectorAll('.board-slot-draggable').forEach(slot => {
slot.classList.remove('board-slot-draggable', 'dragging');
slot.setAttribute('draggable', 'false');
});
}
/* ── Board-Drag via Event-Delegation auf den Rows ────────── */
['row1', 'row2'].forEach(rowId => {
const row = document.getElementById(rowId);
row.addEventListener('dragstart', e => {
const slot = e.target.closest('.board-slot-draggable');
if (!slot || !isMyTurn) { e.preventDefault(); return; }
draggedBoardSlotId = slot.id;
slot.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/board', slot.id);
});
row.addEventListener('dragend', e => {
const slot = e.target.closest('.board-slot-draggable');
if (slot) slot.classList.remove('dragging');
draggedBoardSlotId = null;
discardZone.classList.remove('drag-over');
});
});
/* ── 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(() =>
`<img class="discard-card-back" src="/images/items/rueckseite.png" alt="Abgeworfen" draggable="false">`
).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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + ev.from.split('-slot-')[1] + '</span>';
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 = `<div style="font-family:'Cinzel',serif;font-size:calc(var(--s)*13);font-weight:700;color:#ffd750;text-align:center;padding:0 8px;word-break:break-word;line-height:1.4;text-shadow:0 2px 8px rgba(0,0,0,0.9)">${name}</div>`;
});
/* 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 = `
<div style="background:linear-gradient(#2a1a08,#1a0f04);border:2px solid #c8960c;border-radius:14px;padding:32px 40px;text-align:center;max-width:360px;box-shadow:0 20px 60px rgba(0,0,0,0.9);font-family:'Cinzel',serif;">
<div style="font-size:40px;margin-bottom:12px;">\uD83C\uDFF3\uFE0F</div>
<div style="font-size:18px;color:#f0d060;letter-spacing:3px;margin-bottom:10px;">AUFGEBEN?</div>
<p style="color:#a08060;font-size:12px;line-height:1.7;margin-bottom:24px;">Willst du wirklich aufgeben?<br>Du erh\u00E4ltst <strong style="color:#e74c3c;">keine Punkte</strong> f\u00FCr dieses Match.</p>
<div style="display:flex;gap:12px;justify-content:center;">
<button id="surrender-no" style="padding:10px 24px;background:linear-gradient(#1a4a18,#0f2a0e);border:2px solid #4a8a3c;border-radius:8px;color:#a0e090;font-family:'Cinzel',serif;font-size:12px;cursor:pointer;">\u2716 Weiterk\u00E4mpfen</button>
<button id="surrender-yes" style="padding:10px 24px;background:linear-gradient(#4a1010,#2a0808);border:2px solid #8a3030;border-radius:8px;color:#e07070;font-family:'Cinzel',serif;font-size:12px;cursor:pointer;">\uD83C\uDFF3 Aufgeben</button>
</div>
</div>`;
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
? `<div style="font-size:64px;margin-bottom:20px;">\uD83C\uDFC6</div>
<div style="font-size:34px;color:#f0d060;letter-spacing:6px;margin-bottom:12px;">SIEG!</div>
<div style="font-size:16px;color:#a0d090;letter-spacing:2px;margin-bottom:6px;">Dein Gegner hat aufgegeben.</div>
<div style="font-size:12px;color:#606060;margin-bottom:32px;">Du erh\u00E4ltst die Arena-Punkte.</div>
<button id="surrender-close-btn" style="background:linear-gradient(135deg,#1a5a18,#27ae60);border:2px solid rgba(100,220,100,0.6);border-radius:10px;color:#fff;font-family:'Cinzel',serif;font-size:14px;letter-spacing:3px;padding:12px 36px;cursor:pointer;">\u2714 WEITER</button>`
: `<div style="font-size:64px;margin-bottom:20px;opacity:0.7;">\uD83C\uDFF3\uFE0F</div>
<div style="font-size:34px;color:#e74c3c;letter-spacing:6px;margin-bottom:12px;">AUFGEGEBEN</div>
<div style="font-size:16px;color:#a08060;letter-spacing:2px;margin-bottom:6px;">Du hast das Match aufgegeben.</div>
<div style="font-size:12px;color:#606060;margin-bottom:32px;">Keine Punkte f\u00FCr dieses Match.</div>
<button id="surrender-close-btn" style="background:linear-gradient(135deg,#3a2810,#2a1a08);border:2px solid rgba(200,150,60,0.5);border-radius:10px;color:#c8a860;font-family:'Cinzel',serif;font-size:14px;letter-spacing:3px;padding:12px 36px;cursor:pointer;">\u2190 ZUR\u00DCCK ZUR ARENA</button>`;
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 = `
<img src="${img}" style="width:100%;height:100%;object-fit:cover;display:block;position:absolute;inset:0;">
<div style="
position:absolute;bottom:8%;left:50%;transform:translateX(-50%);
background:rgba(0,0,0,0.72);border:1px solid rgba(255,215,80,0.5);
border-radius:12px;padding:16px 36px;text-align:center;
font-family:'Cinzel',serif;
animation:matchPtsSlideUp 0.5s ease 0.3s both;">
<div style="font-size:calc(var(--s)*18);color:#f0d060;letter-spacing:3px;margin-bottom:6px;">${ptsLine}</div>
${lvlLine ? `<div style="font-size:calc(var(--s)*13);color:#7de87d;letter-spacing:2px;">${lvlLine}</div>` : ''}
</div>`;
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',
`<div style="font-size:calc(var(--s)*11);color:#a0a0a0;margin-top:4px;">Gesamt: ${me.arena_points} Pts • Level ${me.level}</div>`
);
}
}).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'); });