dok/public/js/buildings/arena.js
2026-04-07 15:32:44 +01:00

769 lines
24 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.

export async function loadArena() {
const ui = document.querySelector(".building-ui");
ui.innerHTML = `
<div id="arena-ui">
<!-- Modus-Auswahl -->
<div id="arena-mode-screen">
<div class="arena-title">⚔️ Kampfarena</div>
<p class="arena-subtitle">Wähle deinen Kampfmodus</p>
<div class="arena-modes">
<div class="arena-mode-card" data-mode="1v1">
<div class="arena-mode-icon">🗡️</div>
<div class="arena-mode-label">1v1</div>
<div class="arena-mode-desc">Einzelkampf Beweis deine Stärke im Duell</div>
</div>
<div class="arena-mode-card" data-mode="2v2">
<div class="arena-mode-icon">⚔️</div>
<div class="arena-mode-label">2v2</div>
<div class="arena-mode-desc">Verbünde dich mit einem Kameraden im Kampf</div>
</div>
<div class="arena-mode-card" data-mode="4v4">
<div class="arena-mode-icon">🛡️</div>
<div class="arena-mode-label">4v4</div>
<div class="arena-mode-desc">Schlachtruf Führe deine Truppe zum Sieg</div>
</div>
</div>
<div id="arena-queue-status" style="display:none;"></div>
</div>
<!-- 2v2 Lobby-Screen -->
<div id="arena-2v2-screen" style="display:none;">
<div class="arena-lobby-header">
<button class="arena-back-btn" id="arena-2v2-back">← Zurück</button>
<div class="arena-title" style="margin:0;">⚔️ 2v2 Team-Lobby</div>
</div>
<!-- Team-Panel (erscheint nach Beitritt) -->
<div id="arena-team-panel" style="display:none;">
<div class="arena-team-box">
<div class="arena-team-title">Dein Team</div>
<div id="arena-team-players"></div>
<div id="arena-team-actions"></div>
</div>
<div id="arena-team-status"></div>
</div>
<!-- Lobby-Liste -->
<div id="arena-lobby-section">
<div class="arena-lobby-actions">
<button class="arena-btn-create" id="arena-create-team-btn"> Eigenes Team erstellen</button>
</div>
<div class="arena-lobby-title">Offene Teams</div>
<div id="arena-lobby-list"><div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div></div>
</div>
</div>
</div>
`;
injectArenaStyles();
initArenaModes();
}
/* ── Styles ────────────────────────────────────────────────────────────────── */
function injectArenaStyles() {
if (document.getElementById("arena-popup-styles")) return;
const style = document.createElement("style");
style.id = "arena-popup-styles";
style.textContent = `
@keyframes arenaFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes arenaScaleIn {
from { transform: scale(0.94); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ── Queue Status Box ── */
#arena-queue-status {
margin-top: 18px;
padding: 12px 20px;
border-radius: 10px;
background: rgba(255, 215, 80, 0.08);
border: 1px solid rgba(255, 215, 80, 0.3);
color: #dceb15;
font-family: "Cinzel", serif;
font-size: 13px;
letter-spacing: 1px;
text-align: center;
animation: pulse 2s ease-in-out infinite;
}
#arena-queue-status .qs-cancel {
display: inline-block;
margin-top: 8px;
font-size: 11px;
color: rgba(255,100,100,0.8);
cursor: pointer;
text-decoration: underline;
animation: none;
}
#arena-queue-status .qs-cancel:hover {
color: #e74c3c;
}
/* ── Backdrop ── */
#arena-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.82);
backdrop-filter: blur(5px);
z-index: 9998;
animation: arenaFadeIn 0.25s ease;
}
/* ── Popup ── */
#arena-popup {
position: fixed;
inset: 50px;
z-index: 9999;
display: flex;
flex-direction: column;
border-radius: 14px;
overflow: hidden;
box-shadow:
0 0 0 1px rgba(255, 215, 80, 0.35),
0 30px 90px rgba(0, 0, 0, 0.85),
0 0 60px rgba(255, 215, 80, 0.08);
animation: arenaScaleIn 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ── Titelleiste ── */
#arena-popup-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(10, 8, 5, 0.95);
border-bottom: 1px solid rgba(255, 215, 80, 0.3);
padding: 0 16px;
height: 42px;
flex-shrink: 0;
user-select: none;
}
#arena-popup-titlebar .ap-left {
display: flex;
align-items: center;
gap: 10px;
}
#arena-popup-titlebar .ap-dots {
display: flex;
gap: 7px;
}
#arena-popup-titlebar .ap-dot {
width: 13px;
height: 13px;
border-radius: 50%;
cursor: pointer;
transition: filter 0.15s;
}
#arena-popup-titlebar .ap-dot:hover { filter: brightness(1.3); }
#arena-popup-titlebar .ap-dot.close { background: #e74c3c; border: 1px solid rgba(0,0,0,0.25); }
#arena-popup-titlebar .ap-dot.min { background: #f1c40f; border: 1px solid rgba(0,0,0,0.25); }
#arena-popup-titlebar .ap-dot.expand { background: #2ecc71; border: 1px solid rgba(0,0,0,0.25); }
#arena-popup-titlebar .ap-title {
font-family: "Cinzel", serif;
font-size: 13px;
letter-spacing: 4px;
color: rgba(255, 215, 80, 0.85);
text-transform: uppercase;
text-shadow: 0 1px 8px rgba(0,0,0,0.8);
}
#arena-popup-titlebar .ap-url {
font-size: 11px;
color: rgba(255,255,255,0.22);
letter-spacing: 1px;
}
/* ── Match-Found Overlay ── */
#match-found-overlay {
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(0,0,0,0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
animation: arenaFadeIn 0.3s ease;
}
#match-found-overlay .mfo-title {
font-family: "Cinzel", serif;
font-size: 36px;
color: #ffd750;
text-shadow: 0 0 30px rgba(255,215,80,0.6);
letter-spacing: 6px;
margin-bottom: 12px;
}
#match-found-overlay .mfo-vs {
font-family: "Cinzel", serif;
font-size: 18px;
color: rgba(255,255,255,0.75);
letter-spacing: 3px;
}
#match-found-overlay .mfo-bar {
width: 300px;
height: 4px;
background: rgba(255,215,80,0.2);
border-radius: 2px;
margin-top: 24px;
overflow: hidden;
}
#match-found-overlay .mfo-bar-fill {
height: 100%;
background: #ffd750;
width: 0%;
border-radius: 2px;
transition: width 1.5s ease;
}
/* ── iframe ── */
#arena-popup iframe {
flex: 1;
border: none;
width: 100%;
display: block;
}
/* ── Card: gesucht-Zustand ── */
.arena-mode-card.searching {
opacity: 0.6;
pointer-events: none;
border-color: rgba(255,215,80,0.5) !important;
}
/* ── 2v2 Lobby Screen ── */
#arena-2v2-screen {
flex-direction: column;
gap: 14px;
padding: 16px;
height: 100%;
overflow-y: auto;
}
.arena-lobby-header {
display: flex;
align-items: center;
gap: 16px;
}
.arena-back-btn {
background: none;
border: 1px solid rgba(255,200,80,0.3);
color: #c8960c;
font-family: "Cinzel", serif;
font-size: 12px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
}
.arena-back-btn:hover { background: rgba(200,150,12,0.15); }
.arena-lobby-actions { display: flex; gap: 10px; margin-bottom: 4px; }
.arena-btn-create {
background: linear-gradient(#4a3018, #2a1a08);
border: 2px solid #8b6a3c;
border-radius: 7px;
color: #f0d9a6;
font-family: "Cinzel", serif;
font-size: 12px;
padding: 8px 16px;
cursor: pointer;
transition: 0.2s;
}
.arena-btn-create:hover { border-color: #f0d060; }
.arena-lobby-title {
font-family: "Cinzel", serif;
font-size: 13px;
color: #a08060;
letter-spacing: 1px;
margin-bottom: 6px;
}
.arena-lobby-empty {
font-family: "Cinzel", serif;
font-size: 12px;
color: #606060;
padding: 12px 0;
}
.arena-lobby-row {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(#2a1a08, #1a0f04);
border: 1px solid #6b4b2a;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 6px;
}
.arena-lobby-row-info { display: flex; align-items: center; gap: 12px; }
.arena-lobby-leader { font-family: "Cinzel", serif; font-size: 13px; color: #f0d9a6; }
.arena-lobby-level { font-size: 11px; color: #a08060; }
.arena-lobby-count { font-size: 11px; color: #6a9a4a; }
.arena-btn-join {
background: linear-gradient(#1a4a18, #0f2a0e);
border: 2px solid #4a8a3c;
border-radius: 6px;
color: #a0e090;
font-family: "Cinzel", serif;
font-size: 11px;
padding: 5px 12px;
cursor: pointer;
transition: 0.2s;
}
.arena-btn-join:hover { border-color: #8ae060; color: #c0f0a0; }
/* ── Team-Panel ── */
.arena-team-box {
background: linear-gradient(#2a1a08, #1a0f04);
border: 2px solid #6b4b2a;
border-radius: 10px;
padding: 14px;
}
.arena-team-title {
font-family: "Cinzel", serif;
font-size: 13px;
color: #f0d060;
letter-spacing: 2px;
margin-bottom: 10px;
}
.arena-team-player {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border: 1px solid #3a2810;
border-radius: 6px;
margin-bottom: 5px;
background: rgba(255,255,255,0.03);
}
.arena-team-player.ready { border-color: #4a8a3c; background: rgba(74,138,60,0.1); }
.arena-team-player-name { font-family: "Cinzel", serif; font-size: 12px; color: #f0d9a6; flex: 1; }
.arena-team-player-level { font-size: 11px; color: #a08060; }
.arena-team-player-status { font-size: 11px; }
.arena-waiting-partner {
font-family: "Cinzel", serif;
font-size: 11px;
color: #a08060;
text-align: center;
padding: 8px;
animation: pulse 2s ease-in-out infinite;
}
.arena-btn-ready {
width: 100%;
margin-top: 10px;
background: linear-gradient(#1a4a18, #0f2a0e);
border: 2px solid #4a8a3c;
border-radius: 8px;
color: #a0e090;
font-family: "Cinzel", serif;
font-size: 14px;
padding: 10px;
cursor: pointer;
transition: 0.2s;
}
.arena-btn-ready:hover:not([disabled]) { border-color: #8ae060; background: linear-gradient(#2a6a28, #1a3a18); }
.arena-btn-ready.active { border-color: #8ae060; color: #c0f0a0; cursor: default; }
.arena-searching-box {
font-family: "Cinzel", serif;
font-size: 12px;
color: #dceb15;
text-align: center;
padding: 10px;
border: 1px solid rgba(255,215,80,0.3);
border-radius: 8px;
margin-top: 8px;
animation: pulse 2s ease-in-out infinite;
}
`;
document.head.appendChild(style);
}
/* ── Socket-Referenz holen ─────────────────────────────────────────────────── */
function getSocket() {
// Wird von der Haupt-App als window._socket bereitgestellt
return window._socket || null;
}
/* ── Klick-Handler initialisieren ─────────────────────────────────────────── */
function initArenaModes() {
document.querySelectorAll(".arena-mode-card").forEach((card) => {
card.addEventListener("click", () => {
const mode = card.dataset.mode;
if (mode === "1v1") {
handle1v1Click(card);
} else if (mode === "2v2") {
open2v2Lobby();
} else {
console.log("Arena Modus gewählt:", mode);
}
});
});
}
/* ══════════════════════════════════════════════════════════
2v2 LOBBY
══════════════════════════════════════════════════════════ */
let my2v2TeamId = null;
let myArenaData = null;
async function open2v2Lobby() {
const socket = getSocket();
if (!socket) { showArenaError("Keine Verbindung zum Server."); return; }
// Spielerdaten laden
if (!myArenaData) {
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error(res.status);
myArenaData = await res.json();
} catch {
showArenaError("Spielerdaten konnten nicht geladen werden.");
return;
}
}
// Screen wechseln
document.getElementById("arena-mode-screen").style.display = "none";
document.getElementById("arena-2v2-screen").style.display = "flex";
// Lobbyliste anfordern
socket.emit("get_2v2_lobbies");
// Zurück-Button
document.getElementById("arena-2v2-back").onclick = () => {
leave2v2Team(socket);
document.getElementById("arena-2v2-screen").style.display = "none";
document.getElementById("arena-mode-screen").style.display = "";
my2v2TeamId = null;
};
// Team erstellen
document.getElementById("arena-create-team-btn").onclick = () => {
socket.emit("create_2v2_team", myArenaData);
};
// Socket-Listener registrieren (einmalig)
socket.off("2v2_lobbies");
socket.off("2v2_team_joined");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
socket.off("match_found_2v2");
socket.off("2v2_error");
socket.on("2v2_lobbies", (list) => render2v2LobbyList(list, socket));
socket.on("2v2_team_joined", (data) => {
my2v2TeamId = data.teamId;
document.getElementById("arena-team-panel").style.display = "block";
document.getElementById("arena-lobby-section").style.display = "none";
});
socket.on("2v2_team_update", (data) => render2v2TeamPanel(data, socket));
socket.on("2v2_partner_left", (data) => {
const status = document.getElementById("arena-team-status");
if (status) status.innerHTML = `<span style="color:#e74c3c;">⚠️ ${data.name} hat das Team verlassen.</span>`;
// Bereit-Button zurücksetzen
render2v2TeamPanel({ teamId: my2v2TeamId, players: [{ name: myArenaData.name, level: myArenaData.level, ready: false }], count: 1 }, socket);
});
socket.on("2v2_searching", () => {
const status = document.getElementById("arena-team-status");
if (status) status.innerHTML = `<div class="arena-searching-box">⏳ Suche nach Gegnerteam…</div>`;
const actions = document.getElementById("arena-team-actions");
if (actions) actions.innerHTML = "";
});
socket.on("match_found_2v2", (data) => {
socket.off("2v2_lobbies");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
showMatchFoundOverlay(myArenaData.name, `Team ${data.myTeam === 1 ? 2 : 1}`, () => {
document.getElementById("arena-2v2-screen").style.display = "none";
document.getElementById("arena-mode-screen").style.display = "";
openArenaPopup(
`/arena/2v2?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`,
data.opponents?.join(" & ") || "Gegner",
data.matchId,
);
});
});
socket.on("2v2_error", (data) => {
const status = document.getElementById("arena-team-status");
if (status) { status.innerHTML = `<span style="color:#e74c3c;">❌ ${data.message}</span>`; }
});
}
function render2v2LobbyList(list, socket) {
const el = document.getElementById("arena-lobby-list");
if (!el) return;
if (!list.length) {
el.innerHTML = `<div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div>`;
return;
}
el.innerHTML = list.map(team => `
<div class="arena-lobby-row">
<div class="arena-lobby-row-info">
<span class="arena-lobby-leader">⚔️ ${team.leader}</span>
<span class="arena-lobby-level">Lvl ${team.leaderLevel}</span>
<span class="arena-lobby-count">${team.count}/2 Spieler</span>
</div>
<button class="arena-btn-join" data-teamid="${team.teamId}">Beitreten</button>
</div>
`).join("");
el.querySelectorAll(".arena-btn-join").forEach(btn => {
btn.addEventListener("click", () => {
socket.emit("join_2v2_team", { teamId: btn.dataset.teamid, playerData: myArenaData });
});
});
}
function render2v2TeamPanel(data, socket) {
const playersEl = document.getElementById("arena-team-players");
const actionsEl = document.getElementById("arena-team-actions");
const statusEl = document.getElementById("arena-team-status");
if (!playersEl || !actionsEl) return;
playersEl.innerHTML = data.players.map(p => `
<div class="arena-team-player ${p.ready ? 'ready' : ''}">
<span class="arena-team-player-name">${p.name}</span>
<span class="arena-team-player-level">Lvl ${p.level}</span>
<span class="arena-team-player-status">${p.ready ? '✅ Bereit' : '⌛ Wartet'}</span>
</div>
`).join("");
if (data.count < 2) {
actionsEl.innerHTML = `<div class="arena-waiting-partner">Warte auf Partner…</div>`;
if (statusEl) statusEl.innerHTML = "";
} else {
const myEntry = data.players.find(p => p.name === myArenaData?.name);
const iAmReady = myEntry?.ready;
actionsEl.innerHTML = iAmReady
? `<button class="arena-btn-ready active" disabled>✅ Du bist bereit</button>`
: `<button class="arena-btn-ready" id="arena-ready-btn">⚔️ Bereit</button>`;
if (!iAmReady) {
document.getElementById("arena-ready-btn")?.addEventListener("click", () => {
socket.emit("2v2_player_ready", { teamId: my2v2TeamId });
});
}
}
}
function leave2v2Team(socket) {
if (my2v2TeamId) {
socket.emit("leave_2v2_team");
my2v2TeamId = null;
}
socket.off("2v2_lobbies");
socket.off("2v2_team_joined");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
socket.off("match_found_2v2");
socket.off("2v2_error");
}
/* ── 1v1: Hauptlogik ───────────────────────────────────────────────────────── */
async function handle1v1Click(card) {
const socket = getSocket();
if (!socket) {
showArenaError("Keine Verbindung zum Server. Bitte Seite neu laden.");
return;
}
// Bereits in Suche?
if (card.classList.contains("searching")) return;
// Spielerdaten laden
let me;
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error("Status " + res.status);
me = await res.json();
} catch (err) {
console.error("[1v1] Spielerdaten konnten nicht geladen werden:", err);
showArenaError("Spielerdaten konnten nicht geladen werden. (Eingeloggt?)");
return;
}
// UI: Suche läuft
setCardSearching(card, true);
showQueueStatus(me.level);
// Sicherstellen, dass keine alten Listener hängen
socket.off("match_found");
socket.off("queue_status");
// Queue-Status empfangen
socket.on("queue_status", (data) => {
if (data.status === "waiting") {
showQueueStatus(me.level, data.poolSize);
} else if (data.status === "left") {
setCardSearching(card, false);
hideQueueStatus();
}
});
// Match gefunden
socket.once("match_found", (data) => {
socket.off("queue_status");
setCardSearching(card, false);
hideQueueStatus();
showMatchFoundOverlay(me.name, data.opponent.name, () => {
openArenaPopup(
`/arena/1v1?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`,
data.opponent.name,
data.matchId,
);
});
});
// Matchmaking starten
socket.emit("join_1v1", {
id: me.id,
name: me.name,
level: me.level,
});
}
/* ── Queue abbrechen ───────────────────────────────────────────────────────── */
function cancelQueue(card) {
const socket = getSocket();
if (socket) {
socket.emit("leave_1v1");
socket.off("match_found");
socket.off("queue_status");
}
setCardSearching(card, false);
hideQueueStatus();
}
/* ── Match-Found Splash ────────────────────────────────────────────────────── */
function showMatchFoundOverlay(myName, opponentName, onDone) {
// Verhindert doppeltes Öffnen
if (document.getElementById("match-found-overlay")) return;
const overlay = document.createElement("div");
overlay.id = "match-found-overlay";
overlay.innerHTML = `
<div class="mfo-title">⚔️ Match gefunden!</div>
<div class="mfo-vs">${myName} &nbsp;vs&nbsp; ${opponentName}</div>
<div class="mfo-bar"><div class="mfo-bar-fill" id="mfBar"></div></div>
`;
document.body.appendChild(overlay);
// Ladebalken animieren
requestAnimationFrame(() => {
const bar = document.getElementById("mfBar");
if (bar) bar.style.width = "100%";
});
// Nach 1.6s zum Spielfeld
setTimeout(() => {
overlay.remove();
onDone();
}, 1600);
}
/* ── Popup öffnen ──────────────────────────────────────────────────────────── */
function openArenaPopup(src, opponentName, matchId) {
// Vorhandenen Popup schließen (falls mehrfach)
document.getElementById("arena-backdrop")?.remove();
document.getElementById("arena-popup")?.remove();
const backdrop = document.createElement("div");
backdrop.id = "arena-backdrop";
const popup = document.createElement("div");
popup.id = "arena-popup";
const title = opponentName
? `⚔️ 1v1 &nbsp;·&nbsp; vs ${opponentName}`
: "⚔️ Arena &nbsp;·&nbsp; 1v1";
popup.innerHTML = `
<div id="arena-popup-titlebar">
<div class="ap-left">
<span class="ap-title">${title}</span>
</div>
<span class="ap-url">${matchId || src}</span>
</div>
<iframe src="${src}" allowfullscreen></iframe>
`;
document.body.appendChild(backdrop);
document.body.appendChild(popup);
// Backdrop-Klick deaktiviert Fenster nur über Aufgeben/Spielende schließbar
}
/* ── UI Hilfsfunktionen ────────────────────────────────────────────────────── */
function setCardSearching(card, searching) {
const label = card.querySelector(".arena-mode-label");
const desc = card.querySelector(".arena-mode-desc");
if (searching) {
card.classList.add("searching");
label.textContent = "⏳ Suche…";
desc.textContent = "Warte auf passenden Gegner…";
} else {
card.classList.remove("searching");
label.textContent = "1v1";
desc.textContent = "Einzelkampf Beweis deine Stärke im Duell";
}
}
function showQueueStatus(myLevel, poolSize) {
const box = document.getElementById("arena-queue-status");
if (!box) return;
const range = 5;
const min = Math.max(1, myLevel - range);
const max = myLevel + range;
const pool = poolSize ? ` · ${poolSize} Spieler im Pool` : "";
box.style.display = "block";
box.innerHTML = `
⏳ Suche Gegner (Level ${min}${max})${pool}
<br>
<span class="qs-cancel" id="qs-cancel-btn">Suche abbrechen</span>
`;
// Cancel-Button Card-Referenz über data-attribute
document.getElementById("qs-cancel-btn")?.addEventListener("click", () => {
const card = document.querySelector(".arena-mode-card[data-mode='1v1']");
if (card) cancelQueue(card);
});
}
function hideQueueStatus() {
const box = document.getElementById("arena-queue-status");
if (box) box.style.display = "none";
}
function showArenaError(msg) {
const box = document.getElementById("arena-queue-status");
if (!box) return;
box.style.display = "block";
box.style.animation = "none";
box.style.borderColor = "rgba(231,76,60,0.5)";
box.style.color = "#e74c3c";
box.textContent = "❌ " + msg;
setTimeout(() => {
box.style.display = "none";
box.style.animation = "";
box.style.borderColor = "";
box.style.color = "";
}, 3000);
}