dok/views/1v1-battlefield.ejs
2026-04-09 18:34:39 +01:00

467 lines
21 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>
/* ── 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:hover {
border-color: rgba(200,150,42,0.9) !important;
transform: translateY(-4px);
transition: transform 0.15s;
}
</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";
if (id === "row1" && i === 1) {
s.innerHTML = `<img src="/images/cards/Silberklinge.png" style="width:100%;height:100%;object-fit:cover;border-radius:7px;">`;
} else {
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/cards/rueckseite.png">
<img class="deck-card-back deck-shadow-2" src="/images/cards/rueckseite.png">
<img class="deck-card-back deck-shadow-1" src="/images/cards/rueckseite.png">
<img class="deck-card-top" src="/images/cards/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 sessionStorage (wird von arena.js gesetzt)
const deckId = 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;
slot.innerHTML = card.image
? `<img src="/images/cards/${card.image}"
onerror="this.src='/images/cards/rueckseite.png'"
title="${card.name}"
style="width:100%;height:100%;object-fit:cover;border-radius:7px;">`
: `<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>`;
});
} catch(e) {
console.error("[Battlefield] Deck laden:", e);
}
})();
/* ── 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 ───────────────────────────────────────── */
function handleAufgeben() {
if (!confirm("Wirklich aufgeben? Du erhältst keine Punkte.")) return;
socket.emit("player_surrender", { matchId, slot: mySlot });
}
/* ── Spielende Events ───────────────────────────────── */
socket.on("player_surrendered", data => {
/* Overlay sofort anzeigen, Punkte kommen per match_result */
const iLost = data.slot === mySlot;
showResultOverlay(iLost ? false : true, null);
});
/* 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 ────────────────────────────────── */
function closePopup() {
if (window.parent && window.parent !== window) {
window.parent.document.getElementById("arena-backdrop")?.remove();
window.parent.document.getElementById("arena-popup")?.remove();
} else {
window.close();
}
}
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>