dok/public/js/quickmenu/events.js
2026-04-14 11:29:11 +01:00

1191 lines
41 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

/* ================================
Kristall-Mapping (aus carddeck.js)
================================ */
/* ================================
Stat-Icons SVG
================================ */
const BS_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 BS_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>`;
const RARITY_CRYSTALS = {
1: "roter-cristal.png",
2: "blauer-cristal.png",
3: "gelber-cristal.png",
4: "gruener-cristal.png",
5: "oranger-cristal.png",
6: "violet-cristal.png",
7: "pinker-cristal.png",
};
function rarityImgs(rarity, size = 13) {
const file = RARITY_CRYSTALS[String(rarity)];
if (!file) return "";
const count = parseInt(rarity) || 0;
const img = `<img src="/images/items/${file}" alt="Stufe ${rarity}" style="width:${size}px;height:${size}px;object-fit:contain;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.8));">`;
return img.repeat(count);
}
function cardHTML(card, isFront = true) {
if (!isFront)
return `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
// image zuerst, dann icon als Fallback
const imgFile = card?.image || card?.icon || null;
const img = imgFile
? `/images/cards/${imgFile}`
: "/images/items/rueckseite.png";
return `
<img class="booster-slot-img" src="${img}" alt="${card?.name || ""}" draggable="false"
onerror="this.src='/images/items/rueckseite.png'">
${card?.attack != null ? `<span class="bs-stat-atk">${card.attack}</span>` : ""}
${card?.defends != null ? `<span class="bs-stat-def">${card.defends}</span>` : ""}
${card?.cooldown != null ? `<span class="bs-stat-cd">${card.cooldown}</span>` : ""}
${card?.rarity ? `<div class="bs-rarity">${rarityImgs(card.rarity, 11)}</div>` : ""}
${
card?.range != null || card?.race != null
? `
<div class="bs-range-race">
${card?.range != null ? `<span class="bs-badge-range">${BS_SVG_RANGE}&thinsp;${card.range}</span>` : ""}
${card?.race != null ? `<span class="bs-badge-race">${BS_SVG_RACE}&thinsp;${card.race}</span>` : ""}
</div>`
: ""
}
<div class="bs-card-name">${card?.name || ""}</div>
`;
}
/* ================================
Haupt-Export
================================ */
export async function loadEvents() {
const body = document.getElementById("qm-body-events");
if (!body) return;
if (!document.querySelector('link[href="/css/events.css"]')) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "/css/events.css";
document.head.appendChild(link);
}
const events = [
{
id: 1,
img: "/images/items/runenhaufen.png",
label: "Booster Öffnen",
type: "booster",
},
{ id: 2, img: "/images/items/1v1.png", label: "1 v 1", type: "arena" },
{ id: 3, img: "/images/items/2v2.png", label: "2 v 2", type: "arena2" },
{
id: 4,
img: "/images/items/holz.png",
label: "Holz Spenden",
type: "wood",
woodCost: 100,
},
{
id: 5,
img: "/images/items/gold.png",
label: "Gold Spenden",
type: "gold",
goldCost: 100,
},
];
// Täglichen Status + Holz-Bestand parallel laden
let completedToday = [];
let playerWood = 0;
let playerGold = 0;
try {
const [statusRes, hudRes] = await Promise.all([
fetch("/api/daily/status"),
fetch("/api/hud"),
]);
if (statusRes.ok) completedToday = (await statusRes.json()).completed || [];
if (hudRes.ok) {
const hud = await hudRes.json();
playerWood = hud.wood || 0;
playerGold = hud.gold || 0;
}
} catch (e) {
console.error("Laden fehlgeschlagen", e);
}
body.innerHTML = `
<div class="events-grid" id="events-grid">
${events
.map((ev) => {
const done = completedToday.includes(ev.id);
const locked =
!done &&
((ev.woodCost && playerWood < ev.woodCost) ||
(ev.goldCost && playerGold < ev.goldCost));
const costLabel = ev.woodCost
? `🪵 ${ev.woodCost} Holz`
: ev.goldCost
? `🪙 ${ev.goldCost} Gold`
: "";
const cls = done ? " event-done" : locked ? " event-locked" : "";
return `
<div class="event-card${cls}" data-event-id="${ev.id}" data-type="${ev.type || ""}" data-done="${done}" data-locked="${locked}">
<div class="event-card-img-wrap">
<img src="${ev.img}" alt="${ev.label}" draggable="false">
${done ? `<div class="event-done-overlay"></div>` : ""}
${locked ? `<div class="event-locked-overlay"><span>${costLabel}<br>benötigt</span></div>` : ""}
</div>
<span class="event-card-label">${ev.label}</span>
</div>`;
})
.join("")}
</div>
<!-- Booster UI -->
<div id="booster-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="booster-back-btn">← Zurück</button>
<div class="booster-stage">
<div class="booster-left">
<img id="booster-stapel" src="/images/items/boosterstapel.png" alt="Booster" draggable="false" class="booster-stapel-img">
<span class="booster-stapel-hint" id="booster-hint">Klicken zum Öffnen</span>
</div>
<div class="booster-slots" id="booster-slots">
${Array.from(
{ length: 5 },
(_, i) => `
<div class="booster-slot" id="booster-slot-${i}">
<div class="booster-slot-inner">
<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">
</div>
</div>`,
).join("")}
</div>
</div>
</div>
<!-- Holz-Spenden UI -->
<div id="wood-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="wood-back-btn"> Zurück</button>
<div class="booster-stage">
<div class="booster-left">
<div class="wood-btn-wrap">
<img id="wood-btn" src="/images/items/holz.png" alt="Holz Spenden" draggable="false" class="booster-stapel-img">
<div class="wood-stamp" id="wood-stamp">Bitte Spenden</div>
</div>
<span class="booster-stapel-hint" id="wood-hint">100 Holz spenden</span>
</div>
<div class="booster-slots">
<div class="booster-slot" id="wood-slot-0">
<div class="booster-slot-inner">
<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">
</div>
</div>
</div>
</div>
</div>
<!-- Gold-Spenden UI -->
<div id="gold-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="gold-back-btn"> Zurück</button>
<div class="booster-stage">
<div class="booster-left">
<div class="wood-btn-wrap">
<img id="gold-btn" src="/images/items/goldmuenze.png" alt="Gold Spenden" draggable="false" class="booster-stapel-img">
<div class="gold-stamp" id="gold-stamp">Bitte Spenden</div>
</div>
<span class="booster-stapel-hint" id="gold-hint">100 Gold spenden</span>
</div>
<div class="booster-slots">
<div class="booster-slot" id="gold-slot-0">
<div class="booster-slot-inner">
<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">
</div>
</div>
</div>
</div>
</div>
<!-- Arena Daily UI (nur 1v1) -->
<div id="arena-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="arena-back-btn"> Zurück</button>
<div class="arena-daily-wrap">
<div class="arena-mode-card-daily" id="arena-1v1-card">
<div class="arena-mode-icon">🗡</div>
<div class="arena-mode-label-daily">1v1</div>
<div class="arena-mode-desc-daily">Einzelkampf Beweis deine Stärke im Duell</div>
</div>
<div id="arena-daily-status" style="display:none;"></div>
</div>
</div>
<!-- Arena Daily UI (2v2 Lobby) -->
<div id="arena2-ui" class="booster-ui" style="display:none; flex-direction:column; gap:14px; padding:16px; overflow-y:auto;">
<div style="display:flex; align-items:center; gap:14px;">
<button class="booster-back-btn" id="arena2-back-btn"> Zurück</button>
<span style="font-family:'Cinzel',serif; font-size:14px; color:#f0d060;"> 2v2 Team-Lobby</span>
</div>
<!-- Team-Panel -->
<div id="daily-team-panel" style="display:none;">
<div class="arena-team-box">
<div class="arena-team-title">Dein Team</div>
<div id="daily-team-players"></div>
<div id="daily-team-actions"></div>
</div>
<div id="daily-team-status" style="margin-top:8px;"></div>
</div>
<!-- Lobby-Liste -->
<div id="daily-lobby-section">
<div class="arena-lobby-actions">
<button class="arena-btn-create" id="daily-create-team-btn"> Eigenes Team erstellen</button>
</div>
<div class="arena-lobby-title">Offene Teams</div>
<div id="daily-lobby-list"><div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div></div>
</div>
</div>
<!-- Standard Detail-Popup -->
<div id="event-detail-overlay">
<div id="event-detail-popup">
<button class="edp-close" id="edp-close-btn"></button>
<img class="edp-img" id="edp-img" src="" alt="">
<div class="edp-title" id="edp-title"></div>
<div class="edp-body" id="edp-body">Inhalt folgt...</div>
</div>
</div>
`;
const overlay = body.querySelector("#event-detail-overlay");
const edpImg = body.querySelector("#edp-img");
const edpTitle = body.querySelector("#edp-title");
const edpBody = body.querySelector("#edp-body");
const boosterUi = body.querySelector("#booster-ui");
const woodUi = body.querySelector("#wood-ui");
const goldUi = body.querySelector("#gold-ui");
const arenaUi = body.querySelector("#arena-ui");
const arena2Ui = body.querySelector("#arena2-ui");
const eventsGrid = body.querySelector("#events-grid");
/* ── Event-Karten ── */
body.querySelectorAll(".event-card").forEach((card) => {
card.addEventListener("click", () => {
if (card.dataset.done === "true") return;
if (card.dataset.locked === "true") return;
if (card.dataset.type === "booster") {
eventsGrid.style.display = "none";
boosterUi.style.display = "flex";
resetBooster();
return;
}
if (card.dataset.type === "arena") {
eventsGrid.style.display = "none";
arenaUi.style.display = "flex";
resetArenaDaily();
return;
}
if (card.dataset.type === "arena2") {
eventsGrid.style.display = "none";
arena2Ui.style.display = "flex";
daily2v2Leave();
open2v2DailyLobby();
return;
}
if (card.dataset.type === "gold") {
eventsGrid.style.display = "none";
goldUi.style.display = "flex";
resetGold();
return;
}
if (card.dataset.type === "wood") {
eventsGrid.style.display = "none";
woodUi.style.display = "flex";
resetWood();
return;
}
const id = Number(card.dataset.eventId);
const ev = events.find((e) => e.id === id);
if (!ev) return;
edpImg.src = ev.img;
edpImg.alt = ev.label;
edpTitle.textContent = ev.label;
edpBody.textContent = "Inhalt folgt...";
overlay.classList.add("active");
});
});
/* ── Daily als erledigt markieren (für nicht-Booster Events) ── */
async function markDailyComplete(eventId) {
try {
await fetch("/api/daily/complete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ eventId }),
});
// Karte visuell als erledigt markieren
const card = body.querySelector(
`.event-card[data-event-id="${eventId}"]`,
);
if (card) {
card.dataset.done = "true";
card.classList.add("event-done");
const wrap = card.querySelector(".event-card-img-wrap");
if (wrap && !wrap.querySelector(".event-done-overlay")) {
wrap.insertAdjacentHTML(
"beforeend",
`<div class="event-done-overlay"></div>`,
);
}
if (!card.querySelector(".event-done-label")) {
card.insertAdjacentHTML(
"beforeend",
`<span class="event-done-label">Bereits erledigt</span>`,
);
}
}
} catch (e) {
console.error("Daily markieren fehlgeschlagen", e);
}
}
body
.querySelector("#edp-close-btn")
.addEventListener("click", () => overlay.classList.remove("active"));
overlay.addEventListener("click", (e) => {
if (e.target === overlay) overlay.classList.remove("active");
});
body.querySelector("#booster-back-btn").addEventListener("click", () => {
if (isSpinning) return;
const boosterCard = body.querySelector('.event-card[data-type="booster"]');
const boosterDone = boosterCard?.dataset.done === "true";
if (!allRevealed && !boosterDone) return;
eventsGrid.style.display = "";
boosterUi.style.display = "none";
clearAllIntervals();
isSpinning = false;
});
body.querySelector("#wood-back-btn").addEventListener("click", () => {
if (isWoodSpinning) return;
const woodCard = body.querySelector('.event-card[data-type="wood"]');
const woodDone = woodCard?.dataset.done === "true";
if (!woodRevealed && !woodDone) return;
eventsGrid.style.display = "";
woodUi.style.display = "none";
clearWoodIntervals();
isWoodSpinning = false;
});
body.querySelector("#gold-back-btn").addEventListener("click", () => {
if (isGoldSpinning) return;
const goldCard = body.querySelector('.event-card[data-type="gold"]');
const goldDone = goldCard?.dataset.done === "true";
if (!goldRevealed && !goldDone) return;
eventsGrid.style.display = "";
goldUi.style.display = "none";
clearGoldIntervals();
isGoldSpinning = false;
});
body.querySelector("#arena-back-btn").addEventListener("click", () => {
if (isArenaSearching) return; // Während Suche gesperrt
eventsGrid.style.display = "";
arenaUi.style.display = "none";
cancelArenaSearch();
});
// ESC wird zentral in quickmenu.js behandelt (verhindert Listener-Stapelung)
/* ── Arena Daily Zustand ── */
let isArenaSearching = false;
function resetArenaDaily() {
isArenaSearching = false;
const card = body.querySelector("#arena-1v1-card");
const status = body.querySelector("#arena-daily-status");
if (card) {
card.classList.remove("searching");
card.querySelector(".arena-mode-label-daily").textContent = "1v1";
card.querySelector(".arena-mode-desc-daily").textContent =
"Einzelkampf Beweis deine Stärke im Duell";
}
if (status) status.style.display = "none";
const backBtn = body.querySelector("#arena-back-btn");
if (backBtn) {
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
}
}
function cancelArenaSearch() {
const socket = window._socket;
if (socket) {
socket.emit("leave_1v1");
socket.off("match_found");
socket.off("queue_status");
}
resetArenaDaily();
}
body.querySelector("#arena-1v1-card").addEventListener("click", async () => {
if (isArenaSearching) return;
const socket = window._socket;
if (!socket) {
showArenaStatus("❌ Keine Verbindung zum Server.", true);
return;
}
// Spielerdaten laden
let me;
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error(res.status);
me = await res.json();
} catch {
showArenaStatus("❌ Spielerdaten konnten nicht geladen werden.", true);
return;
}
isArenaSearching = true;
const card1v1 = body.querySelector("#arena-1v1-card");
card1v1.classList.add("searching");
card1v1.querySelector(".arena-mode-label-daily").textContent = "⏳ Suche…";
card1v1.querySelector(".arena-mode-desc-daily").textContent =
"Warte auf passenden Gegner…";
// Zurück-Button sperren während Suche
const backBtn = body.querySelector("#arena-back-btn");
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
showArenaStatus(`⏳ Suche Gegner (Level ${Math.max(1, me.level - 5)}${me.level + 5})…
<br><span class="arena-cancel-link" id="arena-cancel-link">Suche abbrechen</span>`);
body
.querySelector("#arena-cancel-link")
?.addEventListener("click", () => cancelArenaSearch());
socket.off("match_found");
socket.off("queue_status");
socket.on("queue_status", (data) => {
if (data.status === "waiting") {
const pool = data.poolSize ? ` · ${data.poolSize} im Pool` : "";
showArenaStatus(`⏳ Suche Gegner (Level ${Math.max(1, me.level - 5)}${me.level + 5})${pool}
<br><span class="arena-cancel-link" id="arena-cancel-link">Suche abbrechen</span>`);
body
.querySelector("#arena-cancel-link")
?.addEventListener("click", () => {
cancelArenaSearch();
});
}
});
socket.once("match_found", (data) => {
socket.off("queue_status");
isArenaSearching = false;
// Daily markieren
markDailyComplete(2);
// Match-Overlay + Popup öffnen (aus arena.js Logik)
showArenaMatchFound(me.name, data.opponent.name, () => {
eventsGrid.style.display = "";
arenaUi.style.display = "none";
openArenaMatchPopup(
`/arena/1v1?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`,
data.opponent.name,
data.matchId,
);
});
});
socket.emit("join_1v1", { id: me.id, name: me.name, level: me.level });
});
function showArenaStatus(html, isError = false) {
const box = body.querySelector("#arena-daily-status");
if (!box) return;
box.style.display = "block";
box.style.color = isError ? "#e74c3c" : "#dceb15";
box.innerHTML = html;
if (isError)
setTimeout(() => {
box.style.display = "none";
}, 3000);
}
function showArenaMatchFound(myName, opponentName, onDone) {
if (document.getElementById("match-found-overlay")) return;
const overlay = document.createElement("div");
overlay.id = "match-found-overlay";
overlay.style.cssText =
"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;";
overlay.innerHTML = `
<div style="font-family:'Cinzel',serif;font-size:36px;color:#ffd750;letter-spacing:6px;margin-bottom:12px;">⚔️ Match gefunden!</div>
<div style="font-family:'Cinzel',serif;font-size:18px;color:rgba(255,255,255,0.75);letter-spacing:3px;">${myName} &nbsp;vs&nbsp; ${opponentName}</div>
<div style="width:300px;height:4px;background:rgba(255,215,80,0.2);border-radius:2px;margin-top:24px;overflow:hidden;">
<div id="mfBar" style="height:100%;background:#ffd750;width:0%;border-radius:2px;transition:width 1.5s ease;"></div>
</div>`;
document.body.appendChild(overlay);
requestAnimationFrame(() => {
const b = document.getElementById("mfBar");
if (b) b.style.width = "100%";
});
setTimeout(() => {
overlay.remove();
onDone();
}, 1600);
}
function openArenaMatchPopup(src, opponentName, matchId, mode = "1v1") {
document.getElementById("arena-backdrop")?.remove();
document.getElementById("arena-popup")?.remove();
const backdrop = document.createElement("div");
backdrop.id = "arena-backdrop";
backdrop.style.cssText =
"position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(5px);z-index:9998;";
const popup = document.createElement("div");
popup.id = "arena-popup";
popup.style.cssText =
"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);";
popup.innerHTML = `
<div style="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;">
<span style="font-family:'Cinzel',serif;font-size:13px;letter-spacing:4px;color:rgba(255,215,80,0.85);">⚔️ ${mode} · vs ${opponentName}</span>
<span style="font-size:11px;color:rgba(255,255,255,0.22);">${matchId}</span>
</div>
<iframe src="${src}" style="flex:1;border:none;width:100%;display:block;" allowfullscreen></iframe>`;
document.body.appendChild(backdrop);
document.body.appendChild(popup);
}
body.querySelector("#arena2-back-btn").addEventListener("click", () => {
if (isArena2InMatch) return;
eventsGrid.style.display = "";
arena2Ui.style.display = "none";
daily2v2Leave();
});
/* ── Arena2 Daily Zustand (2v2 Lobby) ── */
let isArena2InMatch = false;
let daily2v2TeamId = null;
let daily2v2Me = null;
async function open2v2DailyLobby() {
const socket = window._socket;
if (!socket) {
showDaily2v2Error("❌ Keine Verbindung zum Server.");
return;
}
if (!daily2v2Me) {
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error(res.status);
daily2v2Me = await res.json();
} catch {
showDaily2v2Error("❌ Spielerdaten konnten nicht geladen werden.");
return;
}
}
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.emit("get_2v2_lobbies");
socket.on("2v2_lobbies", (list) => renderDaily2v2Lobbies(list, socket));
socket.on("2v2_team_joined", (data) => {
daily2v2TeamId = data.teamId;
body.querySelector("#daily-team-panel").style.display = "block";
body.querySelector("#daily-lobby-section").style.display = "none";
});
socket.on("2v2_team_update", (data) => renderDaily2v2Team(data, socket));
socket.on("2v2_partner_left", (data) => {
const s = body.querySelector("#daily-team-status");
if (s)
s.innerHTML = `<span style="color:#e74c3c;">⚠️ ${data.name} hat das Team verlassen.</span>`;
});
socket.on("2v2_searching", () => {
const s = body.querySelector("#daily-team-status");
if (s)
s.innerHTML = `<div class="arena-searching-box">⏳ Suche nach Gegnerteam…</div>`;
const a = body.querySelector("#daily-team-actions");
if (a) a.innerHTML = "";
// Zurück sperren während Suche
const backBtn = body.querySelector("#arena2-back-btn");
if (backBtn) {
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
}
isArena2InMatch = true;
});
socket.once("match_found_2v2", (data) => {
socket.off("2v2_lobbies");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
isArena2InMatch = false;
markDailyComplete(3);
showArenaMatchFound(
daily2v2Me.name,
`Team ${data.myTeam === 1 ? 2 : 1}`,
() => {
eventsGrid.style.display = "";
arena2Ui.style.display = "none";
openArenaMatchPopup(
`/arena/2v2?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`,
data.opponents?.join(" & ") || "Gegner",
data.matchId,
"2v2",
);
},
);
});
socket.on("2v2_error", (data) => showDaily2v2Error(`${data.message}`));
// Buttons
body.querySelector("#daily-create-team-btn").onclick = () => {
socket.emit("create_2v2_team", daily2v2Me);
};
}
function renderDaily2v2Lobbies(list, socket) {
const el = body.querySelector("#daily-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(
(t) => `
<div class="arena-lobby-row">
<div class="arena-lobby-row-info">
<span class="arena-lobby-leader">⚔️ ${t.leader}</span>
<span class="arena-lobby-level">Lvl ${t.leaderLevel}</span>
<span class="arena-lobby-count">${t.count}/2</span>
</div>
<button class="arena-btn-join" data-teamid="${t.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: daily2v2Me,
});
});
});
}
function renderDaily2v2Team(data, socket) {
const playersEl = body.querySelector("#daily-team-players");
const actionsEl = body.querySelector("#daily-team-actions");
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>`;
} else {
const myEntry = data.players.find((p) => p.name === daily2v2Me?.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="daily-ready-btn">⚔️ Bereit</button>`;
if (!iAmReady) {
body
.querySelector("#daily-ready-btn")
?.addEventListener("click", () => {
socket.emit("2v2_player_ready", { teamId: daily2v2TeamId });
});
}
}
}
function daily2v2Leave() {
const socket = window._socket;
if (socket) {
socket.emit("leave_2v2_team");
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");
}
daily2v2TeamId = null;
isArena2InMatch = false;
// Reset UI
const panel = body.querySelector("#daily-team-panel");
const section = body.querySelector("#daily-lobby-section");
const backBtn = body.querySelector("#arena2-back-btn");
if (panel) panel.style.display = "none";
if (section) section.style.display = "";
if (backBtn) {
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
}
}
function showDaily2v2Error(msg) {
const s = body.querySelector("#daily-team-status");
if (!s) return;
s.innerHTML = `<span style="color:#e74c3c;">${msg}</span>`;
setTimeout(() => {
s.innerHTML = "";
}, 3000);
}
// Lobby beim Öffnen des Arena2-UI initialisieren
const origArena2Click = body.querySelector('.event-card[data-type="arena2"]');
if (origArena2Click) {
origArena2Click.addEventListener("click", () => {
// open2v2DailyLobby wird nach dem UI-Wechsel in resetArena2Daily aufgerufen
});
}
/* ── Booster Zustand ── */
let allCards = [];
let isSpinning = false;
let allRevealed = false; // true sobald alle 5 Karten aufgedeckt + gespeichert
let spinIntervals = {}; // { index: intervalId }
let slotLocked = {}; // { index: true } → verhindert Überschreiben nach reveal
function clearAllIntervals() {
Object.values(spinIntervals).forEach((id) => clearInterval(id));
spinIntervals = {};
slotLocked = {};
}
async function preloadCards() {
if (allCards.length) return;
try {
const res = await fetch("/api/booster/cards");
if (!res.ok) throw new Error(res.status);
allCards = await res.json();
} catch (e) {
console.error("Karten laden fehlgeschlagen", e);
}
}
function resetBooster() {
clearAllIntervals();
isSpinning = false;
allRevealed = false;
for (let i = 0; i < 5; i++) {
const inner = body.querySelector(
`#booster-slot-${i} .booster-slot-inner`,
);
inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
body
.querySelector(`#booster-slot-${i}`)
.classList.remove("revealed", "spinning");
}
const stapel = body.querySelector("#booster-stapel");
stapel.classList.remove("used");
stapel.style.opacity = "1";
stapel.style.cursor = "pointer";
body.querySelector("#booster-hint").textContent = "Klicken zum Öffnen";
preloadCards();
}
/* ── Slot drehen 350ms, damit man die Karten erkennt ── */
function startSpinSlot(index) {
slotLocked[index] = false;
const slot = body.querySelector(`#booster-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.add("spinning");
const iv = setInterval(() => {
if (!allCards.length || slotLocked[index]) return;
const rnd = allCards[Math.floor(Math.random() * allCards.length)];
inner.innerHTML = cardHTML(rnd, true);
}, 150);
spinIntervals[index] = iv;
}
/* ── Slot enthüllen Sperre setzen BEVOR clearInterval ── */
function revealSlot(index, card) {
slotLocked[index] = true; // zuerst sperren
clearInterval(spinIntervals[index]); // dann stoppen
delete spinIntervals[index];
console.log(`[Booster] Slot ${index} enthüllt:`, card);
const slot = body.querySelector(`#booster-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.remove("spinning");
slot.classList.add("revealed");
// innerHTML setzen und danach nochmal sicherstellen (doppelte Absicherung)
inner.innerHTML = cardHTML(card, true);
setTimeout(() => {
if (slot.classList.contains("revealed")) {
inner.innerHTML = cardHTML(card, true);
}
}, 100);
}
/* ── Stapel klicken ── */
body.querySelector("#booster-stapel").addEventListener("click", async () => {
if (isSpinning) return;
if (!allCards.length) await preloadCards();
if (!allCards.length) return;
isSpinning = true;
const stapel = body.querySelector("#booster-stapel");
stapel.classList.add("used");
stapel.style.opacity = "0.35";
stapel.style.cursor = "default";
body.querySelector("#booster-hint").textContent = "Wird gezogen...";
// Zurück-Button während der Animation sperren
const backBtn = body.querySelector("#booster-back-btn");
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
let drawnCards = [];
try {
const res = await fetch("/api/booster/open", { method: "POST" });
const text = await res.text();
console.log("[Booster] API Antwort raw:", text);
const data = JSON.parse(text);
console.log("[Booster] API Antwort parsed:", data);
drawnCards = data.cards || [];
console.log("[Booster] drawnCards:", drawnCards);
} catch (e) {
console.error("Booster öffnen fehlgeschlagen", e);
resetBooster();
return;
}
for (let i = 0; i < 5; i++) startSpinSlot(i);
for (let i = 0; i < 5; i++) {
setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000);
}
setTimeout(
async () => {
body.querySelector("#booster-hint").textContent =
"Karten werden gespeichert...";
isSpinning = false;
// Karten in user_cards speichern
try {
const cardIds = drawnCards.map((c) => c.id);
const saveRes = await fetch("/api/booster/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cardIds }),
});
if (!saveRes.ok) throw new Error(saveRes.status);
} catch (e) {
console.error("Karten speichern fehlgeschlagen", e);
}
// Booster-Daily als erledigt markieren (event_id = 1)
await markDailyComplete(1);
allRevealed = true;
body.querySelector("#booster-hint").textContent =
"Alle Karten erhalten! ✓";
const backBtn = body.querySelector("#booster-back-btn");
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
},
5 * 5000 + 500,
);
});
preloadCards();
/* ================================
Holz-Spenden Zustand
================================ */
let isWoodSpinning = false;
let woodRevealed = false;
let woodIntervals = {};
let woodLocked = {};
let rarity3Cards = [];
async function preloadRarity3() {
if (rarity3Cards.length) return;
try {
const res = await fetch("/api/booster/cards");
if (!res.ok) throw new Error(res.status);
const all = await res.json();
rarity3Cards = all.filter((c) => parseInt(c.rarity) === 3);
// Fallback: wenn keine rarity-3 vorhanden alle nehmen
if (!rarity3Cards.length) rarity3Cards = all;
} catch (e) {
console.error("Rarity-3 Karten laden fehlgeschlagen", e);
}
}
function clearWoodIntervals() {
Object.values(woodIntervals).forEach((id) => clearInterval(id));
woodIntervals = {};
woodLocked = {};
}
function resetWood() {
clearWoodIntervals();
isWoodSpinning = false;
woodRevealed = false;
const inner = body.querySelector(`#wood-slot-0 .booster-slot-inner`);
inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
body.querySelector(`#wood-slot-0`).classList.remove("revealed", "spinning");
const btn = body.querySelector("#wood-btn");
const stamp = body.querySelector("#wood-stamp");
btn.classList.remove("used");
btn.style.opacity = "1";
btn.style.cursor = "pointer";
if (stamp) stamp.style.display = "";
body.querySelector("#wood-hint").textContent = "100 Holz spenden";
preloadRarity3();
}
function startWoodSlot(index) {
woodLocked[index] = false;
const slot = body.querySelector(`#wood-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.add("spinning");
const iv = setInterval(() => {
if (!rarity3Cards.length || woodLocked[index]) return;
const rnd = rarity3Cards[Math.floor(Math.random() * rarity3Cards.length)];
inner.innerHTML = cardHTML(rnd, true);
}, 150);
woodIntervals[index] = iv;
}
function revealWoodSlot(index, card) {
woodLocked[index] = true;
clearInterval(woodIntervals[index]);
delete woodIntervals[index];
const slot = body.querySelector(`#wood-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.remove("spinning");
slot.classList.add("revealed");
inner.innerHTML = cardHTML(card, true);
setTimeout(() => {
if (slot.classList.contains("revealed"))
inner.innerHTML = cardHTML(card, true);
}, 100);
}
body.querySelector("#wood-btn").addEventListener("click", async () => {
if (isWoodSpinning) return;
if (!rarity3Cards.length) await preloadRarity3();
if (!rarity3Cards.length) return;
isWoodSpinning = true;
const btn = body.querySelector("#wood-btn");
const stamp = body.querySelector("#wood-stamp");
btn.classList.add("used");
btn.style.opacity = "0.35";
btn.style.cursor = "default";
if (stamp) stamp.style.display = "none";
body.querySelector("#wood-hint").textContent = "Wird verarbeitet...";
const backBtn = body.querySelector("#wood-back-btn");
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
// API aufrufen 100 Holz abziehen + Karte ziehen
let drawnCard = null;
try {
const res = await fetch("/api/booster/wood-donate", { method: "POST" });
const data = await res.json();
if (!res.ok) {
body.querySelector("#wood-hint").textContent = data.error || "Fehler";
isWoodSpinning = false;
btn.classList.remove("used");
btn.style.opacity = "1";
btn.style.cursor = "pointer";
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
return;
}
drawnCard = data.card;
} catch (e) {
console.error("Holz-Spenden fehlgeschlagen", e);
resetWood();
return;
}
// Nur Slot 0 starten
startWoodSlot(0);
// Nach 5 Sekunden enthüllen
setTimeout(() => {
revealWoodSlot(0, drawnCard);
body.querySelector("#wood-hint").textContent = "Karte erhalten! ✓";
isWoodSpinning = false;
woodRevealed = true;
markDailyComplete(4);
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
}, 5000);
});
preloadRarity3();
/* ================================
Gold-Spenden Zustand
================================ */
let isGoldSpinning = false;
let goldRevealed = false;
let goldIntervals = {};
let goldLocked = {};
function clearGoldIntervals() {
Object.values(goldIntervals).forEach((id) => clearInterval(id));
goldIntervals = {};
goldLocked = {};
}
function resetGold() {
clearGoldIntervals();
isGoldSpinning = false;
goldRevealed = false;
const inner = body.querySelector(`#gold-slot-0 .booster-slot-inner`);
inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
body.querySelector(`#gold-slot-0`).classList.remove("revealed", "spinning");
const btn = body.querySelector("#gold-btn");
const stamp = body.querySelector("#gold-stamp");
btn.classList.remove("used");
btn.style.opacity = "1";
btn.style.cursor = "pointer";
if (stamp) stamp.style.display = "";
body.querySelector("#gold-hint").textContent = "100 Gold spenden";
preloadRarity3();
}
function startGoldSlot(index) {
goldLocked[index] = false;
const slot = body.querySelector(`#gold-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.add("spinning");
const iv = setInterval(() => {
if (!rarity3Cards.length || goldLocked[index]) return;
const rnd = rarity3Cards[Math.floor(Math.random() * rarity3Cards.length)];
inner.innerHTML = cardHTML(rnd, true);
}, 150);
goldIntervals[index] = iv;
}
function revealGoldSlot(index, card) {
goldLocked[index] = true;
clearInterval(goldIntervals[index]);
delete goldIntervals[index];
const slot = body.querySelector(`#gold-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.remove("spinning");
slot.classList.add("revealed");
inner.innerHTML = cardHTML(card, true);
setTimeout(() => {
if (slot.classList.contains("revealed"))
inner.innerHTML = cardHTML(card, true);
}, 100);
}
body.querySelector("#gold-btn").addEventListener("click", async () => {
if (isGoldSpinning) return;
if (!rarity3Cards.length) await preloadRarity3();
if (!rarity3Cards.length) return;
isGoldSpinning = true;
const btn = body.querySelector("#gold-btn");
const stamp = body.querySelector("#gold-stamp");
btn.classList.add("used");
btn.style.opacity = "0.35";
btn.style.cursor = "default";
if (stamp) stamp.style.display = "none";
body.querySelector("#gold-hint").textContent = "Wird verarbeitet...";
const backBtn = body.querySelector("#gold-back-btn");
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
let drawnCard = null;
try {
const res = await fetch("/api/booster/gold-donate", { method: "POST" });
const data = await res.json();
if (!res.ok) {
body.querySelector("#gold-hint").textContent = data.error || "Fehler";
isGoldSpinning = false;
btn.classList.remove("used");
btn.style.opacity = "1";
btn.style.cursor = "pointer";
if (stamp) stamp.style.display = "";
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
return;
}
drawnCard = data.card;
} catch (e) {
console.error("Gold-Spenden fehlgeschlagen", e);
resetGold();
return;
}
startGoldSlot(0);
setTimeout(() => {
revealGoldSlot(0, drawnCard);
body.querySelector("#gold-hint").textContent = "Karte erhalten! ✓";
isGoldSpinning = false;
goldRevealed = true;
markDailyComplete(5);
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
}, 5000);
});
}