dok/public/js/buildings/himmelstor.js
2026-04-14 07:50:14 +01:00

383 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/* ============================================================
public/js/buildings/himmelstor.js
Himmelstor Tages- und Wochenherausforderung
Gleiche Struktur wie arena.js, aber Daily & Weekly
============================================================ */
export async function loadHimmelstor() {
const ui = document.querySelector(".building-ui");
if (!ui) return;
/* ── Decks laden ─────────────────────────────────────────── */
let decks = [];
try {
const res = await fetch("/api/decks");
if (res.ok) decks = await res.json();
} catch (e) { console.error("[Himmelstor] Decks:", e); }
const deckOptions = decks.length === 0
? `<option value=""> Erst Deck erstellen </option>`
: `<option value=""> Deck wählen </option>` +
decks.filter(d => d.card_count > 0)
.map(d => `<option value="${d.id}">${d.name} (${d.card_count} Karten)</option>`)
.join("") +
(decks.some(d => d.card_count === 0)
? decks.filter(d => d.card_count === 0)
.map(d => `<option value="" disabled>⚠ ${d.name} (leer)</option>`)
.join("")
: "");
ui.innerHTML = `
<div id="himmelstor-ui">
<div id="himmelstor-mode-screen">
<div class="ht-title">🌤️ Himmelstor</div>
<p class="ht-subtitle">Wähle Deck und Herausforderung</p>
<div class="ht-modes">
<div class="ht-mode-wrap">
<div class="ht-mode-card ht-mode-locked" data-mode="daily">
<div class="ht-mode-icon">☀️</div>
<div class="ht-mode-label">Daily</div>
<div class="ht-mode-desc">Tagesherausforderung</div>
</div>
<select class="ht-deck-select" data-mode="daily">
${deckOptions}
</select>
</div>
<div class="ht-mode-wrap">
<div class="ht-mode-card ht-mode-locked" data-mode="weekly">
<div class="ht-mode-icon">🌙</div>
<div class="ht-mode-label">Weekly</div>
<div class="ht-mode-desc">Wochenherausforderung</div>
</div>
<select class="ht-deck-select" data-mode="weekly">
${deckOptions}
</select>
</div>
</div>
${decks.length === 0
? `<div class="ht-no-deck-hint">⚠️ Kein Deck vorhanden gehe zur <strong>Burg → Kartendeck</strong></div>`
: ""}
<div id="ht-queue-status" style="display:none;"></div>
</div>
</div>`;
injectHimmelstorStyles();
initHimmelstorModes();
}
/* ── Styles ──────────────────────────────────────────────── */
function injectHimmelstorStyles() {
if (document.getElementById("ht-popup-styles")) return;
const style = document.createElement("style");
style.id = "ht-popup-styles";
style.textContent = `
@keyframes htFadeIn { from{opacity:0} to{opacity:1} }
@keyframes htScaleIn { from{transform:scale(0.94);opacity:0} to{transform:scale(1);opacity:1} }
@keyframes htPulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
#himmelstor-ui { display:flex; flex-direction:column; height:100%; }
#himmelstor-mode-screen {
display:flex; flex-direction:column; align-items:center; padding:16px;
}
.ht-title {
font-family:"Cinzel",serif; font-size:18px; color:#f0d060;
letter-spacing:3px; text-align:center; margin-bottom:4px;
}
.ht-subtitle {
font-family:"Cinzel",serif; font-size:11px; color:#a08060;
margin:0 0 14px;
}
.ht-modes {
display:flex; gap:10px; justify-content:center;
flex-wrap:wrap; width:100%;
}
.ht-mode-wrap {
flex:1; min-width:120px; max-width:195px;
display:flex; flex-direction:column; gap:6px;
}
.ht-mode-card {
flex:1; min-width:120px; max-width:195px;
background:linear-gradient(180deg,#1a1a2e,#0f0f1a);
border:2px solid #4a3a6b; border-radius:12px;
padding:21px 12px; cursor:pointer; text-align:center;
transition:border-color .2s, transform .1s;
box-shadow: inset 0 1px 0 rgba(160,120,255,0.1), 0 4px 16px rgba(0,0,0,0.6);
}
.ht-mode-card:hover { border-color:#9b72cf; transform:translateY(-2px); }
.ht-mode-card.searching {
opacity:.6; pointer-events:none;
border-color:rgba(155,114,207,.5) !important;
}
.ht-mode-locked {
opacity:.45; cursor:not-allowed !important;
border-color:#2a1e40 !important;
}
.ht-mode-locked:hover { transform:none !important; border-color:#2a1e40 !important; }
.ht-mode-icon { font-size:36px; margin-bottom:9px; }
.ht-mode-label {
font-family:"Cinzel",serif; font-size:19px;
color:#c8a0ff; font-weight:bold;
}
.ht-mode-desc { font-size:15px; color:#6a5080; margin-top:6px; line-height:1.4; }
.ht-deck-select {
width:100%; background:#0f0f1a; border:1px solid #4a3a6b;
border-radius:6px; color:#e0d0ff; font-family:"Cinzel",serif;
font-size:15px; padding:8px 9px; cursor:pointer;
appearance:none; -webkit-appearance:none;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23806090'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 6px center;
padding-right:28px;
}
.ht-deck-select:focus { outline:none; border-color:#9b72cf; }
.ht-deck-select option { background:#0f0f1a; color:#e0d0ff; }
.ht-no-deck-hint {
margin-top:10px; padding:8px 12px; border-radius:7px;
background:rgba(231,76,60,.1); border:1px solid rgba(231,76,60,.3);
color:#e07060; font-family:"Cinzel",serif; font-size:11px; text-align:center;
}
.ht-no-deck-hint strong { color:#c8a0ff; }
#ht-queue-status {
margin-top:12px; padding:10px 18px; border-radius:10px;
width:100%; box-sizing:border-box;
background:rgba(155,114,207,.08); border:1px solid rgba(155,114,207,.3);
color:#c8a0ff; font-family:"Cinzel",serif; font-size:12px;
letter-spacing:1px; text-align:center; animation:htPulse 2s ease-in-out infinite;
}
.ht-cancel {
display:inline-block; margin-top:6px; font-size:11px;
color:rgba(255,100,100,.8); cursor:pointer; text-decoration:underline;
animation:none;
}
.ht-cancel:hover { color:#e74c3c; }
/* Match-Found Overlay */
#ht-match-found-overlay {
position:fixed; inset:0; z-index:10000;
background:rgba(0,0,0,.9); display:flex; flex-direction:column;
align-items:center; justify-content:center; animation:htFadeIn .3s ease;
}
.ht-mfo-title {
font-family:"Cinzel",serif; font-size:36px; color:#c8a0ff;
text-shadow:0 0 30px rgba(155,114,207,.6);
letter-spacing:6px; margin-bottom:12px;
}
.ht-mfo-vs {
font-family:"Cinzel",serif; font-size:18px;
color:rgba(255,255,255,.75); letter-spacing:3px;
}
.ht-mfo-bar {
width:300px; height:4px; background:rgba(155,114,207,.2);
border-radius:2px; margin-top:24px; overflow:hidden;
}
.ht-mfo-bar-fill {
height:100%; background:#9b72cf; width:0%;
border-radius:2px; transition:width 1.5s ease;
}
/* Popup iframe */
#ht-backdrop {
position:fixed; inset:0; background:rgba(0,0,0,.82);
backdrop-filter:blur(5px); z-index:9998; animation:htFadeIn .25s ease;
}
#ht-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(155,114,207,.35),0 30px 90px rgba(0,0,0,.85);
animation:htScaleIn .28s cubic-bezier(.22,1,.36,1);
}
#ht-popup-titlebar {
display:flex; align-items:center; justify-content:space-between;
background:rgba(10,8,20,.95); border-bottom:1px solid rgba(155,114,207,.3);
padding:0 16px; height:42px; flex-shrink:0;
}
#ht-popup-titlebar .ht-ap-title {
font-family:"Cinzel",serif; font-size:13px; letter-spacing:4px;
color:rgba(200,160,255,.85); text-transform:uppercase;
}
#ht-popup-titlebar .ht-ap-url { font-size:11px; color:rgba(255,255,255,.22); }
#ht-popup iframe { flex:1; border:none; width:100%; display:block; }
`;
document.head.appendChild(style);
}
/* ── State ───────────────────────────────────────────────── */
let htSelectedDeckId = null;
/* ── Modus-Initialisierung ───────────────────────────────── */
function initHimmelstorModes() {
/* Deck-Dropdown: Karte freischalten wenn Deck gewählt */
document.querySelectorAll(".ht-deck-select").forEach(select => {
select.addEventListener("change", () => {
const mode = select.dataset.mode;
const card = document.querySelector(`.ht-mode-card[data-mode="${mode}"]`);
if (!card) return;
if (select.value) {
card.classList.remove("ht-mode-locked");
htSelectedDeckId = Number(select.value);
} else {
card.classList.add("ht-mode-locked");
}
});
});
/* Modus-Karte klicken */
document.querySelectorAll(".ht-mode-card").forEach(card => {
card.addEventListener("click", () => {
if (card.classList.contains("ht-mode-locked")) return;
const mode = card.dataset.mode;
const select = document.querySelector(`.ht-deck-select[data-mode="${mode}"]`);
htSelectedDeckId = select ? Number(select.value) : null;
if (!htSelectedDeckId) return;
sessionStorage.setItem("selectedDeckId", htSelectedDeckId);
handleHtModeClick(card, mode);
});
});
}
/* ── Modus klicken ───────────────────────────────────────── */
async function handleHtModeClick(card, mode) {
if (card.classList.contains("searching")) return;
let me;
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error("Status " + res.status);
me = await res.json();
} catch {
showHtError("Spielerdaten konnten nicht geladen werden.");
return;
}
setHtCardSearching(card, mode, true);
showHtQueueStatus(mode);
const socket = window._socket;
if (!socket) { showHtError("Keine Verbindung zum Server."); return; }
socket.off("ht_match_found");
socket.off("ht_queue_status");
socket.on("ht_queue_status", data => {
if (data.status === "waiting") showHtQueueStatus(mode, data.poolSize);
else if (data.status === "left") { setHtCardSearching(card, mode, false); hideHtQueueStatus(); }
});
socket.once("ht_match_found", data => {
socket.off("ht_queue_status");
setHtCardSearching(card, mode, false);
hideHtQueueStatus();
showHtMatchFoundOverlay(me.name, data.opponent?.name || "Gegner", () => {
openHtPopup(
`/himmelstor/${mode}?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}&deck=${encodeURIComponent(htSelectedDeckId || '')}&opponent=${encodeURIComponent(data.opponent?.name || '')}`,
data.opponent?.name || "Gegner", data.matchId
);
});
});
socket.emit("ht_join", {
id: me.id, name: me.name, level: me.level, mode,
});
}
/* ── UI Hilfsfunktionen ──────────────────────────────────── */
const HT_LABELS = { daily: "Daily", weekly: "Weekly" };
const HT_DESCS = { daily: "Tagesherausforderung", weekly: "Wochenherausforderung" };
function setHtCardSearching(card, mode, searching) {
const label = card.querySelector(".ht-mode-label");
const desc = card.querySelector(".ht-mode-desc");
if (searching) {
card.classList.add("searching");
label.textContent = "⏳ Suche…";
desc.textContent = "Warte auf Gegner…";
} else {
card.classList.remove("searching");
label.textContent = HT_LABELS[mode] || mode;
desc.textContent = HT_DESCS[mode] || "";
}
}
function showHtQueueStatus(mode, poolSize) {
const box = document.getElementById("ht-queue-status");
if (!box) return;
const pool = poolSize ? ` · ${poolSize} im Pool` : "";
const label = mode === "daily" ? "Tagesherausforderung" : "Wochenherausforderung";
box.style.display = "block";
box.innerHTML = `⏳ Suche ${label}${pool}
<br><span class="ht-cancel" id="ht-cancel-btn">Suche abbrechen</span>`;
document.getElementById("ht-cancel-btn")?.addEventListener("click", () => {
cancelHtQueue(document.querySelector(`.ht-mode-card[data-mode="${mode}"]`), mode);
});
}
function hideHtQueueStatus() {
const box = document.getElementById("ht-queue-status");
if (box) box.style.display = "none";
}
function showHtError(msg) {
const box = document.getElementById("ht-queue-status");
if (!box) return;
box.style.cssText += ";display:block;animation:none;border-color:rgba(231,76,60,.5);color:#e74c3c;";
box.textContent = "❌ " + msg;
setTimeout(() => {
box.style.display = "none";
box.style.animation = "";
box.style.borderColor = "";
box.style.color = "";
}, 3000);
}
function cancelHtQueue(card, mode) {
const socket = window._socket;
if (socket) { socket.emit("ht_leave", { mode }); socket.off("ht_match_found"); socket.off("ht_queue_status"); }
if (card) setHtCardSearching(card, mode, false);
hideHtQueueStatus();
}
function showHtMatchFoundOverlay(myName, opponentName, onDone) {
if (document.getElementById("ht-match-found-overlay")) return;
const overlay = document.createElement("div");
overlay.id = "ht-match-found-overlay";
overlay.innerHTML = `
<div class="ht-mfo-title">🌤️ Herausforderung!</div>
<div class="ht-mfo-vs">${myName} &nbsp;vs&nbsp; ${opponentName}</div>
<div class="ht-mfo-bar"><div class="ht-mfo-bar-fill" id="htMfBar"></div></div>`;
document.body.appendChild(overlay);
requestAnimationFrame(() => {
const b = document.getElementById("htMfBar");
if (b) b.style.width = "100%";
});
setTimeout(() => { overlay.remove(); onDone(); }, 1600);
}
function openHtPopup(src, opponentName, matchId) {
document.getElementById("ht-backdrop")?.remove();
document.getElementById("ht-popup")?.remove();
const backdrop = document.createElement("div"); backdrop.id = "ht-backdrop";
const popup = document.createElement("div"); popup.id = "ht-popup";
popup.innerHTML = `
<div id="ht-popup-titlebar">
<span class="ht-ap-title">🌤️ Himmelstor · vs ${opponentName}</span>
<span class="ht-ap-url">${matchId || src}</span>
</div>
<iframe src="${src}" allowfullscreen></iframe>`;
document.body.appendChild(backdrop);
document.body.appendChild(popup);
}