1194 lines
48 KiB
JavaScript
1194 lines
48 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════
|
||
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} ${rngVal}</span>` : ""}
|
||
${rceVal != null ? `<span class="cs-race">${SVG_RACE} ${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;
|
||
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} ${rng}</span>` : ""}
|
||
${rce != null ? `<span class="cs-race">${SVG_RACE} ${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 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;">${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}`);
|
||
});
|
||
});
|
||
|
||
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`;
|
||
}
|
||
|
||
/* Avatar-Schüttelanimation + roter Flash */
|
||
if (avEl) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
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 } = data;
|
||
updateHpDisplay(slot, remainingHp, maxHp);
|
||
console.log(`[HP] ${slot} -${damage} → ${remainingHp}/${maxHp}`);
|
||
|
||
/* Schadens-Zahl einblenden */
|
||
const isLeft = slot === (window._leftSlot || "player1");
|
||
const avEl = document.getElementById(isLeft ? "avLeft" : "avRight");
|
||
if (avEl) {
|
||
const dmg = document.createElement("div");
|
||
dmg.textContent = `-${damage}`;
|
||
dmg.style.cssText = `
|
||
position:absolute;top:15%;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 1s ease forwards;`;
|
||
avEl.appendChild(dmg);
|
||
setTimeout(() => dmg.remove(), 1000);
|
||
}
|
||
});
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
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 = `
|
||
<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.jpg";
|
||
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 3 Sekunden zur Arena weiterleiten */
|
||
setTimeout(() => closeToArena(), 3000);
|
||
}
|
||
|
||
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");
|
||
});
|