dok/views/1v1-battlefield.ejs
2026-04-10 06:59:08 +01:00

580 lines
26 KiB
Plaintext
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.

<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title || "Spielfeld" %></title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/1v1.css" />
<style>
#match-result-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.88);
z-index: 9999;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Cinzel", serif;
animation: fadeIn 0.4s ease;
}
#match-result-overlay.show { display: flex; }
@keyframes fadeIn { from{opacity:0;} to{opacity:1;} }
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.5;} }
.result-title { font-size:48px; letter-spacing:6px; margin-bottom:12px; text-shadow:0 0 30px currentColor; }
.result-title.win { color:#f0d060; }
.result-title.lose { color:#c84040; }
.result-points { font-size:22px; color:#c8a860; margin-bottom:6px; letter-spacing:2px; }
.result-levelup { font-size:18px; color:#60e060; margin-bottom:20px; letter-spacing:2px; animation:pulse 1s ease-in-out infinite; }
.result-progress-wrap { width:320px; margin-bottom:24px; }
.result-progress-label { display:flex; justify-content:space-between; font-size:12px; color:#a08060; margin-bottom:6px; }
.result-progress-track { height:10px; background:rgba(255,255,255,0.08); border-radius:5px; overflow:hidden; }
.result-progress-fill { height:100%; background:linear-gradient(90deg,#c8960c,#f0d060); border-radius:5px; transition:width 1s ease; }
.result-close-btn {
background:linear-gradient(#4a3010,#2a1a08); border:2px solid #c8960c;
color:#f0d9a6; font-family:"Cinzel",serif; font-size:14px;
padding:10px 30px; border-radius:8px; cursor:pointer;
letter-spacing:2px; margin-top:10px; transition:0.2s;
}
.result-close-btn:hover { border-color:#f0d060; filter:brightness(1.2); }
</style>
<style>
/* ── Deck-Stapel ────────────────────────── */
.hand-slot-deck {
position: relative;
cursor: pointer;
}
.deck-stack-wrap {
position: relative;
width: 100%;
height: calc(100% - 18px);
}
.deck-card-back,
.deck-card-top {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 7px;
box-shadow: 0 2px 8px rgba(0,0,0,0.7);
}
.deck-shadow-3 { transform: translate(-4px, -4px); }
.deck-shadow-2 { transform: translate(-2px, -2px); }
.deck-shadow-1 { transform: translate(-1px, -1px); }
.deck-card-top { transform: translate(0,0); border: 1px solid rgba(200,150,42,0.5); }
.deck-count {
position: absolute;
bottom: 2px;
left: 0; right: 0;
text-align: center;
font-family: "Cinzel", serif;
font-size: 10px;
color: #f0d060;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
.hand-slot-card {
border: 1px solid rgba(200,150,42,0.4) !important;
}
.hand-slot-card > img {
border-radius: calc(var(--s) * 7);
overflow: hidden;
}
.hand-slot-card:hover {
border-color: rgba(200,150,42,0.9) !important;
transform: translateY(-4px);
transition: transform 0.15s;
}
</style>
</head>
<body>
<div id="connecting-overlay">
<div class="spinner"></div>
<div>Warte auf Gegner…</div>
<p>Verbindung wird hergestellt</p>
</div>
<div class="board">
<div class="top-bar">
<div class="game-title"><%= title || "Spielfeld" %></div>
<div class="top-icons">
<div class="top-icon">⚙</div><div class="top-icon">🗺</div>
<div class="top-icon">📖</div><div class="top-icon">🏆</div>
</div>
<div class="top-bar-actions">
<button class="end-turn-btn" id="end-turn-btn" disabled>Zug beenden</button>
<button class="aufgeben-btn" id="aufgeben-btn">🏳 Aufgeben</button>
</div>
</div>
<div id="board-lock-overlay">
<div id="ready-timer-box">
<div id="ready-timer-label">Bereit machen</div>
<div id="ready-timer-ring">
<svg viewBox="0 0 80 80" width="80" height="80">
<circle cx="40" cy="40" r="34" class="timer-track"/>
<circle cx="40" cy="40" r="34" class="timer-fill" id="timer-circle"/>
</svg>
<div id="ready-timer-number">30</div>
</div>
<div id="ready-timer-sub">Beide Spieler müssen BEREIT klicken</div>
<div id="ready-status-row">
<div class="ready-pip" id="pip-player1">⬜ Spieler 1</div>
<div class="ready-pip" id="pip-player2">⬜ Spieler 2</div>
</div>
<button class="bereit-btn" id="bereit-btn">✔ BEREIT</button>
</div>
</div>
<div class="avatar avatar-left" id="avLeft">
<input type="file" accept="image/*" id="fileInputLeft" />
<img id="avImgL" class="av-img" />
<div class="av-placeholder" id="avPhL"><div class="av-icon">⚔</div></div>
<div class="av-name" id="nameLeft"><%= player1 || "Spieler 1" %></div>
<div class="av-stats">
<div class="stat hp"><span class="s-icon">❤</span><span class="s-val" id="hpLeft"><%= player1hp || 20 %></span></div>
<div class="stat mana"><span class="s-icon">💧</span><span class="s-val" id="manaLeft"><%= player1mana || 3 %></span></div>
</div>
<div class="hp-orb" id="orbLeft"><%= player1hp || 15 %></div>
</div>
<div class="avatar avatar-right" id="avRight">
<input type="file" accept="image/*" id="fileInputRight" />
<img id="avImgR" class="av-img" />
<div class="av-placeholder" id="avPhR"><div class="av-icon">🛡</div></div>
<div class="av-name" id="nameRight"><%= player2 || "Gegner" %></div>
<div class="av-stats">
<div class="stat hp"><span class="s-icon">❤</span><span class="s-val" id="hpRight"><%= player2hp || 20 %></span></div>
<div class="stat mana"><span class="s-icon">💧</span><span class="s-val" id="manaRight"><%= player2mana || 3 %></span></div>
</div>
<div class="hp-orb" id="orbRight"><%= player2hp || 15 %></div>
</div>
<div class="card-area">
<div class="row-label">Reihe 1</div>
<div class="card-row" id="row1"></div>
<div class="row-label">Reihe 2</div>
<div class="card-row" id="row2"></div>
</div>
<div class="bottom-bar">
<div class="hand-area" id="handArea"></div>
<div class="action-hud">
<div class="action-row">
<div class="action-btn" title="Angriff">⚔</div>
<div class="action-btn" title="Magie">✨</div>
<div class="action-btn" title="Verteidigung">🛡</div>
</div>
<div class="action-row">
<div class="action-btn" title="Heilen">💊</div>
<div class="action-btn" title="Karte ziehen">🃏</div>
<div class="action-btn" title="Einstellungen">⚙</div>
</div>
</div>
</div>
</div>
<!-- Match-Ergebnis Overlay -->
<div id="match-result-overlay">
<div class="result-title" id="result-title"></div>
<div class="result-points" id="result-points"></div>
<div class="result-levelup" id="result-levelup" style="display:none;"></div>
<div class="result-progress-wrap" id="result-progress-wrap" style="display:none;">
<div class="result-progress-label">
<span id="result-level-label">Level</span>
<span id="result-pts-label"></span>
</div>
<div class="result-progress-track">
<div class="result-progress-fill" id="result-progress-fill" style="width:0%"></div>
</div>
</div>
<button class="result-close-btn" id="result-close-btn">Schließen</button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
/* ── Spielfeld aufbauen ─────────────────────────────── */
["row1","row2"].forEach(id => {
const row = document.getElementById(id);
for (let i = 1; i <= 11; i++) {
const s = document.createElement("div");
s.className = "card-slot";
s.innerHTML = '<span class="slot-icon">✦</span><span class="slot-num">' + i + "</span>";
row.appendChild(s);
}
});
const hand = document.getElementById("handArea");
// ── Slot 1: Deck-Stapel (Rückseite oben) ─────────────
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">—</span>`;
hand.appendChild(deckSlot);
// ── Slots 24: aufgedeckte Handkarten ────────────────
const handCardIds = ["hand-card-1", "hand-card-2", "hand-card-3"];
handCardIds.forEach(id => {
const s = document.createElement("div");
s.className = "hand-slot hand-slot-card";
s.id = id;
s.innerHTML = `<img src="/images/cards/Silberklinge.png"
style="width:100%;height:100%;object-fit:cover;border-radius:7px;">`;
hand.appendChild(s);
});
// ── Slots 58: leere Slots ───────────────────────────
for (let i = 0; i < 4; i++) {
const s = document.createElement("div");
s.className = "hand-slot";
s.innerHTML = '<span class="hs-icon">🃏</span>';
hand.appendChild(s);
}
// ── Deck via API laden und Karten anzeigen ────────────
(async () => {
try {
// Deck-ID aus URL-Parameter (von arena.js mitgegeben)
const urlP = new URLSearchParams(window.location.search);
const deckId = urlP.get("deck") || sessionStorage.getItem("selectedDeckId");
if (!deckId) return;
const [deckRes, cardsRes] = await Promise.all([
fetch("/api/decks"),
fetch("/api/decks/" + deckId + "/cards")
]);
if (!deckRes.ok || !cardsRes.ok) return;
const decks = await deckRes.json();
const deck = decks.find(d => d.id == deckId);
if (deck) {
const countEl = document.getElementById("deck-count");
if (countEl) countEl.textContent = deck.card_count;
}
const cards = await cardsRes.json();
// Erste 3 Karten aufgedeckt anzeigen
handCardIds.forEach((id, i) => {
const card = cards[i];
const slot = document.getElementById(id);
if (!slot || !card) return;
// Feldnamen aus der API (attack, defends, cooldown)
const atkVal = card.attack ?? null;
const defVal = card.defends ?? null;
const cdVal = card.cooldown ?? null;
const hasAtk = atkVal != null;
const hasDef = defVal != null;
const hasCd = cdVal != null;
const showStats = hasAtk || hasDef || hasCd;
const statsHtml = showStats ? `
<div class="card-stat-overlay">
${hasAtk ? `<span class="cs-badge cs-atk">${atkVal}</span>` : ""}
${hasDef ? `<span class="cs-badge cs-def">${defVal}</span>` : ""}
${hasCd ? `<span class="cs-badge cs-cd">${cdVal}</span>` : ""}
</div>` : "";
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;">
${statsHtml}`
: `<div style="display:flex;flex-direction:column;align-items:center;
justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;">
<span style="font-size:18px;">⚔️</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>
${statsHtml}`;
});
});
} catch(e) {
console.error("[Battlefield] Deck laden:", e);
}
})();
/* ── Hilfsfunktion: Karte mit Stats in einen Slot rendern ── */
function renderCardInSlot(slot, card) {
if (!slot || !card) return;
const atkVal = card.attack ?? null;
const defVal = card.defends ?? null;
const cdVal = card.cooldown ?? null;
const hasAtk = atkVal != null;
const hasDef = defVal != null;
const hasCd = cdVal != null;
const statsHtml = (hasAtk || hasDef || hasCd) ? `
<div class="card-stat-overlay">
${hasAtk ? `<span class="cs-badge cs-atk">${atkVal}</span>` : ""}
${hasDef ? `<span class="cs-badge cs-def">${defVal}</span>` : ""}
${hasCd ? `<span class="cs-badge cs-cd">${cdVal}</span>` : ""}
</div>` : "";
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;">
${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;">⚔️</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>
${statsHtml}`;
}
/* ── Match-Daten ────────────────────────────────────── */
const urlParams = new URLSearchParams(window.location.search);
const matchId = urlParams.get("match") || "<%= matchId || '' %>";
const mySlot = urlParams.get("slot") || "<%= mySlot || 'player1' %>";
const amIPlayer1 = mySlot === "player1";
/* ── Socket ─────────────────────────────────────────── */
const socket = io();
/* Account-ID laden dann arena_join senden */
fetch("/arena/me")
.then(r => r.json())
.then(me => {
socket.emit("arena_join", { matchId, slot: mySlot, accountId: me.id });
})
.catch(() => {
socket.emit("arena_join", { matchId, slot: mySlot, accountId: null });
});
/* ── Gegner verbunden ───────────────────────────────── */
socket.on("arena_opponent_joined", data => {
const name = data.name || "Gegner";
document.getElementById(amIPlayer1 ? "nameRight" : "nameLeft").textContent = name;
document.getElementById("connecting-overlay")?.remove();
});
socket.on("arena_ready", data => {
document.getElementById("connecting-overlay")?.remove();
document.getElementById("nameLeft").textContent = data.player1 || "Spieler 1";
document.getElementById("nameRight").textContent = data.player2 || "Spieler 2";
});
/* ── Bereit-System ──────────────────────────────────── */
let myReady = false;
function handleBereit() {
if (myReady) return;
myReady = true;
const btn = document.getElementById("bereit-btn");
btn.textContent = "✔ 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 => {
const pip1 = document.getElementById("pip-player1");
const pip2 = document.getElementById("pip-player2");
if (data.readySlots) {
if (data.readySlots.includes("player1") && pip1) pip1.textContent = "✅ " + (document.getElementById("nameLeft")?.textContent || "Spieler 1");
if (data.readySlots.includes("player2") && pip2) pip2.textContent = "✅ " + (document.getElementById("nameRight")?.textContent || "Spieler 2");
}
if (data.readyCount === 2) {
document.getElementById("board-lock-overlay")?.remove();
document.getElementById("bereit-btn")?.remove();
document.getElementById("end-turn-btn").disabled = false;
}
});
/* ── Aufgeben Modal ─────────────────────────────────── */
function handleAufgeben() {
// Eigenes Modal statt browser confirm()
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;">🏳️</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ältst <strong style="color:#e74c3c;">keine Punkte</strong> für 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;">✖ Weiterkämpfen</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;">🏳 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 });
// Eigene Weiterleitung nach kurzer Verzögerung
showSurrenderMessage(false);
});
}
/* ── Nachricht anzeigen + weiterleiten ──────────────── */
function showSurrenderMessage(iWon) {
const overlay = document.createElement("div");
overlay.style.cssText = "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.9);display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:'Cinzel',serif;";
overlay.innerHTML = iWon
? `<div style="font-size:48px;margin-bottom:16px;">🏆</div>
<div style="font-size:28px;color:#f0d060;letter-spacing:4px;">SIEG!</div>
<p style="color:#a0e090;font-size:13px;margin-top:12px;">Dein Gegner hat aufgegeben.</p>
<p style="color:#606060;font-size:11px;margin-top:8px;">Du wirst weitergeleitet…</p>`
: `<div style="font-size:48px;margin-bottom:16px;">🏳️</div>
<div style="font-size:28px;color:#e74c3c;letter-spacing:4px;">AUFGEGEBEN</div>
<p style="color:#a08060;font-size:13px;margin-top:12px;">Du hast das Match aufgegeben.</p>
<p style="color:#606060;font-size:11px;margin-top:8px;">Du wirst weitergeleitet…</p>`;
document.body.appendChild(overlay);
setTimeout(() => {
// Immer zur Startseite (launcher) weiterleiten
if (window.parent && window.parent !== window) {
window.parent.document.getElementById("arena-backdrop")?.remove();
window.parent.document.getElementById("arena-popup")?.remove();
}
window.top.location.href = "/launcher";
}, 2500);
}
/* ── Spielende Events ───────────────────────────────── */
socket.on("player_surrendered", data => {
const iLost = data.slot === mySlot;
if (!iLost) {
// Ich habe gewonnen (Gegner hat aufgegeben)
showSurrenderMessage(true);
}
// Wenn ich selbst aufgegeben habe, läuft showSurrenderMessage(false) schon
});
/* Punkte-Ergebnis vom Server (kommt kurz nach player_surrendered) */
socket.on("match_result", data => {
updateResultWithPoints(data);
});
socket.on("match_cancelled", () => {
closePopup();
});
/* ── 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 ? "⚔️ SIEG!" : "💀 NIEDERLAGE";
titleEl.className = "result-title " + (won ? "win" : "lose");
pointsEl.textContent = "Punkte werden berechnet…";
overlay.classList.add("show");
if (data) updateResultWithPoints(data);
}
function updateResultWithPoints(data) {
const pointsEl = document.getElementById("result-points");
const levelupEl = document.getElementById("result-levelup");
const progressEl = document.getElementById("result-progress-wrap");
const fillEl = document.getElementById("result-progress-fill");
const lvlLbl = document.getElementById("result-level-label");
const ptsLbl = document.getElementById("result-pts-label");
/* Overlay anzeigen falls noch nicht sichtbar */
const overlay = document.getElementById("match-result-overlay");
if (!overlay.classList.contains("show")) {
document.getElementById("result-title").textContent = data.won ? "⚔️ SIEG!" : "💀 NIEDERLAGE";
document.getElementById("result-title").className = "result-title " + (data.won ? "win" : "lose");
overlay.classList.add("show");
}
pointsEl.textContent = data.awarded > 0
? "+" + data.awarded + " Arena-Punkte"
: "Keine Punkte (Aufgabe zu früh oder Tageslimit erreicht)";
if (data.level_up) {
levelupEl.style.display = "block";
levelupEl.textContent = "⬆ LEVEL UP! → Level " + data.new_level;
}
/* Fortschrittsbalken vom Server laden */
fetch("/api/points/me")
.then(r => r.json())
.then(me => {
progressEl.style.display = "block";
lvlLbl.textContent = "Level " + me.current_level + (me.next_level ? " → " + me.next_level : " (MAX)");
ptsLbl.textContent = me.points_this_level + " / " + me.points_for_next + " Pts";
requestAnimationFrame(() => {
fillEl.style.width = me.progress_percent + "%";
});
})
.catch(() => {});
}
/* ── Popup schließen → immer zu Launcher ──────────── */
function closePopup() {
if (window.parent && window.parent !== window) {
window.parent.document.getElementById("arena-backdrop")?.remove();
window.parent.document.getElementById("arena-popup")?.remove();
}
window.top.location.href = "/launcher";
}
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"); });
</script>
</body>
</html>